Search
Duplicate

전문가를 위한 C++/ 고급 템플릿

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

템플릿 매개변수에 대한 심화 학습

(생략)

클래스 템플릿 부분 특수화

(생략)

오버로딩으로 함수 템플릿 부분 특수화 흉내내기

(생략)

템플릿 재귀

C++의 템플릿은 단순히 클래스나 함수를 정의하는 것보다 많은 일을 할 수 있는데, 그중 하나가 템플릿 재귀(template recursion)이다. 구체적인 구현 방법을 살펴보기 전에 먼저 템플릿 재귀가 필요한 이유를 알아보자.

N차원 Grid: 첫 번째 시도

지금까지 본 Grid 템플릿은 2차원까지만 지원해서 활용 범위가 제한됐다. 예컨대 3D 틱택토나 4차원 행렬을 계산하는 수학 프로그램을 구현할 수 없다. 물론 원하는 차원마다 템플릿이나 클래스를 새로 만들어도 되긴 하지만 이러면 코드가 중복된다.
또 다른 방법은 일차원 Grid만 만들어두고, 이 Grid를 원소의 타입으로 갖는 Grid를 인스턴수화 하는 방식으로 원하는 차원에 대한 Grid를 만들 수 있다. 이떄 상위 Grid의 원소로 사용하는 일차원 Grid는 실제 원소의 타입으로 인스턴스화 한다. 다음 코드는 OneDGrid 클래스 템플릿의 구현코드를 보여준다.
template<typename T> class OneDGrid { public: explicit OneDGrid(size_t size = kDefaultSize); virtual ~OneDGrid() = default; T& operator[](size_t x); const T& operator[](size_t x) const; void resize(size_t newSize); size_t getSize() const { return mElements.size(); } static const size_t kDefaultSize = 10; private: std::vector<T> mElements; }; template<typename T> OneDGrid<T>::OneDGrid(size_t size) { resize(size); } template<typename T> void OneDGrid<T>::resize(size_t newSize) { mElements.resize(newSize); } template<typename T> T& OneDGrid<T>::operator[](size_t x) { return mElements[x]; } template<typename T> const T& OneDGrid<T>::operator[](size_t x) const { return mElements[x]; }
C++
이렇게 구현한 OneDGrid를 이용해서 다음과 같이 다차원 그리드를 만들 수 있다.
OneDGrid<int> singleDGrid; OneDGrid<OneDGrid<int>> twoDGrid; OneDGrid<OneDGrid<OneDGrid<int>>> threeDGrid; singleDGrid[3] = 5; twoDGrid[3][3] = 5; threeDGrid[3][3][3] = 5;
C++
이렇게 해도 사용하는데 문제는 없지만 선언하는 부분이 좀 지저분하다. 다음 절에서 좀 더 개선해 보자.

진정한 N차원 Grid

템플릿 재귀를 활용하면 진정한 N차원 그리드를 구현할 수 있다. 다음 선언문에서 보듯이 그리드의 치원은 본질적으로 재귀적인 속성이 있기 때문이다.
OneDGrid<OneDGrid<OneDGrid<int>>> threeDGrid;
C++
여기서 각각의 OneDGrid를 재귀의 한 단계로 볼 수 있다. int에 대한 OneDGrid는 재귀의 베이스 케이스 역할을 한다. 다시 말해 3차원 그리드는 int에 대한 일차원 그리드에 대한 일차원 그리드에 대한 일차원 그리드다. 이때 재귀 문장을 길게 나열할 필요 없이 다음과 같이 작성하면 알아서 N차원 그리드로 풀어서 써 준다.
NDGrid<int, 1> singleDGrid; NDGrid<int, 2> twoDGrid; NDGrid<int, 3> threeDGrid;
C++
여기 나온 NDGrid 클래스 템플릿은 원소의 타입과 차원을 지정하는 정수를 인수로 받는다. 여기서 핵심은 NDGrid의 원소 타입이 템플릿 매개변수 리스트에 지정된 원소 타입이 아니라 현재 Grid보다 한 차원 낮은 NDGrid라는데 있다. 다시 말해 3차원 그리드는 2차원 그리드의 벡터고, 2차원 그리드는 1차원 그리드의 벡터다.
이렇게 재귀적으로 구성하려면 베이스 케이스를 지정해야 한다. 1차원으로 NDGrid를 부분 특수화하고, 원소를 NDGrid가 아닌 템플릿 매개변수로 지정한 타입으로 지정해야 한다.
이렇게 일반화한 NDGrid 템플릿의 정의 코드는 다음과 같다.
template<typename T, size_t N> class NDGrid { public: explicit NDGrid(size_t size = kDefaultSize); virtual ~NDGrid() = default; NDGrid<T, N=1>& operator[](size_t x); const NDGrid<T, N-1>& operator[](size_t x) const; void resize(size_t newSize); size_t getSize() const { return mElements.size(); } static const size_t kDefaultSize = 10; private: std::vector<NDGrid<T, N-1>> mElements; };
C++
여기서 mElements는 NDGrid<T, N-1>의 vector로서 재귀 단계에 해당한다. 또한 operator[]는 원소 타입에 대한 레퍼런스를 리턴한다. 이것 역시 T가 아닌 NDGgrid<T, N-1>이다.
베이스 케이스에 대한 템플릿 저으이 코드는 다음과 같이 차원이 1인 부분 특수화로 작성한다.
template<typename T> class NDGrid<T, 1> { public: explicit NDGrid(size_t size = kDefaultSize); virtual ~NDGrid() = default; T& operator[](size_t x); const T& operator[](size_t x) const; void resize(size_t newSize); size_t getSize() const { return mElements.size(); } static const size_t kDefaultSize = 10; private: std::vector<T> mElements; };
C++
여기서 재귀 단계가 끝난다. 원소 타입은 다른 템플릿의 인스턴스가 아닌 T다.
이렇게 구현할 때 템플릿 재귀 정의를 제외한 가장 까다로운 부분은 그리드의 각 차원의 크기를 적절히 정하는 것이다. 여기서는 각 차원의 크기가 같은 N 차원 Grid를 만들었다. 차원마다 크기를 다르게 구현하는 방법은 이보다 훨씬 복잡하다. 하지만 이렇게 단순한 경우에도 여전히 문제는 남아 있다.
예컨대 사용자가 지정한 크기(예: 20이나 50)로 배열을 생성해야 한다. 그러기 위해서는 생성자에 정수 크기를 받는 매개변수가 있어야 한다. 그런데 하위 그리드의 vector 크기를 동적으로 변경할 때 이 크기값을 하위 그리드 원소로 전달할 수 없다. vector는 디폴트 생성자로 객체를 만들기 때문이다. 따라서 vector에 있는 각 그리드 원소마다 resize()를 일일이 호출해야 한다. 이렇게 구현한 코드는 다음과 같다. 여기서 베이스 케이스에 대해서는 크기를 조절할 필요가 없다. 원소 타입이 그리드가 아닌 T이기 때문이다.
제네릭 NDGrid 템플릿의 구현 코드는 다음과 같다.
template<typename T, size_t N> NDGrid<T, N>::NDGrid(size_t size) { resize(size); } template<typename T, size_t N> void NDGrid<T>::resize(size_t newSize) { mElements.resize(newSize); // vector에 대해 resize()를 호출하면 NDGrid<T, N-1> 원소에 대한 영인수 생성자를 호출해서 디폴트 크기로 원소가 생성된다. // 따라서 각 원소마다 명싲거으로 resize()를 재귀호출하는 방식으로 중첩된 Grid 원소의 크기를 조정한다. for(auto& element : mElements) { element.resize(newSize); } } template<typename T, size_t N> NDGrid<T, N-1>& NDGrid<T, N>::operator[](size_t x) { return mElements[x]; } template<typename T, size_t N> const NDGrid<T, N-1>& NDGrid<T, N>::operator[](size_t x) const { return mElements[x]; }
C++
이제 베이스 케이스에 대한 부분 특수화를 구현해보자. 특수화를 구현하는 코드를 하나도 상속하지 않기 때문에 다시 작성해야 할 부분이 많다.
template<typename T> NDGrid<T, 1>::NDGrid(size_t size) { resize(size); } template<typename T> void NDGrid<T, 1>::resize(size_t newSize) { mElements.resize(newSize); } template<typename T> T& NDGrid<T, 1>::operator[](size_t x) { return mElements[x]; } template<typename T> const T& NDGrid<T, 1>::operator[](size_t x) const { return mElements[x]; }
C++
이렇게 작성한 코드의 사용법은 다음과 같다.
NDGrid<int, 3> my3DGrid; my3DGrid[2][1][2] = 5; my3DGrid[1][1][1] = 5; cout << my3DGrid[2][1][2] << endl;
C++

가변 인수 템플릿

일반적으로 템플릿의 매개변수는 개수가 고정돼 있다. 하지만 가변 인수 템플릿은 템플릿 매개변수의 개수가 고정돼 있지 않다. 예컨대 다음과 같이 템플릿 매개변수의 개수를 지정하지 않게 정의할 수 있다. 이때 Types라는 매개변수 팩(parameter pack)을 사용한다.
template<typename... Types> class MyVariadicTemplate {};
C++
Note) typename 뒤에 붙은 … 은 가변 인수 템플릿에 대한 매개변수 팩을 정의하는 구문이다. 매개변수 팩은 다양한 수의 인수를 받을 수 있다. 점 세 개의 앞이나 뒤에 공백을 넣어도 된다.
예컨대 임의 개수의 타입에 대해 MyVariadicTemplate을 인스턴스화하면 다음과 같다.
MyVariadicTemplate<int> instance1; MyVariadicTemplate<string, double, list<int>> instance2;
C++
심지어 인수 없이 템플릿을 인스턴스화 할 수도 있다.
MyVariadicTemplate<> instance3;
C++
가변 인수 템플릿을 인스턴스화 할 때 반드시 템플릿 인수를 지정하게 하려면 다음과 같이 정의한다.
template<typename T1, typename... Types> class MyVariadicTemplate { };
C++
이렇게 정의한 상태에서 MyVariadicTemplate을 인수 없이 인스턴스화하면 컴파일 에러가 발생한다.

