Search
Duplicate

전문가를 위한 C++/ 여러 가지 유틸리티 라이브러리

(전체가 아니라 C#과 차이가 있는 부분을 중심으로 요약 정리)

Ratio 라이브러리

ratio 라이브러리를 이용하면 유한 유리수(finite rational number)를 컴파일 시간에 정확히 표현할 수 있다. 유리수는 다음 절에서 설명할 std::chrono::duration 클래스에서 사용된다. 이 라이브러리에 관련된 내용은 모두 <ratio> 헤더 파일의 std 네임스페이스 아래에 정의돼 있다.
유리수를 구성하는 분자와 분모는 std::intmax_t 타입의 컴파일 상수로 표현한다. 이 타입은 부호 있는 저웃 타입으로 최댓값은 컴파일러마다 다르다. 여기서 제공하는 유리수는 컴파일 시간에 결정되기 때문에 다른 타입에 비해 사용법이 다소 복잡해 보일 수 있다.
ratio 객체를 정의하는 방식은 일반 객체와 다르다. 메서드를 호출할 수 없으며 타입 앨리어스처럼 사용해야 한다. 예컨대 1/60이라는 유리수를 컴파일 시간 상수로 선언하면 다음과 같다.
using r1 = ratio<1, 60>
C++
r1 유리수의 분자와 분모는 컴파일 시간 상수이며 다음과 같이 접근한다.
intmax_t num = r1::num; intmax_t den = r1::den;
C++
다시 한 번 강조하면 ratio는 컴파일 시간 상수(compile-time constant)라서 분자와 분모가 컴파일 시간에 결정된다. 따라서 다음과 같이 작성하면 컴파일 에러가 발생한다.
intmax_t n = 1; intmax_t d = 60; using r1 = ratio<n, d>; // error
C++
유리수는 항상 정규화(normalized, 약분)된 상태로 표현된다.  유리수 ratio<n, d>에 대해 최대공약수가 gcd일 때 분자 nm과 분모 den은 다음과 같이 결정된다.
num = sign(n) * sign(d) * abs(n) / gcd
den = abs(d) / gdc
ratio 라이브러리는 유리수의 덧셈, 뺄셈, 곱셈, 나눗셈을 지원한다. 이 연산도 모두 컴파일 시간에 처리된다. 그래서 표준 산술 연산을 적용할 수 없고, 타입 앨리어스를 이용한 특수 템플릿으로 처리해야 한다.
이러한 용도로 제공되는 산술 ratio 템플릿으로는 ratio_add, ratio_subtract, ratio_multiply, ratio_divide가 있다. 이 템플릿은 계산 결과를 새로운 ratio 타입으로 표현한다. 이 타입은 C++에 정의된 type이라는 타입 앨리어스로 접근한다.
예컨대 다음 코드는 1/60과 1/30에 대한 ratio 값을 정의한다. 그런 다음 ratio_add 템플릿으로 두 유리수를 더해서 result란 유리수를 구하는데, 그 값은 합한 결과를 약분한 1/20이다.
using r1 = ratio<1, 60>; using r2 = ratio<1, 30>; using result = ratio_add<r1, r2>::type;
C++
C++ 표준은 ratio 비교 연산 템플릿(ratio_equal, ratio_not_equal, ratio_less, ratio_less_equal, ratio_greater, ratio_greater_equal)도 제공한다. 산술 ratio 템플릿과 마찬가지로 ratio 비교 연산 템플릿도 컴파일 시간에 처리된다.
이런 비교 연산 템플릿은 결과를 표현하는 std::bool_constant란 타입을 새로 정의한다. bool_constant는 타입과 컴파일 시간 상숫값을 저장하는 struct 템플릿인 std::integral_constant 중 하나다. 예컨대 integral_constant<int, 15>는 15라는 정숫값을 저장한다.
bool_constant는 bool 타입에 대한 integral_constant이다. 예컨대 bool_constant<true>는 true라는 부울 타입 값을 저장하는 integral_constant<bool, true>다. ratio 비교 연산 템플릿 결과는 bool_constant<true>나 bool_constant<false> 중 하나가 된다. bool_constant나 integral_constant에 대한 값은 value라는 데이터 멤버로 접근 할 수 있다.
다음 예는 ratio_less를 사용하는 방법을 보여준다. boolalpha로 부울 값인 true나 false를 출력하는 방법은 13장에서 소개한다.
using r1 = ratio<1, 60>; using r2 = ratio<1, 30>; using result = ratio_less<r2, r1>; cout << boolalpha << res::value << endl;
C++
(이하 예시 생략)

chrono 라이브러리

chrono 라이브러리는 시간을 다루는 클래스로서 다음과 같은 요소로 구성돼 있다.
duration
clock
time_point
std::chrono 네임스페이스에 정의된 것을 사용하려면 반드시 <chrono> 헤더 파일을 인클루드 해야 한다.

duration

duration은 두 시점 사이의 시간 간격(interval)을 표현하는 클래스 템플릿으로서 틱(tick)과 틱 주기(tick period)에 대한 값을 저장한다. 틱 주기란 두 틱 사이의 초 단위 간격으로서 컴파일 시간 상수인 ratio로 표현한다. 따라서 초를 분수로 표현한 것이라고 볼 수 있다.
(이하 설명 생략)

clock

clock은 time_point와 duration으로 구성된 클래스다.
C++ 표준은 clock을 세 가지 버전으로 정의한다. 첫 번째 버전은 system_clock으로서 시스템 관점의 실시간 클럭을 표현한다. 두 번째 버전은 steady_clock으로서 time_point가 절대로 감소하지 않도록 보장해준다. 참고로 system_clock은 언제든지 조정할 수 있기 때문에 time_point가 감소되지 않도록 보장해주지 않는다. 세 번째 버전은 high_resolution_clock으로서 최소 틱 주기를 가진다. 현재 사용하는 컴파일러의 종류에 따라 high_resolution_clock이 steady_clock이나 system_clock과 같을 수 있다.
각 버전의 clock마다 now()라는 static 메서드가 존재하는데, 이 메서드는 현재 시각을 time_point로 리턴한다.
(이하 설명 생략)
(전략) 이렇게 시간 간격이 짧을 때 정확도가 떨어지는 이유는 대다수의 OS가 밀리초 단위를 지원하기는 하지만 갱신 주기가 10ms나 15ms 정도로 다소 길기 때문이다. 그래서 타이머의 한 틱보다 짧은 단위로 발생한 이벤트가 0 단위 시간으로 보이고, 1에서 2틱 사이에 발생한 이벤트가 1 단위 시간으로 보이는 게이팅 에러(gating error)가 발생한다.
예컨대 1ms 주기로 타이머를 갱신하는 시스템에서 44ms가 걸리는 루프의 실행시간은 30ms로 나온다. 이런 타이머로 시간을 측정할 때는 반드시 대상 연산의 실행 시간을 타이머의 최소 틱 단위보다 크게 구성해야 오차를 최소화할 수 있다.

time_point

teim_pont는 특정한 시점을 표현하는 클래스로서 에포크(epoch, 기준 시간)를 기준으로 측정한 duration으로 저장한다. time_point는 항상 특정한 clock을 기준으로 표현하는데, 이 clock의 시작 시간이 에포크가 된다.
예컨대 유닉스/리눅스 시간에 대한 에포크는 1970년 1월 1일이고, duration은 초 단위로 측정한다. 윈도우 시스템의 에포크는 1601년 1월 1일이고, duration을 100나노초 단위로 측정한다. 에포크와 duration은 OS마다 다를 수 있다.
(이하 설명 생략)

무작위수 생성

무작위수를 소프트웨어로 정확히 생성하기란 상당히 어렵다. C++ 11 이전에는 C 스타일 함수인 srand()와 rand() 만으로 무작위수를 생성할 수 밖에 없었다. 애플리케이션에서 srand() 함수를 한 번 호출한 뒤 무작위수 생성기를 시드(seed)로 초기화해야 했다. (이를 시딩(seeding)이라 부른다) 시드값은 주로 현시 시스템 시각을 사용한다.
Caution) 소프트웨어로 무작위수를 생성하려면 시드값을 잘 정해야 한다. 무작위수 생성기를 초기화할 때마다 같은 시드를 사용하면 매번 동일한 무작위수가 생성된다. 그래서 현재 시스템 시각을 시드값으로 많이 사용하는 것이다.
(예시 생략)
Note) 소프트웨어 기반 무작위수 생성기는 진정한 의미의 무작위수를 생성할 수 없다. 그래서 의사(pseudo) 무작위수 생성기라고도 부른다. 무작위인 것처럼 보이게 만든느 수학 공식에 따라 생성하기 때문이다.
기존 srand()와 rand() 함수는 유연성이 좀 떨어진다. 예컨대 무작위수의 분포를 변경할 수 없다. C++ 11부터 다양한 알고리즘과 분포로 무작위수를 생성하는 강력한 라이브러리가 추가됐다. 이 라이브러리는 <random> 헤더 파일에 정의돼 있으며, 크게 세 가지 구성 요소(엔진, 엔진 어댑터, 분포)로 구성된다.
무작위수 엔진(engine)은 실제로 무작위수를 생성하고, 그 뒤에 생성될 무작위수의 상태를 저장한다. 무작위수 분포(distribution)는 생성된 무작위수의 범위와 그 범위 안에서 무작위수가 수학적으로 분포되는 방식을 결정한다. 무작위수 엔진 어댑터(engine adaptor)는 무작위수 엔진의 결과를 수정한다.
무작위수를 생성할 때 srand()와 rand()를 사용하지 말고 <random> 헤더 팡리에 정의된 클래스를 사용하기 바란다.

