티스토리 뷰

개발/C++

[ 씹어먹는 C++ ] 16~17

정으주 2024. 11. 17. 03:00

16-3. 타입을 알려주는 키워드 decltype 와 친구 std::declval

 

1. decltype

C++11에서 도입된 decltype 키워드는 식의 타입을 알아내는데 사용하는 키워드이다.

decltype(/* 타입을 알고자 하는 식*/)

 

기본적인 사용 예시:

#include <iostream>

struct A {
  double d;
};

int main() {
  int a = 3;
  decltype(a) b = 2;  // int

  int& r_a = a;
  decltype(r_a) r_b = b;  // int&

  int&& x = 3;
  decltype(x) y = 2;  // int&&

  A* aa;
  decltype(aa->d) dd = 0.1;  // double
}

 

 

2. 값 카테고리

C++에서 모든 식(expression)에는 두 가지 정보가 따라다닌다:

  1. 식의 타입
  2. 값 카테고리

 

값 카테고리는 크게 세 가지로 나뉜다:

  1. lvalue
    • 정체를 알 수 있고 이동시킬 수 없는 값
    • 예: 변수, 함수 이름, 문자열 리터럴
  2. prvalue
    • 정체를 알 수 없지만 이동시킬 수 있는 값
    • 예: 리터럴(문자열 제외), 산술 연산 결과
  3. xvalue
    • 정체를 알 수 있고 이동시킬 수 있는 값
    • 예: std::move() 결과

 

3. decltype의 타입 추론 규칙

decltype은 전달받은 식에 따라 다음과 같은 규칙으로 타입을 결정한다:

  1. 식별자 표현식의 경우: 해당 식의 타입 그대로
  2. 그 외의 경우:
    • xvalue면 T&&
    • lvalue면 T&
    • prvalue면 T

예시:

int a, b;
decltype(a + b) c;  // int (a + b는 prvalue)
decltype((a)) d;    // int& ((a)는 lvalue)

 

4. std::declval 사용하기

 

std::declval은 생성자 호출 없이 타입의 멤버 함수 타입을 알아낼 때 유용하다

#include <utility>

template <typename T>
decltype(std::declval<T>().f()) call_f_and_return(T& t) {
  return t.f();
}

struct A {
  int f() { return 0; }
};

struct B {
  B(int x) {}  // 기본 생성자 없음
  int f() { return 0; }
};

int main() {
  A a;
  B b(1);

  call_f_and_return(a);  // OK
  call_f_and_return(b);  // OK - declval 덕분에 가능
}

 

5. deltype의 유용한 사용 사례

1. 정확한 타입 복사:

const int i = 4;
auto j = i;         // int
decltype(i) k = i;  // const int

 

2. 배열 타입 보존:

int arr[10];
auto arr2 = arr;     // int*
decltype(arr) arr3;  // int[10]

 

3. 템플릿 함수의 리턴 타입 추론:

template <typename T, typename U>
auto add(T t, U u) -> decltype(t + u) {
  return t + u;
}

 

C++14부터는 함수의 리턴 타입을 auto로 지정하면 컴파일러가 자동으로 추론해주므로 더 간단하게 작성할 수 있다.

 

 

 


 

17-1. type_traits 라이브러리, SFINAE, enable_if

 

1. 템플릿 메타 함수와 type_traits

템플릿 메타 함수는 값을 다루는 일반 함수와 달리 타입을 다룬다는 점이 특징이다.

 

#include <iostream>
#include <type_traits>

template <typename T>
void tell_type() {
    if (std::is_void<T>::value) {
        std::cout << "T 는 void!\n";
    } else {
        std::cout << "T 는 void 가 아니다.\n";
    }
}

int main() {
    tell_type<int>();  // void 아님!
    tell_type<void>(); // void!
}

 

위 코드에서 std::is_void주어진 타입이 void인지 판별하는 메타 함수이다.

실제로는 템플릿 특수화로 구현되어 있다. 모든 타입에 대해 value가 false가 되지만, void 타입에 대해 특수화하여 value를 true로 설정한다.

 

template <typename T>
struct is_void {
    static constexpr bool value = false;
};

template <>
struct is_void<void> {
    static constexpr bool value = true;
};

 

 

2. type_traits의 다양한 메타 함수

C++ 표준 라이브러리 type_traits는 타입을 분석하거나 조작하는 여러 메타 함수를 제공한다. 몇 가지 대표적인 메타 함수는 다음과 같다.

 

  • std::is_integral