타입에 안전한 가변 길이 인수 리스트

가변 인수 템플릿을 사용하면 타입에 안전한 가변 길이 인수 리스트를 만들 수 있다. 다음 예제는 processValues()라는 가변 인수 템플릿을 정의한 것이다. 이 템플릿은 인수의 타입과 개수가 일정하지 않더라도 타입에 안전하게 처리한다.
processValues() 함수는 가변 길이 인수 리스트로 주어진 각각의 인수마다 handleValue()를 호출한다. 그러므로 처리하려는 타입마다 handleValue() 함수를 구현해야 한다. 이 예제에서는 int, double, string 타입을 사용한다.
void handleValue(int value) { cout << "Integer: " << value << endl; } void handleValue(double value) { cout << "Double: " << value << endl; } void handleValue(string_view value) { cout << "String: " << value << endl; } void processValues() { /* 베이스 케이스에 대해서는 특별히 할 일이 없다. */ } template<typename T1, typename... Tn> void processValues(T1 arg1, Tn... args) { handleValue(arg1); processValues(args...); }
C++
위 예제에서 점 새 개 연산자가 세 번 나오는데, 두 가지 용도로 사용했다. 첫 번째 용도는 템플릿 매개변수 리스트의 typename 뒤와 함수 매개변수 리스트의 Tn 타입 뒤에 적은 것처럼 매개변수 팩을 표현하는 것이다. 매개변수 팩은 가변 인수를 받는다.
두 번쨰 용도 …를 함수 바디 매개변수 이름인 args 뒤에 붙여서 매개변수 팩 풀기 연산을 하는 것이다. 다시 말해 매개변수 팩으 풀어서(unpack/expand) 개별 인수로 분리한다. 기본적으로 이 연산자는 좌변을 인수로 받고, 팩에 있는 모든 템플릿 매개변수에 대해 반복하면서 각 인수를 콤마로 구분해서 하나씩 대입한다. 다음 코드를 보자.
processValues(args...);
C++
이렇게 하면 args 매개변수 팩을 개별 인수로 풀고, 각각을 콤마로 구분한다. 그러고 나서 펼쳐진 인수 리스트로 processValues() 함수를 호출한다. 이 템플릿은 최소한 T1이라는 템플릿 매개변수를 받는다. processValues()를 args…에 대해 재귀적으로 호출하면 매 단계마다 매개변수를 하나씩 줄이면서 재귀적으로 호출한다.
processValues() 함수를 재귀적으로 구현했기 때문에 재귀 호출이 종료하는 조건도 반드시 지정해야 한다. 여기서는 인수를 받지 않는 processValues() 함수를 구현하는 방식으로 지정했다.
이렇게 작성한 processValues() 가변 인수 템플릿을 다음과 같이 테스트할 수 있다.
processValues(1, 2, 3.56, "test", 1.1f);
C++
이때 재귀 호출되는 과정은 다음과 같다.
processValues(1, 2, 3.56, "test", 1.1f); handleValue(1); processValues(2, 3.56, "test", 1.1f); handleValue(2); processValues(3.56, "test", 1.1f); handleValue(3.56); processValues("test", 1.1f); handleValue("test"); processValues(1.1f); handleValue(1.1f); processValues();
C++
여기서 명심할 부분은 이 메서드의 가변 길이 인수 리스트는 타입에 매우 안전하다는 점이다. processValues() 함수는 실제 타입에 맞게 오버로딩된 handleValue()를 알아서 호출한다. C++의 다른 코드처럼 자동 캐스팅할 수 있다. 예컨대 앞에 나온 예제에서 1.1f를 자동으로 float으로 캐스팅한다. 하지만 processValues()를 호출할 때 handleValue()를 지원하지 않는 타입으로 인수를 지정하면 컴파일 에러가 발생한다.
앞서 구현한 코드에 한 가지 문제가 있는데, 재귀적으로 호출했기 때문에 매번 processValues()를 호출할 때마다 매개변수가 복제된다. 그러면 인수의 타입에 따라 오버헤드가 커질 수 있다. processValues()에 값이 아닌 레퍼런스로 전달하면 복제를 줄일 수 있을거라 생각하기 쉽지만 아쉽게도 그렇게 하면 리터럴에 대해 processValues()를 호출할 수 없게 된다.
non-const 레퍼런스를 사용하면서 리터럴값을 사용하게 하려면 포워드 레퍼런스(forward reference)를 사용하면 된다. 다음 코드는 포워드 레퍼런스인 T&&를 사용했고, 모든 매개변수에 대해 퍼펙트 포워딩(perfect forwarding)을 적용하도록 std::forward()를 사용했다.
여기서 퍼펙트 포워딩이란 processValues()에 우측값(rvalue)이 전달되면 우측값 레퍼런스로 포워드(forward)되고, 좌측값(lvalue)이나 좌측값 레퍼런스가 전달되면 좌측값 레퍼런스로 포워드 된다는 뜻이다.
void processValues() { /* 베이스 케이스에 대해서는 특별히 할 일이 없다. */ } template<typename T1, typename... Tn> void processValues(T1&& arg1, Tn&&... args) { handleValue(std::forward<T1>(arg1)); processValues(std::forward<Tn>(args)...); }
C++
다음 문장에 대해서는 보충 설명이 필요하다.
processValues(std::forward<Tn>(args)...);
C++
… 연산자는 매개변수 팩을 풀 때(unpack) 사용한다. 이 연산자는 매개변수 팩에 있는 각 인수를 std::forward()로 호출하고, 그들을 콤마로 구분해서 분리한다. 예컨대 args란 매개변수 팩이 A1, A2, A3 타입으로 된 a1, a2, a3라는 인수로 구성됐다고 하자. 이 팩을 풀려면 다음과 같이 호출한다.
processValues(std::forward<A1>(a1), std::foward<A2>(a2), std::forward<A3>(a3));
C++
매개변수 팩을 사용하는 함수의 바디 안에서 이 팩에 담긴 인수의 개수를 알아내는 방법은 다음과 같다.
int numOfArgs = sizeof...(args);
C++
가변 인수 템플릿을 실제로 활용하는 예로 보안과 타입에 안전한 printf() 류의 함수를 구현하는 경우를 들 수 있다.