무작위수 엔진

<random>은 다음과 같은 무작위수 엔진을 제공한다.
random_device
linear_congruential_engine
mersenne_twister_engine
substract_with_carry_engine
random_device 엔진은 소프트웨어 기반이 아니다. 컴퓨터에 특수 하드웨어가 장착돼 있어야 쓸 수 있는 진정한 비결정적(non-deterministic, 결과를 예측할 수 없는) 무작위수 발생기다. 예컨대 일정한 시간 간격 동안 발생한 알파 입자의 수를 세는 방사성 동위 원소의 자연 붕괴 속도 측정 기법이있다. 컴퓨터에서 방사능이 누출될까봐 꺼림칙하다면 역바이어스 다이오드(reverse-biased diode)에서 발생하는 ‘노이즈’를 측정하는 방식처럼 다른 물리 법칙 기반의 무작위수 생성기를 사용해도 된다.
random_device의 규격을 보면 현재 사용하는 컴퓨터에 특수 하드웨어가 없을 때는 소프트웨어 알고리즘 중 아무거나 적용하도록 정의돼 있다. 어떤 알고리즘을 적용할지는 라이브러리 디자이너가 결정한다.
무작위수 발생기의 성능은 엔트로피(entropy)로 측정한다. random_device 클래스에서 제공하는 entropy() 메서드는 소프트웨어 기반 의사 무작위수 생성기를 사용할 때는 0.0을 리턴하고, 하드웨어 장치를 사용할 때는 0이 아닌 값을 리턴한다. 이때 리턴하는 0이 아닌 값은 장착된 디바이스의 엔트로피에 대한 측정치로 결정된다.
random_device 엔진의 사용법은 다소 간단하다.
random_device rnd; cout << "Entropy: " << rnd.entropy() << endl; cout << "Min value: " << rnd.min() << ", Max value: " << rnd.max() << endl; cout << "Random number: " << rnd() << endl;
C++
random_device는 대체로 의사 무작위수 생성 엔진보다 느리다. 그래서 생성해야 할 무작위수가 아주 많다면 의사 무작위수 생성 엔진을 사용하고 random_device는 이 엔진의 시드를 생성하는데만 사용하는 것이 좋다.
<random>은 random_device 외에 다음 세 가지 의사 무작위수 생성 엔진을 제공한다.
inear_congruential_engine(선형 합동 무작위수 엔진)
상태 저장을 위한 메모리 사용량이 가장 적다. 여기서는 상태를 최종 생성된 무작위수를 포함한 정수 또는 아직 무작위수를 생성한 적이 없다면 초기 시드값을 담은 정수 하나로 표현한다. 이 엔진의 주기는 알고리즘에 대한 매개변수에 따라 다르며 최대 2까지 지정할 수 있지만 대체로 그보다 적은 수로 설정한다. 이러한 이유로 선형 합동 무작위수 엔진은 무작위 품질이 아주 높아야 할 때는 사용하지 않는 것이 좋다.
64
mersenne_twister_engine(메르센 트위스터 무작위수 엔진)
소프트웨어 기반 무작위수 생성기 중에서 가장 품질이 좋다. 이 엔진의 주기는 알고리즘 매개변수에 따라 달라지지만 linear_congruential_engine보다 훨씬 길다. 상태 저장에 필요한 메모리 사용량도 이 매개변수에 따라 결정되지만 정수 하나로 표현하는 linear_congruential_engine보다 훨씬 크다. 예컨대 기본 정의된 mersenne_twister_engine mt19937의 주기는 2 – 1이고, 상태를 2.5kbyte 가량 차지하는 625개의 정수로 표현한다. 이 엔진도 속도가 빠르다고 손꼽힌다.
19937
subtract_with_carry_engine(감산 캐리/자리내림 무작위수 엔진)
상태를 100byte 가량의 메모리에 저장하지만 무작위수의 생성 속도와 품질은 mersenne_twister_engine 보다 떨어진다.
random_device 엔진을 사용하는 방법은 간단하며 매개변수를 지정할 필요도 없다. 하짐나 앞서 소개한 세 가지 의사 무작위수 생성기 중 하나에 대한 인스턴스를 생성하려면 수학 매개변수를 지정해야 하는데, 이 값을 정하는 방법은 쉽지 않다. 어떤 매개변수를 지정하는가에 따라 생성된 무작위수의 품질이 크게 달라진다. 예컨대 mersenne_twister_engine 클래스는 매개변수를 다음과 같이 정의하고 있다.
template<class UIntType, size_t w, size_t n, size_t m, size_t r, UIntType a, size_t u, UIntType d, size_t s, UIntType b, size_t t, UintType c, size_t l, UIntType f> class mersenne_twister_engine { ... }
C++
매개변수가 무려 14개나 된다. linear_congruential_engine과 subtract_with_carry_engine 클래스도 수학 매개변수를 많이 지정해야 한다. 그래서 표준에서는 몇 가지 엔진으 미리 정의해서 제공한다. 그중 하나가 mt19937이라는 mersenne_twister_engine이다. 이 엔진은 다음과 같이 정의돼 있다.
using mt19937 = mersenne_twister_engine<uint_fast32_t, 32, 624, 397, 31, 0x9908b0df, 11, 0xffffffff, 7, 0x9d2c5680, 15, 0xefc60000, 18, 1812433253>;
C++
매개변수의 의미를 제대로 이해하려면 메르센 트위스터 알고리즘을 깊이 이해해야 한다. 일반적으로 의사 무작위수 생성 기법을 전공한 수학자가 아니라면 이러한 매개변숫값을 변경할 일은 없다. 따라서 mt19937처럼 C++에서 제공하는 타입 앨리어스를 사용하기 바란다.