타입이 정수형인지 판단한다

#include <iostream>
#include <type_traits>

std::cout << std::is_integral<int>::value; // 1 (true)
std::cout << std::is_integral<float>::value; // 0 (false)

 

 

  • std::is_class

타입이 클래스인지 확인한다.

namespace detail {
    template <typename T>
    char test(int T::*);

    struct two { char c[2]; };

    template <typename T>
    two test(...);
}

template <typename T>
struct is_class
    : std::integral_constant<bool, sizeof(detail::test<T>(0)) == 1> {};


std::is_class의 구현 방식은 독특한데, 데이터 멤버를 가리키는 포인터 문법을 이용한다. 클래스에서만 사용 가능한 문법을 통해 클래스를 판별하는 방식이다.

 

  • std::is_integral_v, std::is_class_v (C++17 이후)

value를 직접 사용하는 대신 _v 접미사를 붙여 간단하게 사용할 수 있다.

 

3. SFINAE (치환 오류는 컴파일 오류가 아니다)

C++에서 템플릿 인자를 치환할 때 문법적으로 잘못된 경우 SFINAE 규칙에 따라 컴파일 오류를 발생시키지 않고 해당 함수가 오버로딩 후보에서 제외된다.

#include <iostream>

template <typename T>
void test(typename T::x a) {
    std::cout << "T::x 호출됨\n";
}

template <typename T>
void test(typename T::y b) {
    std::cout << "T::y 호출됨\n";
}

struct A { using x = int; };
struct B { using y = int; };

int main() {
    test<A>(33);  // T::x 호출됨
    test<B>(22);  // T::y 호출됨
}

 

위 코드에서 T::x와 T::y 중 문법적으로 유효한 것만 오버로딩 후보로 남게 된다. SFINAE는 템플릿 메타프로그래밍의 핵심 규칙 중 하나이다.

 

4. enable_if 로 조건부 템플릿 정의

enable_if특정 조건이 참일 때에만 템플릿을 활성화할 수 있도록 돕는다. 다음은 정수형 타입만 인자로 받는 함수의 예이다.

#include <iostream>
#include <type_traits>

template <typename T, 
          typename = typename std::enable_if<std::is_integral<T>::value>::type>
void test(const T& t) {
    std::cout << "t: " << t << "\n";
}

int main() {
    test(1);    // OK
    test('c');  // OK
    // test(3.14); // 컴파일 오류
}

 

enable_if의 첫 번째 인자는 조건식이고, 두 번째는 조건이 참일 경우 활성화할 타입이다. 조건이 거짓이면 type이 정의되지 않으므로 SFINAE에 의해 제외된다.

 

 

5. void_t로 코드 간소화하기

C++17에서 추가된 void_t는 여러 조건식을 한 번에 처리하는 데 유용하다. 예를 들어 특정 컨테이너의 begin과 end가 존재하는지 확인하고 싶을 때 다음과 같이 작성할 수 있다.

#include <iostream>
#include <type_traits>

template <typename Cont,
          typename = std::void_t<decltype(std::declval<Cont>().begin()),
                                 decltype(std::declval<Cont>().end())>>
void print(const Cont& container) {
    for (const auto& elem : container) {
        std::cout << elem << " ";
    }
    std::cout << "\n";
}

int main() {
    std::vector<int> v = {1, 2, 3};
    print(v); // [ 1 2 3 ]
}

 

void_t는 주어진 조건이 모두 유효할 경우 void로 평가되며, 조건 중 하나라도 잘못되면 해당 템플릿 함수는 오버로딩 후보에서 제외된다.


 

17-2. C++ 정규 표현식(<regex>) 라이브러리 소개

 

1. 전체 문자열 매칭하기: std::regex_match

정규 표현식을 사용해 문자열 전체가 특정 패턴과 일치하는지 확인할 수 있다. 예를 들어, 서버 로그 파일 이름이 **db-123-log.txt**와 같은 형식인지 확인하려면 다음과 같이 작성할 수 있다.

#include <iostream>
#include <regex>
#include <vector>

int main() {
    std::vector<std::string> file_names = {"db-123-log.txt", "db-124-log.txt", 
                                           "not-db-log.txt", "db-12-log.txt", 
                                           "db-12-log.jpg"};
    std::regex re("db-\\d*-log\\.txt");

    for (const auto& file_name : file_names) {
        std::cout << file_name << ": " << std::boolalpha
                  << std::regex_match(file_name, re) << '\n';
    }
}

 