가변 개수의 믹스인 클래스

매개변수 팩은 거의 모든 곳에서 사용할 수 있다. 예컨대 다음 코드는 매개변수 팩을 이용하여 MyClass에 대한 가변 개수의 믹스인 클래스를 정의한다.
class Mixin1 { public: Mixin1(int i) : mValue(i) {} virtual void Mixin1Func() { cout << "Maxin1: " << mValue << endl; } private: int mValue; }; class Mixin2 { public: Mixin2(int i) : mValue(i) {} virtual void Mixin2Func() { cout << "Maxin2: " << mValue << endl; } private: int mValue; }; template<typename... Mixins> class MyClass : public Mixins... { public: MyClass(const Mixins&... mixins) : Mixins(mixins)... {} virtual ~MyClass() = default; };
C++
이 코드는 먼저 믹스인 클래스 두 개를 정의하고 각 클래스마다 정수를 인수로 받아서 저장하는 생성자와 각 인스턴스의 정보를 화면에 출력하는 함수를 하나씩 정의했다. 가변 인수 템플릿인 MyClass는 매개변수 팩인 typename… Mixins를 사용하여 다양한 수의 믹스인 클래스를 받는다.
이 클래스는 이렇게 전달된 모든 믹스인 클래스를 상속하고, 생성자에서도 같은 수의 인수를 받아서 각자 상속한 믹스인 클래스를 초기화한다. 여기서 … 연산자는 기본적으로 좌변의 내용을 인수로 받아서 팩에 있는 템플릿 매개변수에 대해 루프를 돌면서 콤마로 구분하면서 푼다. 이렇게 정의한 클래스는 다음과 같이 사용한다.
MyClass<Minxin1, Minxin2> a(Mixin1(11), Mixin2(22)); a.Mixin1Func(); a.Mixin2Func(); MyClass<Mixin1> b(Mixin1(33)); b.Mixin1Func(); //b.Mixin2Func(); // 컴파일 에러 MyClass<> c; //c.Mixin1Func(); // 컴파일 에러 //c.Mixin2Func(); // 컴파일 에러
C++
b에 대해 Mixin2Func()를 호출하면 컴파일 에러가 발생하는데, b는 Mixin2 클래스를 상속하지 않았기 때문이다.

폴딩 표현식