무작위수 엔진 어댑터

무작위수 엔진 어댑터는 무작위수 생성에 사용하는 엔진의 결과를 수정할 때 사용하며, 어댑터 패턴의 대표적인 예이기도 하다. C++ 라이브러리에 정의된 어댑터는 다음 세 가지다.
template<class Engine, size_t p, size_t r> class discard_block_engine { ... } template<class Engine, size_t w, class UIntType> class independent_bits_engine { ... } template<class Engine, size_t k> class suffle_order_engine { ... }
C++
discard_block_engine 어댑터는 베이스 엔진에서 생성된 값 중에서 일부를 제거하는 방식으로 무작위수를 생성한다. 이 어댑터는 세 가지 매개변수를 받는데, 첫 번째는 연결할 엔진에 대한 것이고, 두 번째는 블록 크기인 p이고, 세 번째는 사용된 블록 크기인 r이다. 베이스 엔진은 p개의 무작위수를 생성하는데 사용된다. 그러면 어댑터는 p-r개의 무작위수를 제거하고, 나머지 r개의 무작위수만 리턴한다.
independent_bits_engine 어댑터는 w로 지정된 비트 수로부터 베이스 엔진이 생성한 여러가지 무작위수를 조합하는 방식으로 무작위수를 생성한다.
shuffle_order_engine 어댑터는 베이스 엔진과 똑같은 무작위수를 생성하지만 리턴 순서는 다르다.
이러한 어댑터의 내부 작동 과정은 기반이 되는 수학 기법에 따라 다르다.