실행 결과:

db-123-log.txt: true
db-124-log.txt: true
not-db-log.txt: false
db-12-log.txt: true
db-12-log.jpg: false

 

**std::regex_match**는 문자열 전체가 정규 표현식과 일치할 경우에만 true를 반환한다. 여기서 사용된 std::regex 객체는 패턴을 정의하며, 정규 표현식 문법은 기본적으로 ECMAScript 표준을 따른다.

 

2. 문자열 일부 검색하기: std::regex_search

문자열의 일부가 특정 패턴에 부합하는지 검색하려면 std::regex_search를 사용할 수 있다. 예를 들어, HTML 코드에서 특정 태그를 추출하려면 다음과 같이 작성한다.

#include <iostream>
#include <regex>

int main() {
    std::string html = R"(
        <div class="sk-circle1 sk-circle">a</div>
        <div class="sk-circle2 sk-circle">b</div>
        <div class="sk-circle3 sk-circle">asd</div>
    )";

    std::regex re(R"(<div class="sk[\w -]*">[\w]*</div>)");
    std::smatch match;

    while (std::regex_search(html, match, re)) {
        std::cout << match.str() << '\n';
        html = match.suffix(); // 검색된 패턴 뒤의 문자열로 업데이트
    }
}

 

실행 결과:

<div class="sk-circle1 sk-circle">a</div>
<div class="sk-circle2 sk-circle">b</div>
<div class="sk-circle3 sk-circle">asd</div>

 

std::regex_search는 문자열의 일부분이 정규 표현식과 매칭되면 true를 반환하며, 매칭된 결과를 std::smatch 객체에 저장한다.

 

3. 문자열 치환하기: std::regex_replace

특정 패턴에 맞는 문자열을 다른 문자열로 치환하려면 std::regex_replace를 사용한다. 예를 들어, sk-circle1 같은 문자열을 1-sk-circle로 변환하려면 다음과 같이 작성한다.

#include <iostream>
#include <regex>

int main() {
    std::string html = R"(
        <div class="sk-circle1 sk-circle">a</div>
        <div class="sk-circle2 sk-circle">b</div>
        <div class="sk-circle3 sk-circle">asd</div>
    )";

    std::regex re(R"(sk-circle(\d))");
    std::string modified_html = std::regex_replace(html, re, "$1-sk-circle");

    std::cout << modified_html;
}

 

실행결과:

<div class="1-sk-circle">a</div>
<div class="2-sk-circle">b</div>
<div class="3-sk-circle">asd</div>

 

여기서 사용된 $1은 정규 표현식의 캡쳐 그룹으로, (\d)에 매칭된 숫자를 나타낸다. 치환 문자열에서 캡쳐 그룹을 활용하면 정교한 변환이 가능하다.

 

캡쳐 그룹과 중첩된 치환

캡쳐 그룹을 중첩해 더 복잡한 패턴을 처리할 수도 있다. 예를 들어, <div class="sk-circle1 sk-circle">를 <div class="1-sk-circle">로 변경하려면 다음과 같이 작성한다.

#include <iostream>
#include <regex>

int main() {
    std::string html = R"(
        <div class="sk-circle1 sk-circle">a</div>
        <div class="sk-circle2 sk-circle">b</div>
        <div class="sk-circle3 sk-circle">asd</div>
    )";

    std::regex re(R"((sk-circle(\d) sk-circle))");
    std::string modified_html = std::regex_replace(html, re, "$2-sk-circle");

    std::cout << modified_html;
}

 

실행 결과:

<div class="1-sk-circle">a</div>
<div class="2-sk-circle">b</div>
<div class="3-sk-circle">asd</div>

 

중첩된 캡쳐 그룹에서 괄호가 열리는 순서대로 $1, $2 등이 할당된다.

 


 

17-3. 난수 생성(<random>)과 시간 관련 라이브러리(<chrono>) 소개

1. 난수 생성: <random>

C 스타일의 rand()는 난수 생성에 있어 여러 단점이 존재한다:

  1. 시드값의 제한적 변경: srand(time(NULL))은 같은 초에 실행되는 프로그램에서 동일한 난수열을 생성한다.
  2. 비균등한 난수 생성: rand() % 100 방식은 특정 값에 치우친 결과를 낼 수 있다.
  3. 낮은 품질의 난수: rand()는 선형 합동 생성기를 사용하여 상관 관계가 높은 난수를 생성한다.