C++ 17부터 추가된 폴딩 표현식(folding expression)이란 기능을 활용하면 가변 인수 템플릿에서 매개변수 팩을 보다 쉽게 다룰 수 있다. 다음 표는 C++에서 지원하는 네 가지 종류의 폴딩 표현식을 보여주고 있다. 여기서 Θ 자리에 나올 수 있는 연산자는 + – * / % ^ & | << >> += -= *= /= %= ^= &= |= <<= >>= = == != < > <= >= && || , .* ->* 등이 있다.
Search
이름
표현식
펼친 형태
단항 좌측 폴드
Open
(… Θ pack)
((pack0 Θ pack1) Θ … ) Θ packn
이항 우측 폴드
Open
(pack Θ … Θ Init)
pack0 Θ (… Θ (packn-1 Θ (packn Θ Init)))
이항 좌측 폴드
Open
(Init Θ … Θ pack)
(((Init Θ pack0) Θ pack1) Θ …) Θ packn
앞서 본 processValue() 함수 템플릿은 다음과 같이 재귀적으로 정의 했다.
void processValues() { /* 베이스 케이스에 대해서는 특별히 할 일이 없다. */ } template<typename T1, typename... Tn> void processValues(T1 arg1, Tn... args) { handleValue(arg1); processValues(args...); }
C++
재귀적으로 저으이했기 때문에 재귀를 멈출 베이스 케이스를 지정해야 한다. 폴딩 표현식을 사용하면 단항 우측 폴드를 이용한 함수 템플릿 하나로 구현할 수 있다. 그러므로 베이스 케이스를 따로 지정하지 않아도 된다.
template<typename... Tn> void processValues(Tn... args) { (handleValue(args), ...); }
C++
기본적으로 함수 본문에 있는 점 세 개 (…) 연산자로 폴딩한다. 이 문장이 펼쳐지면서 매개변수 팩에 있는 인수마다 handleValue()를 호출하며 결과가 콤바로 구분돼 담긴다. 예컨대 args가 a1, a2, a3라는 세 인수로 구성된 매개변수 팩에 대해 단항 우측 폴드가 다음과 같이 펼쳐진다.
(handleValue(a1), (handleValue(a2), handleValue(a3)));
C++
또 다른 예를 보자. printValues() 함수 템플릿은 주어진 인수를 각각 한 줄씩 구분해서 콘솔에 출력한다.
template<typename... Values> void printValues(const Values&... values) { ((cout << values << endl), ...); }
C++
여기서 values가 v1, v2, v3라는 세 인수로 구성된 매개변수 팩이라면 단항 우측 폴드로 인해 다음과 같이 펼쳐진다.
((cout << v1 << endl), ((cout << v2 << endl), (cout << v3 << endl)));
C++
printvalues()에 원하는 수만큼 인수를 얼마든지 지정해서 호출할 수 있다.
printValues(1, "test", 2.34);
C++
이 예제에서는 폴딩에 콤마 연산자를 사용했지만 거의 모든 연산자와 함께 사용할 수도 있다. 예컨대 다음 코드는 주어진 값을 모두 더한 결과를 구하는 가변 인수 함수 템플릿을 이항 좌측 폴드로 정의하고 있다. 이항 좌측 폴드에는 반드시 Init 값을 지정해야 한다. 따라서 sumValues()는 두 개의 템플릿 타입 매개변수(Init의 타입을 지정하는 일반 매개변수와 0개 이상의 인수를 받을 수 있는 매개변수 팩)로 구성된다.
template<typename T, typename... Values> double sumValues(const T& init, const Values&... values) { return (init + ... + values); }
C++
만약 values가 v1, v2, v3라는 세 인수로 구성된 매개변수 팩이라면 이항 좌측 폴드를 펼친 결과는 다음과 같다.
return (((init + v1) + v2 + v3);
C++
이렇게 만든 sumValues() 함수 템플릿을 사용하는 방법은 다음과 같다.
cout << sumvalues(1, 2, 3.3) << endl; cout << sumValues(1) << endl;
C++
이렇게 템플릿을 정의하면 인수를 최소 한 개 이상 지정해야 한다. 따라서 다음과 같이 작성하면 컴파일 에러가 발생한다.
cout << sumValues() << endl;
C++

메타 프로그래밍

메타 프로그래밍은 책 한 권을 쓸 정도로 방대한 주제이므로 간단히 소개만 하겠다.
템플릿 메타 프로그래밍은 실행 시간이 아닌 컴팡리 시간에 연산을 수행할 목적으로 사용한다. 기본적으로 C++ 위에 정의된 프로그래밍 언어다.

컴파일 시간에 팩토리얼 계산하기

템플릿 메타 프로그래밍을 이용하면 실행 시간이 아닌 컴파일 시간에 계산을 수행할 수 있다. 다음 코드는 어떤 수의 팩토리얼을 컴파일 시간에 계산하는 예를 보여준다. 여기서는 앞서 사용한 템플릿 재귀 코드를 구현했다. 그래서 재귀 템플릿과 재귀를 멈추기 위한 베이스 템플릿을 작성해야 한다. 팩토리얼의 수학 정의에 따르면 0 팩토리얼은 1이기 때문에 이 값을 베이스 케이스로 지정한다.
template<unsigned char f> class Factorial { public: static const unsigned long long val = (f * Factorial<f-1>::val); }; template<> class Factorial<0> { public: static const unsigned long long val = 1; }; int main() { cout << Factorial<6>::val << endl; return 0; }
C++
Note) 여기 나온 팩토리얼 계산이 수행되는 시점은 컴파일 시간이라는 점을 명심한다. 이렇게 컴파일 시간에 계산된 결과는 실행 시간에서 볼 때 정적 상숫값이므로 ::val로 접근한다.
(이하 예시 생략)

루프 언롤링

템플릿 메타 프로그래밍의 두 번째 예로 반복문을 실행 시간에 처리하지 않고, 컴파일 시간에 일렬로 펼쳐놓는 방식으로 처리하는 루프 언롤링(loop unrolling)이란 기법이 있다. 참고로 루프 언롤링은 꼭 필요할 때만 사용하는 것이 좋다. 굳이 언롤링하도록 작성하지 않아도 컴파일러의 판단에 따라 자동으로 언롤링하기 때문이다.
이번 예제도 템플릿 재귀로 작성한다. 컴팡리 시간에 루프 안에서 작업을 처리해야 하기 때문에 각 재귀 단계마다 Loop 템플릿이 i – 1에 대해 인스턴스화된다. 0에 도달하면 재귀가 종료 된다.
template<int i> class Loop { public: template<typename FuncType> static inline void Do(FuncType func) { Loop<i-1>::Do(func); func(i); } }; template<> class Loop<0> { public: template <typename FuncType> static inline void Do(FuncType /* func */) {} };
C++
이렇게 작성한 Loop를 사용하는 방버은 다음과 같다.
void DoWork(int i) { cout << "DoWork(" << i << ")" << endl; } int main() { Loop<3>::Do(DoWork); }
C++
이렇게 작성하면 컴파일러는 DoWork() 함수를 세 번 호출하는 문장으로 루프를 펼친다. 이 코드를 실행한 결과는 다음과 같다.
DoWork(1) DoWork(2) DoWork(3)
C++
람다 표현식을 이용해서 매개변수를 한 개 이상 받게 만들 수도 있다.
void DoWork2(string str, int i) { cout << "DoWork2(" << str << ", " << i << ")" << endl; } int main() { Loop<2>::Do([](int i) { DoWork2("TestStr", i); }); }
C++
이 코드는 먼저 string과 int를 인수로 받는 함수를 구현한다. main() 함수는 람다 표현식을 이용하여 반복문을 한 번씩 돌 때마다 첫 번째 매개변수로 주어진 스트링 (“TestStr”)에 대해 DoWork2()를 호출한다. 이 코드를 컴파일해서 실행하면 다음과 같은 결과가 나온다.
DoWork2(TestStr, 1) DoWork2(TestStr, 2)
C++