기본으로 제공하는 엔진과 엔진 어댑터

앞서 설명 했듯이 의사 무작위수 엔진과 엔진 어댑터의 매개변수는 건드리지 않는 것이 좋다. 그 대신 표준에서 정의해둔 엔진과 엔진 어댑터를 사용하는 것이 바람직하다. C++은 다음과 같은 엔진과 엔진 어댑터를 기본으로 제공한다. 모두 <random> 헤더 파일에 정의돼 있으며, 템플릿 인수가 복잡하게 지정돼 있다. 물론 이러한 인수의 의미를 몰라도 엔진과 엔진 어댑터를 사용하는데는 문제없다.
Search
이름
템플릿
minstd_rand
Open
linear_congruential_engine
mt19937
Open
mersenne_twister_engine
mt19937_64
Open
mersenne_twister_engine
ranlux24_base
Open
subtract_with_carry_engine
ranlux48_base
Open
subtract_with_carry_engine
ranlux24
Open
discard_block_engine
ranlux48
Open
discard_block_engine
knuth_b
Open
shuffle_order_engine
default_random_engine
Open
구현마다 다름

무작위수 생성하기

무작위수를 생성하기 전에 먼저 엔진 인스턴스부터 생성해야 한다. 소프트웨어 기반 엔진을 사용할 때는 분포도 지정해야 한다. 여기서 분포란 주어진 범위 안에서 숫자가 분포되는 방식을 표현하는 수학 공식이다. 추천하는 엔진 생성 방법은 앞서 소개한 기본 제공 엔진 중 하나를 그냥 사용하는 것이다.
다음 코드는 기본 제공 엔진이자 소프트웨어 기반 무작위수 생성기인 mt19937이란 메르센 트위스터 엔진을 사용하는 예를 보여준다. 기존 rand() 생성기를 사용할 떄와 마찬가지로 소프트웨어 기반 엔진도 시드로 초기화해야 한다. srand()에서는 주로 현재 시스템 시간을 시드로 사용했다. 최신 C++에서는 random_device로 시드를 생성하거나 random_device를 사용하기 힘든 상황이라면 차선책으로 시스템 시간에 기반한 시드를 사용하도록 권장한다.
random_device seeder; const auto seed = seeder.entropy() ? seeder() : time(nullptr); mt19937 eng(static_cast<mt19937::result_type>(seed));
C++
다음으로 분포를 지정한다. 이 예제에서는 1부터 99사이의 범위에 대해 균등 정수 분포(uniform integer distribution)로 지정한다.
uniform_int_distributeion<int> dist(1, 99);
C++
엔진과 분포를 정의했다면 분포에 대한 함수 호출 연산자에 엔진을 인수로 지정해서 호출한다. 그러면 무작위수가 생성된다. 예컨대 다음과 같다.
cout << dist(eng) << endl;
이 코드에서 볼 수 있듯이 소프트웨어 기반 엔진으로 무작위수를 생성하기 위해서는 항상 엔진과 분포를 지정해야 한다. 18장에서 소개한 <functional> 헤더에 정의된 std::bind() 유틸리티를 사용하면 엔진과 분포를 지정하지 않고도 무작위수를 생성할 수 있다. 다음 코드는 앞에서와 마찬가지로 mt19937 엔진과 균등 분포를 적용한 다음 std::bind()로 dist()의 첫 번째 매개변수를 eng로 바인딩해서 gen()을 정의한다. 이렇게 하면 무작위수를 생성할 때마다 인수를 지정하지 않고 gen()만 호출할 수 있다. 그런 다음 gen()을 generate() 알고리즘과 함께 조합해서 열 개의 무작위수로 구성된 vector를 만든다.
random_device seeder; const auto seed = seeder.entropy() ? seeder() : time(nullptr); mt19937 eng(static_cast<mt19937::result_type>(seed)); uniform_int_distribution<int> dist(1, 99); auto gen = std::bind(dist, eng); vector<int> vec(10); generate(begin(vec), end(vec), gen); for (auto i : vec) { cout << i << " "; }
C++
get()의 타입을 정확히 몰라도 gen()을 무작위수 생성기를 사용하려는 다른 함수에 인수로 전달할 수 있다. 이때 두 가지 옵션이 있다. 하나는 std::function<int()> 타입으로 매개변수를 지정하는 것이고, 다른 하나는 함수 템플릿으로 지정하는 것이다. 앞의 예제를 fillVector() 함수에서 무작위수를 생성하는데 활용해도 된다. 이 코드는 std::function으로 구현했다.
void fillVector(vector<int>& vec, const std::function<int()>& generator) { generate(begin(vec), end(vec), generator); }
C++
함수 템플릿 버전은 다음과 같다.
template<typename T> void fillVector(vector<int>& vec, const T& generator) { generate(begin(vec), end(vec), generator); }
C++
이렇게 작성한 함수는 다음과 같이 사용할 수 있다.
random_device seeder; const auto seed = seeder.entropy() ? seeder() : time(nullptr); mt19937 eng(static_cast<mt19937::result_type>(seed)); uniform_int_distribution<int> dist(1, 99); auto gen = std::bind(dist, eng); vector<int> vec(10); fillVector(vec, gen); for (auto i : vec) { cout << i << " "; }
C++