C++에서는 <random> 라이브러리를 통해 더 나은 품질의 난수를 생성할 수 있다.

#include <iostream>
#include <random>

int main() {
    std::random_device rd;                // 진짜 난수를 제공하는 random_device
    std::mt19937 gen(rd());               // Mersenne Twister 엔진 초기화
    std::uniform_int_distribution<int> dis(0, 99); // 0~99 균등 분포

    for (int i = 0; i < 5; i++) {
        std::cout << "난수 : " << dis(gen) << std::endl;
    }
}

 

실행 결과:

난수 : 42
난수 : 73
난수 : 15
난수 : 88
난수 : 7

 

 

  • std::random_device: 운영체제 기반의 진정한 난수를 제공한다. 느리기 때문에 초기화에만 사용한다.
  • std::mt19937: 고성능 Mersenne Twister 엔진으로, rand()보다 품질이 우수하다.
  • std::uniform_int_distribution: 특정 범위 내의 값을 균등하게 생성한다.

 

2. 다양한 분포 생성

 

<random>은 균등 분포 외에도 다양한 분포를 제공한다.

 

정규 분포(Normal distribution) 생성:

#include <iostream>
#include <map>
#include <random>

int main() {
    std::random_device rd;
    std::mt19937 gen(rd());
    std::normal_distribution<> dist(0, 1); // 평균 0, 표준편차 1

    std::map<int, int> histogram;
    for (int i = 0; i < 10000; i++) {
        ++histogram[std::round(dist(gen))];
    }

    for (const auto& p : histogram) {
        std::cout << p.first << ": " << std::string(p.second / 100, '*') << '\n';
    }
}

 

실행 결과:

-3: *
-2: ****
-1: **************
 0: ********************
 1: ***************
 2: ***
 3: *

 

정규 분포에서 무작위 샘플을 생성하여 히스토그램 형태로 출력한다.

 

3. 시간 측정: <chrono>

<chrono>는 다음과 같은 주요 요소로 구성된다:

  1. clock: 현재 시간을 제공한다. 예: system_clock, high_resolution_clock.
  2. time_point: 특정 시간을 나타낸다.
  3. duration: 시간 간격을 나타낸다.

chrono를 사용해 코드 실행 시간을 측정하는 예:

#include <iostream>
#include <chrono>
#include <random>
#include <vector>

int main() {
    std::random_device rd;
    std::mt19937 gen(rd());
    std::uniform_int_distribution<> dist(0, 1000);

    for (int n = 1; n <= 1000000; n *= 10) {
        std::vector<int> numbers;
        numbers.reserve(n);

        auto start = std::chrono::high_resolution_clock::now();
        for (int i = 0; i < n; ++i) {
            numbers.push_back(dist(gen));
        }
        auto end = std::chrono::high_resolution_clock::now();

        auto duration = std::chrono::duration_cast<std::chrono::microseconds>(end - start);
        std::cout << n << "개 난수 생성 시간: " << duration.count() << "us" << std::endl;
    }
}

 

실행 결과:

1개 난수 생성 시간: 1us
10개 난수 생성 시간: 2us
100개 난수 생성 시간: 15us
1000개 난수 생성 시간: 150us
10000개 난수 생성 시간: 1200us

 

 

  • high_resolution_clock: 매우 정밀한 시계로, 성능 측정에 적합하다.
  • duration_cast: 시간 차이를 원하는 단위(마이크로초, 밀리초 등)로 변환한다.
  • count(): 시간 차이 값을 반환한다.

 

4. 현재 시간 가져오기

현재 시간을 날짜와 시간 형식으로 출력하려면:

#include <iostream>
#include <chrono>
#include <ctime>
#include <iomanip>

int main() {
    auto now = std::chrono::system_clock::now();
    std::time_t t = std::chrono::system_clock::to_time_t(now);
    std::cout << "현재 시간: " << std::put_time(std::localtime(&t), "%F %T") << '\n';
}

 

 

실행 결과:

현재 시간: 2024-11-18 13:45:12

 

std::put_time은 날짜와 시간을 형식화하여 출력한다. %F, %T 등은 strftime과 동일한 형식을 따른다.

 

 

 

17-4

17-5

 

 

 

 

 

 

 

 

.

 

 

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/05   »
1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30 31
글 보관함