tuple 출력하기

이번에는 std::tuple에 있는 각 원소를 화면에 출력하는 기능을 템플릿 메타 프로그래밍으로 구현해보자. tuple은 타입이 별도로 지정된 값을 원하는 만큼 담을 수 있다. tuple의 크기와 값의 타입은 컴파일 시간에 결정된다. 하지만 튜플은 원소에 대해 반복하는 메커니즘을 기본적으로 제공하지 않는다. 다음 코드는 템플릿 메타프로그래밍으로 tuple의 원소에 대해 컴파일 시간에 루프를 돌 수 있도록 구현하는 예이다.
다른 템플릿 메타프로그래밍 예제와 마찬가지로 이번에도 템플릿 재귀를 사용한다. tuple_print 클래스 템플릿은 템플릿 매개변수를 두 개 받는다. 하나는 tuple 타입이고, 다른 하나는 초기화할 때 설정할 튜플의 크기를 나타내는 정수다. 생성자에서 이 템플릿을 재귀적으로 인스턴스화하고 매번 호출될 때마다 정숫값을 하나씩 감소시킨다. 이 정숫값이 0에 도달하면 재귀를 멈추도록 tuple_print 부분을 특수화한다. main() 함수는 이렇게 정의한 tuple_print 클래스 템플릿을 사용하는 방법을 보여주고 있다.
template<typename TupleType, int n> class tuple_print { public: tuple_print(const TupleType& t) { tuple_print<TupleType, n-1> tp(t); cout << get<n-1>(t) << endl; } }; template<typename TupleType> class tuple_print<TupleType, 0> { public: tuple_print(const TupleType&) {} }; int main() { using MyTuple = tuple<int, string, bool>; MyTuple t1(16, "Test", true); tuple_print<MyTuple, tuple_size<MyTuple>::value> tp(t1); }
C++
main() 함수의 코드를 보면 tuple_print 템플릿을 사용하는 문장이 다소 복잡하게 표현돼 있다. tuple의 정확한 타입과 tuple의 크기를 템플릿 매개변수로 지정하기 때문이다. 템플릿 매개변수를 자동으로 추론하는 헬퍼 함수 템플릿을 사용하면 이 부분을 좀 더 간결하게 표현할 수 있다. 예컨대 다음과 같다.
template<typename TupleType, int n> class tuple_print_helper { public: tuple_print_helper(const TupleType& t) { tuple_print_helper<TupleType, n-1> tp(t); cout << get<n-1>(t) << endl; } }; template<typename TupleType> class tuple_print_helper<TupleType, 0> { public: tuple_print_helper(const TupleType&) {} }; template<typename T> void tuple_print(const T& t) { tuple_print_helper<T, tuple_size<T>::value> tph(t); } int main() { auto t1 = make_tuple(167, "Testing", false, 2.3); tuple_size(t1); }
C++
가장 먼저 tuple_print 클래스 템플릿을 적은 부분을 tuple_print_helper로 바꿨다. 그런 다음 tuple_print()라는 간단한 함수 템플릿을 구현했다. 이 템플릿은 tuple의 타입을 템플릿 타입 매개변수로 받으며, tuple 자체에 대한 레퍼런스 함수 매개변수로 받는다. 이 함수 템플릿의 본문에서는 tuple_print_helper 클래스 템플릿을 인스턴스화 한다.
main() 함수는 이렇게 간략하게 수정한 버전을 사용하는 방법을 보여준다. 이렇게 하면 tuple의 구체적인 타입은 몰라도 되기 때문에 make_tuple()에 auto를 적용할 수 있다. tuple_print() 함수 템플릿을 호출하는 코드가 다음과 같이 굉장히 간단해졌다.
tuple_print(t1);
C++
함수 템플릿 매개변수는 직접 지정할 필요 없다. 인수를 보고 컴파일러가 추론하기 때문이다.

constexpr if