무작위수 분포

분포란 일정한 범위에서 숫자가 분포된 방식을 표현하는 수학 공식이다. 무작위수 발생기 라이브러리는 의사 무작위수 엔진에서 사용할 수 있도록 다음과 같이 다양하게 정의된 분포를 함께 제공한다.
(생략)
균등 메르센 트위스터 분포 예시
정규 분포 예시

optional

std::optional은 <optional>에 정의돼 있으며, 어떤 타입의 값이 있거나 없을 수 있는 것을 표현한다. optional은 함수의 매개변수를 옵션으로 지정할 때 사용한다. 또한 함수의 리턴 타입으로 지정해서 그 함수가 값을 리턴할 수도 있고, 리턴하지 않을 수도 있다는 것을 표현하기도 한다.
optional을 이용하면 리턴값이 없다는 것을 nullptr, end(), -1, EOF 등과 같은 특수한 값으로 표현하지 않아도 된다.
optional을 리턴하는 함수를 작성하는 예는 다음과 같다.
optional<int> getData(bool giveIt) { it (giveIt) { return 42; } return nullopt; // 또는 그냥 return {}; 이라고만 적는다. }
C++
이렇게 작성한 함수는 다음과 같이 호출한다.
auto data1 = getData(true); auto data2 = getData(false);
C++
optional에 값이 실제로 있는지 확인하려면 has_value() 메서드를 호출하거나 optional을 곧바로 if 문의 조건으로 사용하면 된다.
cout << "data1.has_value = " << data1.has_value() << endl; if (data2) { cout << "data2 has a value" << endl; }
C++
optional에 값이 있다면 value() 메서드나 역참조 연산자로 값을 가져올 수 있다.
cout << "data1.value = " << data1.value() << endl; cout << "data1.value = " << *data1 << endl;
C++
값이 없는 optional에 value() 메서드를 호출하면 bad_optional_access 익셉션이 발생한다. value_or()을 호출하면 optional에 값이 있으면 그 값을 리턴하고, 값이 없으면 다른 값을 리턴한다.
cout << "data2.value = " << data2.value_or(0) << endl;
C++
참고로 optional에 레퍼런스를 직접 저장할 수는 없다. 그래서 optional<T&>와 같이 적으면 작동하지 않는다. 레퍼런스를 담고 싶다면 optional<T*>, optional<reference_wrapper<T>> 또는 optional<reference_wrapper<const T>>로 표현한다. std::reference_wrapper<T>는 std::ref(), reference_wrapper<const T>는 cref()로 생성할 수 있다.

variant

std::variant는 주어진 타입 집합 중에서 어느 한 타입의 값을 가지며, <variant>에 정의돼 있다. variant를 정의하려면 여기에 담길 수 있는 타입들을 반드시 지정해야 한다. 예컨대 정수, 스트링, 부동소수점수 중 하나를 담을 수 있는 variant를 정의하려면 다음과 같이 정의한다.
variant<int, string, float> v;
C++
이렇게 별도로 초기화하지 않고 선언된 variant는 디폴트값을 첫 번째 타입인 int로 설정한다. 이렇게 variant를 디폴트로 생성하려면 반드시 여기에 지정한 첫 번째 타입이 디폴트 생성을 지원해야 한다. 예컨대 다음 코드에서 Foo는 디폴트 생성할 수 없기 때문에 컴파일 에러가 발생한다.
class Foo { public: Foo() = delete; Foo(int) {} }; class Bar { public: Bar() = delete; Bar(int) {} }; int main() { variant<Foo, Bar> v; }
C++
이 코드에서 Foo와 Bar 모두 디폴트 생성할 수 없다. 그래도 디폴트 생성하고 싶다면 variant의 첫 번째 타입을 std::monostate로 지정ㅎ나다.
variant<monostate, Foo, Bar> v;
C++
다음과 같이 대입 연산자를 이용하면 variant에 특정한 값을 저장할 수 있다.
variant<int, string, float> v; v = 12; v = 12.5f; v = "An std::string"s;
C++
variant는 언제나 값 하나만 가질 수 있다. 그래서 위 세 가지 대입문에서 첫 문장으로 인해 정수 12가 저장되지만 그 다음 문장에서 부동소수점수로 변경되고, 마지막에는 string으로 바뀐다.
variant에 현재 저장된 값의 타입에 대한 인덱스를 알고 싶다면 index()를 호출하면 된다. std::holds_alternative() 함수 템플릿을 이용하면 variant가 인수로 지정한 타입의 값을 담고 있는지 확인할 수 있다.
cout << "Type index: " v.index() << endl; cout << "Contains an int: " << holds_alternative<int>(v) << endl;
C++
std::get<index>()나 std::get<T>()를 이용하면 variant에 담긴 값을 가져올 수 있다. 이 함수를 호출할 때 가져오려는 값의 타입이나 인덱스를 잘못 지정하면 bad_variant_access 예외가 발생한다.
cout << std::get<string>(v) << endl; try { cout << std::get<0>(v) << endl; } catch (const bad_variant_access& ex) { cout << "Exception: " << ex.what() << endl; }
C++
이 익셉션이 발생하지 않게 하려면 std::get_if<index>()나 std::get_if<T>() 헬퍼 함수를 사용한다. 이 함수는 variant에 대한 포인터를 인수로 받아서 요청한 값에 대한 포인터를 리턴한다. 에러가 발생하면 nullptr를 리턴한다.
string* theString = std::get_if<string>(&v); int* theInt = std::get_if(int>(&v); cout << "retrieved string: " << (theString ? *theString : "null") << endl; cout << "retrieved int: " << (theInt ? *theInt : 0) << endl;
C++
std::visit() 헬퍼 함수도 있는데, variant에 대한 비지터(방문자) 패턴을 적용할 떄 사용한다. 예컨대 다음과 같이 클래스에 함수 호출 연산자가 다양한 버전으로 오버로딩 돼 있을 때 각 타입을 variant로 표현할 수 있다.
class MyVisitor { public: void operator()(int i) { cout << "int " << i << endl; } void operator()(const string& s) { cout << "string " << s << endl; } void operator()(float f) { cout << "float " << f << endl; } };
C++
이렇게 정의된 클래스를 다음과 같이 std::visit() 로 호출할 수 있다.
visit(MyVisitor(), v);
C++
이렇게 하면 오버로딩된 함수 호출 연산자 중에서 현재 variant에 저장된 값에 적합한 것이 호출된다.
optional과 마찬가지로 variant에도 레퍼런스를 직접 저장할 수 없다. 포인터를 저장하거나 reference_wrapper<T> 또는 reference_wrapper<const T>의 인스턴스로 저장해야 한다.

any

std::any는 모든 타입의 값을 저장하는 클래스다. any 인스턴스를 생성했다면 이 인스턴스에 대해 값이 있는지 있다면 타입은 뭔지 조회할 수 있다. any에 담긴 값을 구하려면 any_cast()를 사용해야 한다. 오류가 발생하면 bad_any_cast 익셉션이 발생한다.
(이하 설명 생략)

tuple

std::tuple은 std::pair를 일반화한 클래스이다. tuple은 여러 수를 하나로 묶어서 저장할 수 있고, 각각의 타입도 따로 지정할 수 있다. pair와 마찬가지로 tuple도 크기와 값의 타입이 고정돼 있으며 컴파일 시간에 결정된다.
tuple은 tuple 생성자로 만든다. 이때 템플릿 타입과 실젯값을 모두 지정한다.
using MyTuple = tuple<int, string, bool>; MyTuple t1(16, "Test", true);
C++
std::get<i>()는 tuple의 i번째 원소를 가져온다.
cout << "t1 = (" << get<0>(t1) << ", " << get<1>(t1) << ", " << get<2>(t1) << ")" << endl;
C++
get<i>()가 정확한 타입을 리턴했는지 확인하려면 <typeinfo>에 정의된 typeid()를 호출하면 된다.
cout << "Type of get<1>(t1) = " << typeid(get<1>(t1)).name() << endl;
C++
또한 튜플의 원소를 가져올 때 인덱스를 지정하지 않고 std::get<T>()에 조회할 원소의 타입(T)을 지정하는 방식으로 튜플의 원소를 가져올 수도 있다. 이때 요청한 타입으로 된 원소가 여러 개라면 컴파일 에러가 발생한다.
아쉽게도 tuple에 담긴 값에 대해 반복하기는 쉽지 않다. 루프문 안에서 get<i>(mytuple)만 호출하는 식으로 간단히 구현할 수 없다. i의 값이 컴팡리 시간에 확정돼야 하기 때문이다. 이럴 때는 템플릿 메타 프로그래밍으로 구현하면 된다. 이에 대해서는 22장에 소개한다.
tuple의 크기는 std::tuple_size 템플릿으로 알아낼 수 있다.
cout << "Tuple size = " << tuple_size<MyTuple>::value << endl;
C++
tuple의 타입을 정확히 모르면 decltype()로 알아낼 수 있다.
cout << "Tuple size = " << tuple_size<decltype(t1)>::value << endl;
C++
C++ 17부터 생성자에서 템플릿 인수를 추론하는 기능이 추가됐다. 그래서 tuple을 생성할 때 템플릿 타입 매개변수를 생략하면 생성자에 전달된 인수의 타입을 컴파일러가 알아낸다. 예컨대 다음과 같이 작성해도 t1이라는 tuple을 앞서와 마찬가지로 정수와 string과 부울 타입으로 구성하도록 정의할 수 있다.
std::tuple t1(16, "Test"s, true);
C++
타입 추론 기능이 적용되기 때문에 &로 레퍼런스를 지정할 수 없다. 레퍼런스나 const 레퍼런스를 담은 tuple을 템플릿 인수 추론 기능을 적용해서 생성하려면 다음 코드와 같이 ref()나 cref()를 사용해야 한다.
double d = 3.14; string str1 = "Test"; std::tuple t2(16, ref(d), cref(d), ref(str1));
C++
(이하 생략)
C++ 17에서 제공하는 템플릿 인수 추론 기능을 사용하지 않고도 std::make_tuple()이란 유틸리티 함수로 tuple을 생성할 수 있다. 이 헬퍼 함수 템플릿도 실젯값을 지정하는 방식으로 tuple을 생성할 수 있다. 타입은 컴파일 시간에 자동으로 결정된다.
auto t2 = std::make_tuple(16, ref(d), cref(d), ref(str1));
C++

tuple 분리하기

tuple을 개별 원소로 분리하는 방법은 두 가지다. 하나는 C++ 17부터 추가된 구조적 바인딩을 사용하는 것이고, 다른 하나는 std::tie()을 이용하는 것이다.

구조적 바인딩

구조적 바인딩을 이용하면 tuple을 개별 원소에 대한 변수로 쉽게 분리할 수 있다.
tuple t1(16, "Test"s, true); auto[i, str, b] = t1;
C++
구조적 바인딩으로 tuple을 분리할 때는 개별 원소를 생략할 수 없다. tuple에 담긴 원소가 세 개라면 구조적 바인딩에 지정하는 변수도 세 개여야 한다. 원소를 생략하고 싶다면 tie()를 사용한다.

tie

구조적 바인딩을 적용하지 않고 tuple을 분리하려면 std::tie()라는 유틸리티 함수를 활용하면 된다. 이 함수는 레퍼런스로 구성된 tuple을 생성한다.
tuple<int, string, bool> t1(16, "Test", true); int i = 0; string str; bool b = false; tie(i, str, b) = t1;
C++
tie()를 이용하면 분리하고 싶지 않은 원소를 생략할 수 있다. 함수를 호출할 때 분리하고 싶지 않은 원소의 자리에 std::ignore 값을 적으면 된다.
tuple<int, string, bool> t1(16, "Test", true); int i = 0; string str; bool b = false; tie(i, std::ignore, b) = t1;
C++

연결

std::tuple_cat()을 이용하면 두 tuple을 하나로 연결할 수 있다.
tuple<int, string, bool> t1(16, "Test", true); tuple<double, string> t2(3.14, "string 2"); auto t3 = tuple_cat(t1, t2);
C++

비교

튜플은 ==, !=, <, >, <=, >= 같은 비교 연산자도 제공한다. 이러한 비교 연산자로 tuple 끼리 비교하려면 그 tuple에 있는 원소의 타입도 이 연산을 지원해야 한다.
tuple<int, string> t1(123, "def"); tuple<int, string> t2(123, "abc"); if (t1 < t2) { cout << "t1 < t2" << endl; } else { cout << "t1 >= t2" << endl; } // 실행 결과 // t1 >= t2
C++

make_from_tuple

std::make_from_tuple<T>는 T 타입의 생성자에 tuple 원소를 인수로 전달해서 T 객체를 만든다. 다음 클래스를 보자.
class Foo { public: Foo(string str, int i) : mStr(str), mInt(i) { } private: string mStr; int mInt; };
C++
이때 make_from_tuple()을 다음과 같이 호출할 수 있다.
auto myTuple = make_tuple("Hello world", 42); auto foo = make_from_tuple<Foo>(myTuple);
C++
make_from_tuple()에 전달하는 인수가 반드시 tuple일 필요는 없지만, 최소한 std::get<>()과 std::tuple_size를 지원해야 한다. 이를 만족하는 예로 std::array와 std::pair가 있다.
이 함수를 실제로 사용할 일은 많지 않지만, 템플릿을 이용한 메타 프로그래밍을 하면서 코드를 범용적으로 구성할 때 굉장히 유용하다.

apply

std::apply()는 호출 가능 개체(예: 함수, 람다 표현식, 함수 객체 등)를 호출하는데, 이때 지정한 tuple의 원소를 인수로 전달한다.
int add(int a, int b) { return a + b; } ... cout << apply(add, std::make_tuple(39, 3)) << endl;
C++
make_from_tuple()과 마찬가지로 이 함수도 실전에서 자주 사용하지 않고 템플릿 메타 프로그래밍할 때 제네릭 코드를 구현하는데 유용하다.

파일시스템 지원 라이브러리

path

파일시스템 지원 라이브러리의 기본 구성요소는 경로를 표현하는 path다. path는 절대 경로와 상대 경로를 표현할 수 있으며, 파일 이름이 포함될 수도 있고 빠질 수도 있다. 예컨대 다음 코드는 다양한 path 생성 예를 보여준다.
path p1(LR"D:\Foo\Bar)"); path p2(L"D:/Foo/Bar"); path p3(L"D:/Foo/Bar/MyFile.txt"); path p4(LR"(..\SomeFolder)"); path p5(L"/usr/lib/X11");
C++
예컨대 path를 string으로 변환하거나(c_str() 메서드를 사용해서) 스트림에 추가하면 이 코드를 실행하는 시스템의 네이티브 포맷으로 변환된다.
(예시 생략)
append() 메서드나 operator/=를 이용하면 path에 다른 항목을 추가할 수 있다. 이때 경로 구분자가 자동으로 추가된다. 예컨대 다음과 같다.
path p(L"D:\\Foo"); p.append("Bar"); p /= "Bar"; cout << p << endl;
C++
concat()이나 operator+=를 사용하면 path에 다른 스트링을 연결할 수 있다. 그런데 이번에는 append()와 달리 경로 구분자가 추가되지 않는다.
path p(L"D:\\Foo"); p.concat("Bar"); p += "Bar"; cout << p << endl;
C++
path는 경로의 구성 요소에 대해 반복문을 수행할 수 있도록 반복자도 제공한다. 예컨대 다음과 같다.
path p(LR"(C:\Foo\Bar)"); for (const auto& component : p) { cout << component << endl; }
C++
path 인터페이스는 remove_filename(), replace_filename(), replace_extension(), root_name(), parent_path(), extension(), has_extension(), is_absolute(), is_relative() 등과 같은 연산도 제공한다.

directory_entry

path는 파일시스템에 존재하는 디렉터라니 파일을 표현하기만 한다. 그래서 path가 표현하는 디렉터리나 파일이 실제로는 시스템에 없을 수 있다. 파일시스템에 디렉터리나 파일이 실제로 존재하는지 확인하려면 path로부터 directory_entry를 생성해야 한다. 인수로 지정한 디렉터리나 파일이 시스템에 존재하지 않으면 directory_entry가 생성되지 않는다.
directory_entry 인터페이스는 is_directory(), is_regular_file(), is_socket(), is_symlink(), file_size, last_write_time()을 비롯한 다양한 연산을 제공한다.
다음 코드는 파일 크기를 조회하기 위해 path에서 directory_entry를 생성하는 예를 보여준다.
path myPath(L"c:/windows/win/ini"); directory_entry dirEntry(myPath); if (dirEntry.exists() && dirEntry.is_regular_file()) { cout << "File size: " << dirEntry.file_size() << endl; }
C++

헬퍼 함수

헬퍼 함수는 다양하게 제공된다. 예컨대 파일이나 디렉터리를 복제하는 copy(), 파일시스템에 디렉터리를 새로 만드는 create_directory(), 주어진 디렉터리나 파일이 실제로 존재하는지 조회하는 exists(), 파일의 크기를 알아내는 file_size(), 파일의 최종 수정 시각을 알아내는 last_write_time(), 파일을 삭제하는 remove(), 임시 파일을 저장하기 위한 디렉터리를 구하는 temp_directory_path(), 파일시스템의 여유 공간을 조회하는 space() 등이있다.
(이하 생략)

directory_iterator와 recursive_directory_iterator

주어진 디렉터리에 속한 파일이나 하위 디렉터리(서브디렉터리)에 대해 재귀적으로 반복하는 코드를 작성하려면 다음과 같이 recursive_directory_iterator를 사용하면 된다.
void processPath(const path& p) { if (!exists(p)) { return; } auto begin = recursive_directory_iterator(p); auto end = recursive_directory_iterator(); for (auto iter = begin; iter != end; ++iter) { const string spacer(iter.depth() * 2, ' '); auto& entry = *iter; if (is_regular_file(entry)) { cout << spacer << "File: " << entry; cout << " (" << file_size(entry) << " bytes)" << endl; } else if (is_directory(entry)) { std::cout << spacer << "Dir: " << entry << endl; } } }
C++
directory_iterator로도 디렉터리의 내용을 기준으로 반복문을 작성할 수 있다. 이때 재귀 호출 부분은 직접 구현한다. 예컨대 앞서 나온 코드와 똑같이 작동하는 코드를 recursive_directory_iterator 대신 directory_iterator를 사용해서 구현하면 다음과 같다.
void processPath(const path& p, size_t level = 0) { if (!exists(p)) { return; } const string spacer(level * 2, ' '); if (is_regular_file(p)) { cout << spacer << "File: " << p; cout << " (" << file_size(p) << " bytes)" << endl; } else if (is_directory(p)) { std::cout << spacer << "Dir: " << p << endl; for (auto& entry : directory_iterator(p)) { processPath(entry, level + 1); } } }
C++