C++ 17부터 constexpr if가 추가됐다. constexpr if는 실행 시간이 아닌 컴팡리 시간에 수행된다. constexpr if의 조건을 만족하지 않으면 컴파일되지 않는다. 이 구문을 사용하면 템플릿 메타 프로그래밍 코드를 훨씬 간결하게 작성할 수 있다. 또한 SFINAE에서도 굉장히 유용하게 쓰인다.
예컨대 앞에서 tuple의 원소를 화면에 출력하는 코드에 constexpr if를 적용해서 다음과 같이 간결하게 표현할 수 있다. 이렇게 하면 템플릿 재귀의 베이스 케이스를 더는 지정하지 않아도 된다. constexpr if 문에서 재귀가 멈추기 때문이다.
template<typename TupleType, int n> class tuple_print { public: tuple_print(const TupleType& t) { if constexpr(n > 1) { tuple_print_helper<TupleType, n-1> tp(t); } cout << get<n-1>(t) << endl; } }; template<typename TupleType> class tuple_print<const T& t> { tuple_print_helper(T, tuple_size<T>::value> tph(t); };
C++
이렇게 하면 클래스 템플릿 자체를 제거하고, 그 자리에 다음과 같이 간단히 구현한 tuple_print_helper라는 함수 템플릿을 넣어도 된다.
template<typename TupleType, int n> class tuple_print_helper<const TupleType& t> { if constexpr(n > 1) { tuple_print_helper<TupleType, n-1>(t); } cout << get<n-1>(t) << endl; }; template<typename T> void tuple_print(const T& t) { tuple_print_helper<T, tuple_size<T>::value>(t); }
C++
이 코드에 나온 두 메서드를 하나로 합쳐서 다음과 같이 좀 더 간결하게 표현할 수 있다.
template<typename TupleType, int n = tuple_size<TupleType>::value> void tuple_print(const TupleType& t) { if constexpr(n > 1) { tuple_print<TupleType, n-1>(t); } cout << get<n-1>(t) << endl; }
C++
이렇게 바꿔도 이전과 같은 방식으로 호출 할 수 있다.
auto t1 = make_tuple(167, "Testing", false, 2.3); tuple_size(t1);
C++

폴딩으로 컴파일 시간 정수 시퀀스 사용하기

C++은 <utility> 헤더 파일에 정의된 std::integer_sequence를 이용한 컴파일 시간 정수 시퀀스를 제공한다. 이 기능은 템플릿 메타 프로그래밍에서 인덱스의 시퀀스, 즉 size_t 타입에 대한 정수 시퀀스를 컴파일 시간에 생성하는데 주로 사용된다. 이를 위해 std::index_sequence도 제공한다. 주어진 매개변수 팩과 같은 길이의 인덱스 시퀀스를 생성할 때는 std::index_sequence_for를 사용하면 된다.
튜플을 출력하는 코드를 다음과 같이 가변 인수 템플릿, 컴파일 시간 인덱스 시퀀스 그리고 C++ 17부터 제공하는 폴딩 표현식으로 구현할 수 있다.
template<typename Tuple, size_t... Indices> void tuple_print_helper(const Tuple& t, index_sequence<Indices...>) { ((cout << get<Indices>(t) << endl), ...); } template<typename... Args> void tuple_print(const tuple<Args...>& t) { tuple_print_helper(t, index_sequence_for<Args...>()); }
C++
이렇게 작성해도 이전과 똑같은 방식으로 호출할 수 있다.
auto t1 = make_tuple(167, "Testing", false, 2.3); tuple_size(t1);
C++
이렇게 호출하면 tuple_print_helper() 함수 템플릿에 있는 단항 우측 폴드 표현식이 다음과 같이 펼쳐진다.
(((cout << get<0>(t) << endl), ((cout << get<1>(t) << endl), ((cout << get<2>(t) << endl), (cout << get<3>(t) << endl)))));
C++

타입 트레이트

타입 트레이트를 이용하면 타입에 따라 분기하는 동작을 컴팡리 시간에 처리할 수 있다. 예컨대 특정한 타입을 상속하는 타입, 특정한 타입으로 변환할 수 있는 타입, 정수 계열의 타입을 요구하는 템플릿 등을 작성할 수 있다. C++ 표준에서는 이를 위해 몇 가지 헬퍼 클래스를 제공한다. 타입 트레이드에 관련된 기능은 모두 <type_traits> 헤더 파일에 정의돼 있다.
타입 트레이트는 몇 가지 범주로 나눌 수 있다.
(상세 분류 생략)
타입 트레이트는 C++에서도 상당히 고급 기능에 속한다. 여기서는 몇 가지 활용 사례만 소개한다.

타입 범주에 속한 타입 트레이트

타입 트레이트를 사용하는 템플릿의 예를 보기 전에 먼저 is_integral과 같은 클래스의 작동 방식을 좀 더 살펴볼 필요가 있다. C++ 표준에서는 다음과 같이 integral_constant 클래스를 정의하고 있다.
template<class T, T v> struct integral_constant { static constexpr T value = v; using value_type = T; using type = integral_constant<T, v>; constexpr operator value_type() const noexcept { return value; } constexpr value_type operator()() const noexcept { return value; } };
C++
또한 bool_constant, true_type, false_type과 같은 타입 앨리어스도 정의하고 있다.
template<bool B> using bool_constant = integral_constant<bool, B>; using true_type = bool_constant<true>; using false_type = bool_constant<false>;
C++
이 코드는 true_type과 false_type이라는 두 가지 타입을 정의한다. true_type::value로 접근하면 true란 값을 구하고, false_type::value로 접근하면 false란 값을 구할 수 있다. 또한 true_type::type으로 접근하면 true_type이란 타입을 구할 수 있다. false_type도 마찬가지로 적용할 수 있다. is_integral이나 is_class 같은 클래스는 true_type이나 false_type을 상속한다. 예컨대 is_integral을 다음과 같이 bool 타입에 대해 특수화할 수 있다.
template<> struct is_integral<bool> : public true_type { };
C++
이렇게 하면 is_integral<bool>::value란 문장으로 true란 값을 구할 수 있다. 특수화하는 코드는 직접 작성하지 않아도 된다. 표준 라이브러리에서 기본으로 제공하기 때문이다.
타입 범주를 사용하는 가장 간단한 예는 다음과 같다.
if (is_integral<int>>::value) { cout << "int is integral" << endl; } else { cout << "int is not integral" << endl; } if (is_class<string>::value) { cout << "string is a class" << endl; } else { cout << "string is not a class" << endl; }
C++
여기서는 is_integral로 int가 정수 타입인지 검사하고, is_class로 string이 클래스인지 확인한다.
C++ 17부터 value 멤버가 있는 트레이트마다 트레이트 이름 뒤에 _v가 붙은 가변 템플릿이 추가됐다. 그래서 some_trait<T>::value라고 적지 않고, some_trait_v<T>와 같은 형태로 표현할 수 있다. 앞에 나온 코드를 이러한 헬퍼를 사용하도록 수정하면 다음과 같다.
if (is_integral_v<int>>) { cout << "int is integral" << endl; } else { cout << "int is not integral" << endl; } if (is_class_v<string>) { cout << "string is a class" << endl; } else { cout << "string is not a class" << endl; }
C++
물론 타입 트레이트를 이렇게 활용할 일은 거의 없다. 그보다는 타입의 특정한 속성을 기준으로 코드를 생성하기 위해 템플릿과 함께 사용할 때 유용하다. 다음에 나온 템플릿 함수가 바로 이러한 예를 보여준다.
이 코드는 타입을 템플릿 매개변수로 받는 process_helper() 함수 템플릿을 두 가지 방식으로 오버로딩하도록 정의한다. 첫 번째 매개변수는 값이고, 두 번째 매개변수는 true_type이나 false_type 중 한 인스턴스다. process() 함수 템플릿은 매개변수 하나만 받아서 process_helper()를 호출한다.
template<typename T> void process_helper(const T& t, true_type) { cout << t << " is an integral type" << endl; } template<typename T> void process_helper(const T& t, false_type) { cout << t << " is an non-integral type" << endl; } template<typename T> void process(const T& t) { process_helper(t, typename is_integral<T>::type()); }
C++
process_helper()를 호출할 때 두 번째 인수를 다음과 같이 지정했다.
typename is_integral<T>::type()
C++
이 인수는 is_integral을 이용하여 T가 정수 계열 타입인지 검사한다. 그 결과로 나오는 integral_constant 타입을 ::type으로 접근하면 true_type이나 false_type 중 하나가 나온다.
process_helper() 함수는 true_type이나 false_type 중 한 인스턴스를 두 번째 매개변수로 받는다. 그래서 ::type 뒤에 빈 소괄호를 붙였다. 여기서 process_helper()에 대한 두 가지 오버로딩 버전은 true_type과 false_type이란 타입에 대해 이름 없는 매개변수를 받는다. 이렇게 이름이 없는 이유는 함수 본문에서 이 매개변수를 사용하지 않기 때문이다. 이 매개변수는 여러 가지 오버로딩 버전 중 하나를 결정하는데만 사용된다.
이렇게 작성한 코드를 사용하는 방법은 다음과 같다.
process(123); process(2.2); process("Test"s);
C++
앞의 예제를 다음과 같이 함수 템플릿 하나만으로 표현할 수 있다. 하지만 이렇게 작성하면 타입 트레이트로 주어진 타입에 맞는 오버로딩 버전을 결정하는 예를 볼 수 없다.
template<typename T> void process(const T& t) { if constexpr (is_integral_v<T>) { cout << t << " is an integral type" << endl; } else { cout << t << " is an non-integral type" << endl; } }
C++

타입 관계 활용 방법

타입 관계에 대한 예로 is_same, is_base_of, is_convertible 등이 있다. 여기서는 is_same을 사용하는 방법에 대한 예제만 소개한다. 나머지 타입 관계도 사용법은 비슷한다.
다음 코드에 나온 same() 함수 템플릿은 is_same 타입 트레이트를 이용하여 주어진 두 인수의 타입이 서로 같은지 검사한 뒤 결과에 따라 메시지를 출력한다.
template<typename T1, typename T2> void same(const T1& t1, const T2& t2) { bool areTypesTheSame = is_same_v<T1, T2>; cout << "'" << t1 << "' and '" << t2 << "' are "; cout << (areTypesTheSame ? "the same types" : "different types") << endl; } int main() { same(1, 32); same(1, 3.01); same(3.01, "Test"s); }
C++

enable_if 사용법

enable_if는 C++의 난해한 특성 중 하나인 SFINAE(substitution failure is not an error, 치환 실패는 에러가 아니다)에 기반을 두고 있다. 이 절에서는 SFINAE의 기본 개념만 소개한다.
오버로딩된 함수가 여러 개 있을 때 enable_if를 이용하여 특정한 타입 트레이트에 따라 오버로딩된 함수 중 일부를 끌 수 있다. enable_if 트레이트는 오버로딩 함수들에 대한 리턴 타입을 기준으로 분기할 때 주로 사용한다. enable_if는 템플릿 타입 매개변수를 두 개 받는다. 하나는 부울값이고, 다른 하나는 타입인데 디폴트값은 void다.
부울값을 true로 지정하면 enable_if는 중첩된 타입을 가지며, ::type으로 접근할 수 있다. 이렇게 중첩된 타입의 타입은 두 번째 템플릿 타입 매개변수로 지정한 타입이다. 부울값을 false로 지정하면 중첩된 타입이 생기지 않는다.
C++ 표준은 enable_if 처럼 type 멤버를 가진 트레이트에 대한 앨리어스 템플릿을 몇 가지 정의하고 있다. 각각의 이름은 트레이트 이름 뒤에 _t가 붙어 있다. 예컨대 다음 문장을
typename enable_if<..., bool>::type
C++
다음과 같이 간략하게 표현할 수 있다.
enable_if t<..., bool>
C++
앞 절에서 본 same() 함수 템플릿을 다음과 같이 enable_if를 사용하여 오버로딩 버전인 check_type() 함수 템플릿으로 표현할 수 있다. 이때 check_type() 함수는 주어진 두 값의 타입이 같은지 여부에 따라 true나 false 중에서 하나를 리턴한다. check_type()에서 아무 것도 리턴하고 싶지 않다면 return 문을 삭제하고, enable_if 문의 두 번째 템플릿 타입 매개변수도 삭제하거나 void로 지정한다.
template<typename T1, typename T2> enable_if_t<is_same_v<T1, T2>, bool> check_type(const T1& t1, const T2& t2) { cout << "'" << t1 << "' and '" << t2 << "' "; cout << "are the same types" << endl; return true; } template<typename T1, typename T2> enable_if_t<!is_same_v<T1, T2>, bool> check_type(const T1& t1, const T2& t2) { cout << "'" << t1 << "' and '" << t2 << "' "; cout << "are different types" << endl; return false; } int main() { check_type(1, 32); check_type(1, 3.01); check_type(3.01, "Test"s); }
C++
이 코드는 check_type()을 두 가지 버전으로 정의한다. 두 버전의 리턴 타입은 모두 enable_if에 대한 중첩 타입인 bool이다. 먼저 is_same_v로 두 타입이 같은지 검사하고, 그 결과를 enable_if_t로 전달한다. enable_if_t의 첫 번째 인수가 true면 enable_if_t는 bool 타입을 갖고 그렇지 않으면 타입이 없다. 바로 여기서 SFINAE가 적용된다.
main()의 첫 문장을 컴파일 할 때 정숫값 두 개를 받는 check_type()이 있는지 찾는다. 먼저 소스 코드에 있는 첫 번째 check_type() 함수 템플릿을 보고 T1과 T2를 모두 정수로 만들어서 이 함수 템플릿의 인스턴스를 사용할 수 있다고 추론한다. 그런 다음 리턴 타입을 알아낸다. 두 인수 모두 정수라서 is_same_v<T1, T2>의 결과는 true가 된다. 그래서 enable_if_t<true, bool>의 타입은 bool이 된다. 이렇게 인스턴스화하는 과정에서 아무런 문제가 발생하지 않으면 컴파일러는 이 버전의 check_type()을 적용한다.
그런데 main()의 두 번째 문장을 컴파일할 때도 적절한 check_type() 함수를 찾는 작업을 또 수행한다. 먼저 check_type()을 찾아서 T1을 int로, T2를 double로 설정해서 오버로딩하도록 처리한다. 그런 다음 리턴 타입을 결정하는데, 이번에는 T1과 T2가 서로 타입이 다르기 때문에 is_same_t<T1, T2>의 결과는 false가 된다. 그래서 enable_if_t<false, bool>은 타입을 표현하지 않고, check_type() 함수의 리턴 타입도 지정하지 않는다. 컴파일러는 이 에러를 발견해도 SFINAE를 적용하기 때문에 실제로 컴파일 에러가 발생하지 않는다. 그 대신 지금까지 하던 작업을 조용히 역추적해서 다른 check_type() 함수를 찾는다. 이제 두 번째 check_type()이 !is_same_v<T1, T2>에 대해 true가 된다는 것을 발견하고 enable_if_t<true, bool>의 타입이 bool이 돼 정상적으로 인스턴스화된다.
enable_if를 여러 버전의 생성자에 적용할 때 리턴 타입에는 적용할 수 없다. 생성자는 원래 리턴 타입이 없기 때문이다. 이럴 때는 생성자에 디폴트값을 가진 매개변수를 하나 더 추가해서 enable_if를 적용하면 된다.
enable_if를 사용할 때는 조심해야 한다. 특수화나 부분 특수화와 같은 다른 기법으로는 도저히 적합한 오버로딩 버전을 찾기 힘들 때만 사용하는 것이 좋다. 예컨대 잘못된 타입으로 템플릿을 사용할 때 그냥 컴파일 에러만 발생시키고 싶다면 SFINAE를 적용하지 말고 27장에서 설명하는 static_assert()를 사용한다.
물론 enable_if를 사용하는 것이 좋을 때도 있다. 예컨대 vector와 비슷한 기능을 정의하는 커스텀 클래스에서 복제 함수를 제공할 때 enable_if와 is_trivially_copyable 타입 트레이트를 활용하면 단순히 복제할 수 있는 타입을 비트 단위 복제로 처리하도록 특수화할 수 있다. 예컨대 memcpy()를 사용하도록 복제 함수를 특수화할 수 있다.
Caution) SFINAE를 적용하는 방법은 상당히 까다롭고 복잡하다. SFINAE와 enable_if를 적용할 떄 여러 가지 오버로딩 버전 중 엉뚱한 버전을 비활성화하게 되면 알 수 없는 컴파일 에러가 발생하는데 에러 메시지만 보고 문제의 원인을 찾기 굉장히 힘들다.

constexpr if로 enable_if 간결하게 표현하기

앞의 예제에서 볼 수 있듯이 enable_if를 사용하면 코드가 굉장히 복잡해질 수 있다. C++ 17부터 추가된 constexpr if 기능을 활용하면 enable_if를 활용하는 코드를 훨씬 간결하게 표현할 수 있다. 예컨대 다음과 같이 정의된 두 클래스를 살펴보자.
class IsDoable { public: void doit() const { cout << "IsDoable::doit()" << endl; } }; class Derived : public IsDoable { };
C++
doit() 메서드가 제공된다면 이를 호출하고 그렇지 않으면 콘솔에 에러 메시지를 출력하는 call_doit() 이란 함수 템플릿을 만들어보자. 이때 enable_if를 이용해서 주어진 타입이 IsDoable을 상속했는지 확인할 수 있다.
template<typename T> enable_if_t<is_base_of_v<IsDoable, T>, void> call_doit(const T& t) { t.doit(); } template<typename T> enable_if_t<!is_base_of_v<IsDoable, T>, void> call_doit(const T&) { cout << "Cannot call doit()!" << endl; }
C++
이 템플릿을 다음과 같이 사용할 수 있다.
Derived d; call_doit(d); call_doit(123); // 실행 결과 // IsDoable::doit() // Cannot call doit()!
C++
C++ 17부터 추가된 constexpr if를 활용하면 이 코드를 다음과 같이 좀 더 간결하게 표현할 수 있다.
template<typename T> void call_doit(const T& [[maybe_unused]] t) { if constexpr (is_base_of_v<IsDoable, T>) { t.doit(); } else { cout << "Cannot call doit()!" << endl; } }
C++
기존 if 문으로는 절대로 이렇게 할 수 없다. 일반 if 문은 모든 분기문이 반드시 컴파일 돼야 하기 때문에 IsDoable을 상속하지 않은 타입을 T에 지정하면 에러가 발생한다. 이 예제는 t.doit()이 나오는 문장에서 컴파일 에러가 발생한다. 하지만 constexpr if 문을 사용하여 IsDoable을 상속하지 않은 타입을 지정하면 t.doit()이란 문장 자체를 아예 컴파일하지 않게 된다.
여기서 C++ 17부터 추가된 [[maybe_unused]] 어트리뷰트를 사용한 점도 주목한다. IsDoable을 상속하지 않은 타입으로 T를 지정하면 t.doit()이 컴파일 되지 않기 떄문에 call_doit()을 인스턴스화한 코드에서 매개변수 t가 사용되지 않는다. 대다수의 컴파일러는 이렇게 사용하지 않는 매개변수가 있을 때 경고나 에러 메시지를 출력한다. 이때 [[maybe_unsed]] 속성을 지정하면 이러한 매개변수 t에 대해 경고나 에러가 발생하지 않는다.
is_base_of 타입 트레이트 대신 C++ 17부터 추가된 is_invocable 트레이트를 사용해도 된다. 이 트레이트는 주어진 함수가 주어진 인수 집합에 대해 호출되는지 검사한다. is_invocable 트레이트로 call_doit() 을 구현하면 다음과 같다.
template<typename T> void call_doit(const T& [[maybe_unused]] t) { if constexpr (is_invocable_v<decltype(&IsDoable::doit), T>) { t.doit(); } else { cout << "Cannot call doit()!" << endl; } }
C++

논리 연산자 트레이트

논리 연산자에 대해서도 세 가지 트레이트(conjunction, disjunction, negation)을 제공한다. _v로 끝나는 가변 템플릿도 제공된다. 이러한 트레이트는 다양ㅎ나 개수의 템플릿 타입 매개변수를 받으며, 타입 트레이트에 대해 논리 연산을 수행하는데 활용할 수 있다. 예컨대 다음과 같다.
cout << conjunction_v<is_integral<int>, is_integral<short>> << " "; cout << conjunction_v<is_integral<int>, is_integral<double>> << " "; cout << disjunction_v<is_integral<int>, is_integral<double>, is_integral<short>> << " "; cout << negation_v<is_integral<int>> << " "; // 실행 결과 // 1 0 1 0
C++

템플릿 메타 프로그래밍 맺음말

지금까지 살펴본 것처럼 템플릿 메타 프로그래밍은 굉장히 강력한 도구지만 코드가 상당히 난해해질 위험도 있다. 또한 모든 작업을 컴파일 시간에 처리하기 때문에 문제가 발생해도 찾을 수 없다는 문제도 있다.
템플릿 메타 프로그래밍으로 코드를 작성할 때는 반드시 주석에 메타 프로그래밍을 사용하는 목적과 진행 과정을 명확히 밝히는 것이 좋다. 그렇지 않으면 다른 사람이 코드를 이해하기 굉장히 어려워진다. 심지어 본인도 나중에 다시 보면 무슨 뜻인지 알 수 없을 수도 있다.