그러나 템플릿으로 그 보다 더 많은 일을 할 수 있다. 컴파일러의 구현에 제한만 없다면 템플릿으로 무슨 계산이든 컴파일 시간에 프로그램할 수 있다. 현대의 어떤 컴퓨터 언어도 제공하지 못하는 이 놀라운 특징은 컴파일 시간에 세 가지 일을 할 수 있다는 사실에 뿌리가 있다.
물론 컴파일러에게 소수를 계산하라고 요구하는 것이 그 하나이다. 그러나 최고의 속도를 얻기 위해 그렇게 하는 것과는 완전히 다른 일이다. 컴파일러가 복잡한 계산을 대신 수행해 주더라도 속도가 크게 빨라질 것이라고는 기대하지 마라. 그것은 요점을 멀리 벗어난 것이다. 어쨌든 컴파일러에게 C++의 템플릿 언어를 사용하여 사실상 무엇이든 계산하라고 요구할 수 있다. 물론 소수 계산을 포함해서 말이다....
이 장은 템플릿의 이 놀라운 특징들을 다룬다. 템플릿에 관련된 미묘한 점들을 짧게 살펴 본 후에 템플릿 메타 프로그래밍의 핵심 특징을 소개한다.
템플릿 유형 매개변수와 템플릿 비-유형 매개변수 외에도 제 삼의 템플릿 매개변수가 있다. 템플릿 템플릿 매개변수가 그것이다. 이 종류의 템플릿 매개변수를 다음에 소개한다. 이를 토대로 유형속성(trait) 클래스와 정책(policy) 클래스를 연구한다.
이 장은 템플릿의 여러 흥미로운 적용 방식을 연구하며 끝낸다. 컴파일러 에러 메시지 적용하기와 클래스 유형으로 변환하기 그리고 컴파일 시간에 리스트를 처리하는 방법을 보여주는 정교한 예제를 다루고 끝내겠다.
이 장은 두 권의 권장 도서로부터 영감을 받았다.
typename
키워드의 특별한 적용 방법을 연구했다. 거기에서 typename
키워드는 (복합) 유형에 대하여 이름을 정의할 뿐만 아니라 클래스 템플릿으로 정의된 유형과 클래스 템플릿으로 정의된 멤버를 구분한다는 것도 배웠다. 이 절은 typename
키워드의 적용 방법을 두 가지 더 소개한다.
typename
키워드를 적용한다.
typename
키워드의 특별한 적용 방법 말고도 23.1.3항에서 typename
키워드의 확장 사용법과 관련하여 몇 가지 새로운 구문을 소개한다. ::template
과 .template
그리고 ->template
은 템플릿 안에 사용된 이름이 클래스 템플릿이라는 사실을 컴파일러에게 알려준다.
nested
멤버는 이 내포 클래스의 객체를 돌려준다. 인-클래스로 멤버를 구현했지만 바람직하지 않다. 그 이유는 잠시 후에 밝혀진다.
template <typename T> class Outer { public: class Nested {}; Nested nested() const { return Nested(); } };위의 예제는 흠없이 컴파일된다.
Outer
클래스 안에 nested
클래스의 반환 유형에 관련하여 전혀 모호함이 없기 때문이다.
그렇지만 다음의 좋은 관례를 따라 인라인 함수와 템플릿 멤버는 자신의 클래스 멤버 바로 아래에 구현되어야 한다 (7.8.1항). 그래서 구현을 인터페이스 밖으로 빼냈다.
template <typename T> class Outer { public: class Nested {}; Nested nested() const; }; template <typename T> Outer<T>::Nested Outer<T>::nested() const { return Nested(); }갑자기 컴파일러는
nested
멤버를 컴파일하기를 거부하고 다음과 같은 에러 메시지를 뱉어 낸다.
error: expected constructor, destructor, or type conversion before 'Outer'.이제 구현이 인터페이스 밖으로 이동했으므로 반환 유형은 (즉,
에러: 'Outer' 앞에 생성자나 소멸자 또는 유형 변환이 예상됨 .
Outer<T>::Nested
는) Outer<T>
에 정의된 유형을 참조한다. Outer<T>
의 멤버를 더 이상 참조하지 않는다.
여기에서 typename
키워드를 다시 한 번 사용해야 한다. 유형 자체가 템플릿 유형 매개변수에 따라 달라지면 그 부유형(subtype)을 참조할 때마다 typename
키워드를 붙여야 한다. 인라인 구현을 사용하면 그런 의존성은 없다. 함수의 반환 유형이 그냥 Nested
이기 때문이다. (`좋은 관례'이므로) 함수를 클래스 인터페이스 밖에 구현하면 Nested
를 정의한 클래스를 그 함수의 반환 유형에 대하여 제공해야 한다. 그래서 Outer<T>::Nested
가 되는데 이 유형은 확실히 템플릿 유형 매개변수에 따라 달라진다.
앞과 마찬가지로 typename
을 Outer<T>::Nested
앞에 쓰면 컴파일 에러가 사라진다. 그래서 올바르게 nested
함수를 구현하면 다음과 같다.
template <typename T> typename Outer<T>::Nested Outer<T>::nested() const { return Nested(); }
Base
와 Derived
두 개의 클래스 템플릿이 있다. Base
는 Derived
의 바탕 클래스이다.
#include <iostream> template <typename T> class Base { public: void member(); }; template <typename T> void Base<T>::member() { std::cout << "This is Base<T>::member()\n"; } template <typename T> class Derived: public Base<T> { public: Derived(); }; template <typename T> Derived<T>::Derived() { member(); }
이 예제는 컴파일되지 않는다. 컴파일러는 다음과 같이 경고한다.
error: there are no arguments to 'member' that depend on a template parameter, so a declaration of 'member' must be available 에러: 'member'에 인자가 없습니다. 'member'는 템플릿 매개변수에 의존합니다. 먼저 'member'를 선언해야 합니다.이 에러는 혼란을 야기한다. 보통의 (비-템플릿) 바탕 클래스는 자신의 공개 멤버와 보호 멤버를 파생 클래스에게 쉽게 노출시켜 버리기 때문이다. 이것은 클래스 템플릿에 대해서도 마찬가지이지만 컴파일러가 프로그래머의 의도를 짐작할 수 있을 경우에만 그렇다. 컴파일러는 위의 예제를 컴파일하지 못한다.
T
가 무슨 유형인지 모르기 때문이다. member
멤버 함수는 Derived<T>::Derived
로 호출될 때 반드시 초기화해야 한다.
다음과 같이 특정화를 정의해 왜 그래야 하는지 감상해 보자:
template <> Base<int>::member() { std::cout << "This is the int-specialization\n"; }컴파일러는
Derived<SomeType>::Derived
가 호출될 때 member
의 특정화가 실재하는지 모르기 때문에 (Derived<T>::Derived
를 컴파일할 때) 어떤 유형으로 member
를 구체화할지 결정하지 못한다. Derived::Derived
의 member
를 호출하면 템플릿 유형 매개변수를 요구하지 않기 때문이다.
어느 유형을 사용해야 할지 결정하기 위하여 템플릿 유형 매개변수를 사용할 수 없으면 컴파일러에게 사용할 템플릿 유형 매개변수에 관하여 (그러므로 호출할 특별한 함수(member
)에 대하여) 구체화 시간이 될 때까지 결정을 미루라고 알려주어야 한다.
이것은 두 가지 방식으로 구현이 가능하다. 파생 클래스의 템플릿 유형에 대하여 구체화한 this
를 사용하거나 아니면 바탕 클래스를 명시하면 된다. this
를 사용하면 컴파일러에게 유형 T
를 참조하고 있고 그 유형에 대하여 템플릿이 구체화되어 있다고 알린다. (파생 클래스 멤버인가 아니면 바탕 클래스 멤버인가) 어느 멤버 함수를 사용해야 할지 혼란스러우면 파생 클래스 멤버를 선택하면 해결된다. 대안으로, (Base<T>
또는 Derived<T>
를 사용하여) 바탕 클래스 또는 파생 클래스를 명시적으로 언급할 수 있다. 이는 다음 예제에 보여준다. int
템플릿 유형이라면 int
특정화가 사용되는 것을 눈여겨보라.
#include <iostream> template <typename T> class Base { public: virtual void member(); }; template <typename T> void Base<T>::member() { std::cout << "This is Base<T>::member()\n"; } template <> void Base<int>::member() { std::cout << "This is the int-specialization\n"; } template <typename T> class Derived: public Base<T> { public: Derived(); virtual void member(); }; template <typename T> void Derived<T>::member() { std::cout << "This is Derived<T>::member()\n"; } template <typename T> Derived<T>::Derived() { this->member(); // `this'를 사용하면 // T를 구체화한 유형을 사용한다는 뜻이다. Derived<T>::member(); // 같음: Derived 멤버를 호출한다. Base<T>::member(); // 같음: Base 멤버를 호출 std::cout << "Derived<T>::Derived() completed\n"; } int main() { Derived<double> d; Derived<int> i; } /* 출력: This is Derived<T>::member() This is Derived<T>::member() This is Base<T>::member() Derived<T>::Derived() completed This is Derived<T>::member() This is Derived<T>::member() This is the int-specialization Derived<T>::Derived() completed */
위의 예제는 또한 가상 멤버 템플릿의 사용법을 보여준다 (물론 가상 멤버는 자주 사용되지 않는다). 예제에서 Base
는 virtual void member
를 선언하고 Derived
는 자신의 재정의 member
함수를 선언한다. 이 경우 Derived::Derived
의 this->member()
는 member
의 가상 함수적 성질 때문에 Derived::member
를 호출한다. 그렇지만 서술문 Base<T>::member()
는 언제나 Base
의 member
멤버 함수를 호출하며 그러므로 동적 다형성을 우회할 수 있다.
typename
키워드를 사용한다.
그러나 typename
이 언제나 구세주가 되는 것은 아니다. 소스를 파싱할 때 컴파일러는 일련의 토큰들을 받는다. 토큰은 프로그램 소스에서 각각 의미있는 텍스트 한 단위를 대표한다. 토큰은 식별자나 숫자를 나타낼 수 있다. 다른 토큰들은 =
나 +
또는 <
같은 연산자를 대표할 수 있다. 문제를 일으키는 토큰은 언제나 정확하게 가장 마지막 토큰이다. 토큰은 저마다 의미가 다르기 때문이다. 컴파일러가 <
를 만나는 문맥에서 언제나 올바르게 의미를 결정할 수 있는 것은 아니다. 어떤 상황에서는 <
가 보다 작은을 나타내는 연산자가 아니라는 사실을 컴파일러가 인지한다. 템플릿 매개변수 리스트가 template
키워드 다음에 오기 때문이다. 예를 들어,
template <typename T, int N>이 경우 분명히
<
는 `보다 작은' 연산자를 나타내지 않는다.
이 항은 template
다음에 오는 <
의 특별한 의미를 토대로 구문적 생성을 논의한다.
클래스가 다음과 같이 정의되어 있다고 가정하자.
template <typename Type> class Outer { public: template <typename InType> class Inner { public: template <typename X> void nested(); }; };
Outer
클래스 템플릿은 Inner
클래스 템플릿을 내포한다. 이어서 Inner
는 템플릿 멤버 함수를 정의한다.
다음으로 Usage
클래스 템플릿을 정의한다. Usage
는 caller
멤버 함수를 제공한다. 위의 Inner
유형의 객체 중 하나를 기대한다. Usage
를 위한 최초의 설정은 다음과 같다.
template <typename T1, typename T2> class Usage { public: void caller(Outer<T1>::Inner<T2> &obj); ... };컴파일러는 이것을 받아들이지 않는다.
Outer<T1>::Inner
를 클래스 유형으로 이해하기 때문이다. 그러나 Outer<T1>::Inner
클래스는 없다. 여기에서 컴파일러는 다음과 같은 에러 메시지를 토해 낸다.
error: 'class Outer<T1>::Inner' is not a type 에러: 'class Outer<T1>::Inner'는 유형이 아님컴파일러에게
Inner
자체가 템플릿 유형 매개변수 <T2>
를 사용하는 템플릿이라는 사실을 알려 주기 위해 ::template
지정이 필요하다. 컴파일러에게 다음에 나오는 <
를 `보다 작은' 토큰으로 이해하지 말고 템플릿 유형 인자로 이해하라고 알려줄 것이다. 그래서 선언을 다음과 같이 변경한다.
void caller(Outer<T1>::template Inner<T2> &obj);이렇게 해도 여전히 목적지에 도달하지 못한다. 어쨋거나
Inner<T2>
는 유형이다. 클래스 템플릿 아래에 내포되어 있고 템플릿 유형 매개변수에 따라 달라진다. 사실, 원래의 Outer<T1>::Inner<T2> &obj
선언 때문에 일련의 에러 메시지가 발생한다. 그 중에 하나는 이것이다.
error: expected type-name before '&' token 에러: '&' 토큰 앞에 유형-이름을 예상함.멋지게도 이 에러 메시지는 올바르게 처리하기 위해 무엇을 해야 하는지 알려준다.
typename
을 추가하라.
void caller(typename Outer<T1>::template Inner<T2> &obj);
물론 caller
자체가 선언되어 있어야 할 뿐만 아니라 구현도 되어 있어야 한다. 그의 구현은 Inner
클래스의 nested
멤버를 호출해야 한다고 가정하자. 역시 또다른 유형 X
에 대하여 구체화되어야 한다. 그러므로 Usage
클래스 템플릿은 세 번째 템플릿 유형 매개변수를 받아야 한다. 이 매개변수는 T3
이라고 부른다. 그것이 이미 정의되어 있다고 간주하고 다음과 같이 caller
를 구현한다.
void caller(typename Outer<T1>::template Inner<T2> &obj) { obj.nested<T3>(); }여기에서 또 한 번 문제에 봉착한다. 함수의 몸체에서 컴파일러는 한 번 더
<
를 `보다 작은'으로 이해한다. 오른쪽에 있는 논리 표현식을 기본 표현식으로 간주한다. 템플릿 유형 T3
을 지정하는 함수 호출로 보지 않는다.
<T3>
을 구체화할 유형으로 이해해야 한다고 컴파일러에게 알려주려면 template
키워드를 한 번 더 사용해야 한다. 이번에는 멤버 선택 연산자의 문맥에서 사용된다. .template
를 지정함으로써 다음에 오는 것은 `보다 작은' 연산자가 아니라 유형 지정이라고 컴파일러에게 알린다. 함수의 최종 구현은 다음과 같이 된다.
void caller(typename Outer<T1>::template Inner<T2> &obj) { obj.template nested<T3>(); }
값 또는 참조 매개변수 대신에 함수는 포인터 매개변수를 정의할 수도 있다. obj
가 포인터 매개변수로 정의되어 있다면 그 구현은 .template
생성이 아니라 ->template
생성을 사용해야 했을 것이다. 예를 들어,
void caller(typename Outer<T1>::template Inner<T2> *ptr) { ptr->template nested<T3>(); }
이미 보았듯이 클래스 템플릿은 바탕 클래스 템플릿으로부터 상속받을 수 있다. 바탕 클래스 템플릿은 정적 멤버 템플릿을 선언할 수 있다. 이 바탕 클래스를 상속받은 클래스는 이를 이용할 수 있다. 그런 바탕 클래스는 모습이 다음과 같을 것이다.
template <typename Type> struct Base { template <typename Tp> static void fun(); };
바탕 클래스에 정적 멤버가 정의되어 있으면 그냥 이름 앞에 클래스 이름을 붙여서 호출하기만 하면 된다. 예를 들어,
int main() { Base<int>::fun<double>(); }
Derived
클래스가 Base
를 상속받아 특정한 유형에 대하여 구체화될 경우에도 잘 작동한다.
struct Der: public Base<int> { static void call() { Base<int>::fun<int>(); // OK fun<int>(); // 역시 OK }; };그렇지만 파생 클래스 자체가 클래스 템플릿이라면 이런 식으로
fun
을 호출하는 것은 더 이상 컴파일되지 않는다. 왜냐하면 Base<Type>::fun<int>
안의 Base<Type>::fun
을 유형으로 추론하므로 int
에 대하여 구체화되기 때문이다. fun
이 바로 템플릿이라고 알려 주면 이런 추론을 멈추게 할 수 있다. 이를 위하여 앞에 ::template
키워드를 배치한다.
template <typename Type> struct Der: public Base<Type> { //template <typename Tp> // 'call'은 멤버 템플릿일 수 있다. //static // 'call'은 정적 멤버일 수 있다. void call() { // fun<int>(); // 컴파일 안됨 // Base<Type>::fun<int>(); // 컴파일 안됨 Base<Type>::template fun<int>(); // OK Base<Type>::template fun<Tp>(); // call이 멤버 템플릿이라면 // OK }; };
enum
)로 나타내기를 선호한다. 열거체를 int const
값보다 선호하는 이유는 열거체 값은 링크를 요구하지 않기 때문이다. 순수한 심볼 값으로서 메모리 표현이 없다.
프로그래머가 reinterpret_cast
연산자로 유형을 변환해야 경우를 생각해 보자. reinterpret_cast
의 문제는 궁극적으로 컴파일 점검을 모조리 꺼버린다는 것이다. 모든 것이 취소되고 다음과 같이 극단적이지만 절대적으로 의미 없는 reinterpret_cast
서술문을 작성할 수 있다.
int intVar = 12; ostream &ostr = reinterpret_cast<ostream &>(intVar);
컴파일러가 그런 이상한 것들에 대하여 에러 메시지로 경고해 주면 멋지지 않을까?
그렇다면 이상한 것과 미친 것을 구별할 방법이 있어야 한다. 다음 구분에 동의한다고 가정하자. 목표 유형이 표현 (소스) 유형보다 더 큰 유형이면 reinterpret_cast
는 절대로 받아들일 수 없다. 그렇게 되면 즉시 목표 유형에 허용된 메모리가 넘쳐 버릴 것이다. 이런 이유 때문에 reinterpret_cast<double *>(&intVar)
는 확실히 어리석지만 reinterpret_cast<char *>(&intVar)
는 허용할 만하다.
이제 새로운 종류의 유형 변환을 만들어 보자. reinterpret_to_smaller_cast
라고 부르도록 하자. 목표 유형이 소스 유형보다 메모리를 더 적게 점유할 경우에만 reinterpret_to_smaller_cast
을 수행되도록 허용해야 한다 (이것은 안드레스쿠(Alexandrescu (2001), section 2.1)의 의도와 정확하게 정반대임을 눈여겨보자.).
먼저, 다음 템플릿을 생성한다.
template<typename Target, typename Source> Target &reinterpret_to_smaller_cast(Source &source) { // 목표가 소스보다 더 작은지 결정한다. return reinterpret_cast<Target &>(source); }
주석 위치에 연상 이름을 가진 심볼을 정의한 열거체를 삽입한다. 요구 조건을 만족하지 못하면 컴파일 시간 에러가 일어난다. 그리고 그 에러 메시지는 심볼의 이름을 보여준다. 0으로 나누는 것은 당연히 허용하지 않는다. 그리고 false
값은 0으로 나타낸다는 것에 주목하면 그 조건은 다음과 같을 것이다.
1 / (sizeof(Target) <= sizeof(Source));흥미로운 부분은 이 조건은 코드를 전혀 생산하지 않는다는 것이다. 열거체의 값은 평범한 값으로서 표현식을 평가하는 동안 컴파일러가 계산해 준다.
template<typename Target, typename Source> Target &reinterpret_to_smaller_cast(Source &source) { enum { the_Target_size_exceeds_the_Source_size = 1 / (sizeof(Target) <= sizeof(Source)) }; return reinterpret_cast<Target &>(source); }
reinterpret_to_smaller_cast
를 사용하여 int
를 double
로 유형을 변환하면 컴파일러는 다음과 같이 에러를 일으킨다.
error: enumerator value for 'the_Target_size_exceeds_the_Source_size' is not an integer constant error: 'the_Target_size_exceeds_the_Source_size'에 대한 열거 값이 정수형 상수가 아닙니다.반면에
double
로 정의된 doubleVar
를 가지고 reinterpret_to_smaller_cast<int>(doubleVar)
을 요구하면 에러가 보고되지 않는다.
위의 예제는 열거체를 사용하여 (컴파일 시간에) 가정을 만족하지 못하면 불법인 값을 계산했다. 창조적인 부분은 적절한 표현식을 찾는 것이다.
열거 값들은 이런 상황에 잘 맞는다. 메모리를 소비하지 않으며 평가해도 실행 코드를 전혀 생산하지 않기 때문이다. 값을 추적하는 데에도 사용할 수 있다. 열거 값이 최종 결과가 된다. 실행 코드가 아니라 컴파일러가 계산한 값이다. 이는 다음 절에 보여준다. 일반적으로 컴파일 시간에 할 수 있다면 실행시간에 하지 말아야 한다. 상수 값이 결과인 복잡한 계산은 이 원칙의 좋은 예이다.
int
값을 `템플릿화'하는 것이다. 이것은 특정화를 선택하기 위해 스칼라 값(보통은 bool
)을 사용할 수 있지만 그 선택을 하기 위한 근거로 유형이 필요한 상황에 유용하다. 이런 상황을 잠시 후에 만나 본다 (23.2.2항).
정수 값을 템플릿화할 수 있는 것은 클래스 템플릿이 템플릿 인자를 보고 유형을 정의하기 때문이다. 예를 들어 vector<int>
와 vector<double>
는 유형이 다르다.
정수 값을 템플릿으로 쉽게 바꿀 수 있다. 템플릿을 정의하고 정수 값을 열거체 안에 저장한다 (안에 내용이 전혀 없어도 된다):
template <int x> struct IntType { enum { value = x }; };
IntType
에 멤버가 없기 때문에 `class IntType
'은 `struct IntType
'처럼 정의할 수 있다. 그러면 public:
을 타자하지 않아도 된다.
열거체로 `value
'를 정의하면 메모리를 전혀 소비하지 않고도 구체화에 사용된 그 값을 열람할 수 있다. 열거 값은 변수도 아니고 데이터 멤버도 아니다. 그래서 주소도 없다. 그저 값일 뿐이다.
struct IntType
를 사용하는 것은 어렵지 않다. 익명 또는 명명 객체를 정의하려면 int
비-유형 매개변수에 값을 지정하면 된다. 예를 들어:
int main() { IntType<1> it; cout << "IntType<1> objects have value: " << it.value << "\n" << "IntType<2> objects are of a different type " "and have values " << IntType<2>().value << '\n'; }실제로 명명 객체도 익명 객체도 필요 없다.
struct IntType
와 관련하여 열거체가 평범한 값으로 정의되어 있기 때문에 그냥 struct IntType
에 정의된 int
를 구체적으로 지정하기만 하면 `value
'를 다음과 같이 열람할 수 있다.
int main() { cout << "IntType<100>, no object, defines `value': " << IntType<100>::value << "\n"; }
if
와 switch
서술문이 있다. `컴파일러를 프로그래밍'하고 싶다면 이 특징도 템플릿으로 제공해야 한다.
값을 저장하는 템플릿처럼 선택을 하는 템플릿은 실행 시간에 어떤 코드도 실행하기를 요구하지 않는다. 선택은 순수하게 컴파일러에 의해 컴파일 시간에 이루어진다. 템플릿 메타 프로그래밍의 정수는 어떠한 코드도 실행하거나 어떤 코드에도 의존하지 않는다는 것이다. 템플릿 메타 프로그래밍의 결과가 실행 코드일지라도 그 코드는 그냥 컴파일러가 만들어 낸 결정 함수일 뿐이다.
템플릿 (멤버) 함수는 실제로 사용될 때만 구체화된다. 결과적으로 서로 배타적인 특수 함수를 정의할 수 있다. 그리하여 이 상황에서는 컴파일되지만 저 상황에서는 컴파일되지 않는 그리고 저 상황에서는 컴파일되지만 이 상황에서는 컴파일되지 않는 특수 함수를 정의할 수 있다. 특정화를 사용하면 특정한 상황의 요구에 맞게 코드를 만들어 낼 수 있다.
이와 같은 특징은 실행 시간 코드로는 구현할 수 없다. 총칭 저장 클래스를 설계할 때 프로그래머는 최종 저장 클래스에 값 클래스 유형의 객체는 물론 다형적 클래스 유형의 객체도 저장하려고 할 것이다. 그리하여 그 프로그래머는 저장 클래스에 객체 자체가 아니라 객체를 가리키는 포인터가 있어야 한다고 결론을 내린다. 초기 구현 시도는 다음과 같을 것이다.
template <typename Type> void Storage::add(Type const &obj) { d_data.push_back( d_ispolymorphic ? obj.clone() : new Type(obj) ); }
Type
이 다형적 클래스이면 Type
클래스의 clone
멤버 함수를 사용하고 Type
이 값 클래스이면 표준 복사 생성자를 사용하는 것이 의도이다.
안타깝하게도 이 전략은 실패한다. 값 클래스에 clone
멤버 함수가 정의되어 있지 않고 다형성에 기반한 클래스는 자신의 복사 생성자를 삭제해야 하기 때문이다 (7.6절). clone
이 값 클래스에 대하여 전혀 호출되지 않으며 복사 생성자는 값 클래스에만 사용할 수 있고 다형적 클래스에는 사용할 수 없다는 사실에 컴파일러는 개의치 않는다. 컴파일러는 그저 코드를 컴파일할 뿐이며 멤버가 없기 때문에 그렇게 할 수도 없다. 아주 단순하다.
add
멤버 함수를 디자인하는 것이다. 그 중에 하나만 호출되고 구체화될 것이다. (Type
자체와 더불어) 추가 템플릿 비-유형 매개변수에 기반하여 선택할 것이다. 이 매개변수는 다형적 클래스인지 아니면 비-다형적 클래스인지에 대하여 Storage
를 사용할지 말지를 나타낸다. Storage
클래스는 다음과 같이 시작한다.
template <typename Type, bool isPolymorphic> class Storage맨처음에
add
멤버의 중복정의 버전을 두 가지 정의한다. Storage
객체에 사용되는 버전 하나는 다형적 객체를 저장하고 (true
를 템플릿 비-유형 인자로 사용) 다른 버전은 값 클래스 객체를 저장한다 (false
를 템플릿 비-유형 인자로 사용).
불행하게도 작은 문제에 봉착한다. 함수는 인자 값으로 중복정의할 수 없고 인자 유형으로만 중복정의가 가능하다. 그러나 이 작은 문제는 해결할 수 있다. 유형이 템플릿의 이름과 인자로 정의된다는 사실을 알고 있다면 true
와 false
값을 유형으로 변환할 수 있다. 23.2.1.1목에서 정수 값을 유형으로 변환하는 법에 관하여 얻은 지식을 활용하자.
(다형적 클래스를 구현하기 위해) 한 (비밀) add
멤버에 IntType<true>
매개변수를 제공하고 (비-다형적 클래스를 구현하기 위해) 또다른 (비밀) add
멤버에 IntType<false>
매개변수를 제공하겠다.
이 두 개의 비밀 멤버 말고도 세 번째 (공개) add
멤버를 정의한다. Storage
의 템플릿 비-유형 매개변수로부터 생성된 IntType
인자를 제공하면 그에 맞는 비밀 add
멤버를 호출한다.
다음은 add
멤버의 세 가지 구현이다.
// Storage의 비밀 구역에 선언된다. template <typename Type, bool isPolymorphic> void Storage<Type, isPolymorphic>::add(Type const &obj, IntType<true>) { d_data.push_back(obj.clone()); } template <typename Type, bool isPolymorphic> void Storage<Type, isPolymorphic>::add(Type const &obj, IntType<false>) { d_data.push_back(new Type(obj)); } // Storage의 공개 구역에 선언된다. template <typename Type, bool isPolymorphic> void Storage<Type, isPolymorphic>::add(Type const &obj) { add(obj, IntType<isPolymorphic>()); }적절한
add
멤버가 구체화되어 호출된다. 원시 값을 유형으로 변환할 수 있기 때문이다. 템플릿 비-유형 값에 따라 각각 중복정의 클래스 템플릿 멤버 함수가 정의된다.
클래스 템플릿 멤버는 사용될 때만 구체화되기 때문에 중복정의 비밀 add
멤버중 하나만 구체화된다. 다른 멤버들은 절대로 호출되지 않으므로 (그리하여 절대로 구체화되지 않으므로) 컴파일 에러가 방지된다.
Storage
클래스가 포인터를 사용하여 값 클래스 객체의 사본을 저장하는 것을 탐탁지 않게 생각한다. 값 클래스 객체는 포인터보다 값으로 저장하는 편이 더 좋다고 그들은 주장한다. 그들은 값 클래스 객체는 값으로 저장하고 다형적 클래스 객체는 포인터로 저장하기를 선호한다.
그런 구분은 템플릿 메타 프로그래밍에서 자주 일어난다. 다음 IfElse
구조체를
사용하면 bool
선택자 값에 따라 두 유형 중 하나를 얻을 수 있다.
먼저 템플릿의 총칭 형태를 정의한다.
template<bool selector, typename FirstType, typename SecondType> struct IfElse { typedef FirstType type; };다음 부분적 특정화를 정의한다. 특정화는 특정한 선택자 값을 나타낸다 (예,
false
). 그리고 앞으로 더 지정하도록 나머지 유형은 열어 둔다.
template<typename FirstType, typename SecondType> struct IfElse<false, FirstType, SecondType> { typedef SecondType type; };앞의 (총칭적) 정의는
FirstType
을 IfElse::type
유형 정의에 연관짓고 (논리 값 false
에 대하여 부분적으로 특정화된) 뒤의 정의는 SecondType
을 IfElse::type
유형 정의에 연관짓는다.
IfElse
템플릿을 사용하여 템플릿의 매개변수에 대하여 조건적으로 데이터가 조직되는 클래스 템플릿을 정의할 수 있다. IfElse
템플릿 덕분에 Storage
클래스는 포인터를 정의하여 다형적 클래스 유형의 객체의 사본을 저장하며 그리고 값을 정의하여 값 클래스 유형의 객체를 저장할 수 있다.
template <typename Type, bool isPolymorphic> class Storage { typedef typename IfElse<isPolymorphic, Type *, Type>::type DataType; std::vector<DataType> d_data; private: void add(Type const &obj, IntType<true>); void add(Type const &obj, IntType<false>); public: void add(Type const &obj); } template <typename Type, bool isPolymorphic> void Storage<Type, isPolymorphic>::add(Type const &obj, IntType<true>) { d_data.push_back(obj.clone()); } template <typename Type, bool isPolymorphic> void Storage<Type, isPolymorphic>::add(Type const &obj, IntType<false>) { d_data.push_back(obj); } template <typename Type, bool isPolymorphic> void Storage<Type, isPolymorphic>::add(Type const &obj) { add(obj, IntType<isPolymorphic>()); }위의 예제는
IfElse
가 정의한 유형인 FirstType
이나 SecondType
을 사용한다. IfElse
는 Storage
의 벡터 데이터 유형에 대하여 사용할 실제 데이터 유형을 정의한다.
이 예제에서 놀라운 결과는 Storage
클래스의 데이터 조직이 이제 자신의 템플릿 인자에 따라 달라진다는 것이다. isPolymorphic == false
일 때와 isPolymorphic == true
일 때 서로 다른 데이터 유형을 사용하기 때문에 중복정의 비밀 add
멤버는 이 차이를 즉시 이용할 수 있다. 예를 들어 add(Type const &obj, IntType<false>)
는 직접적으로 복사 생성하여 obj
의 사본을 d_vector
에 저장한다.
여러 유형으로부터 선택하는 것도 가능하다. IfElse
구조체는 내포가 가능하기 때문이다. IfElse
를 사용하더라도 최종 실행 프로그램의 크기나 속도에 전혀 영향을 미치지 않는다는 것을 눈여겨보라. 최종 프로그램은 그냥 마침내 적절하게 선택된 유형을 가질 뿐이다.
MapType
를 정의한다. 이 접근법을 예시한다.
template <typename Key, typename Value, int selector> class Storage { typedef typename IfElse< selector == 1, // if selector == 1: map<Key, Value>, // use map<Key, Value> typename IfElse< selector == 2, // if selector == 2: map<Key, Value *>, // use map<Key, Value *> typename IfElse< selector == 3, // if selector == 3: map<Key *, Value>, // use map<Key *, Value> // otherwise: map<Key *, Value *> // use map<Key *, Value *> >::type >::type >::type MapType; MapType d_map; public: void add(Key const &key, Value const &value); private: void add(Key const &key, Value const &value, IntType<1>); ... }; template <typename Key, typename Value, int selector> inline void Storage<selector, Key, Value>::add(Key const &key, Value const &value) { add(key, value, IntType<selector>()); }위의 예제에서 사용된 원칙은 클래스 템플릿이 템플릿 비-유형 매개변수에 따라 달라지는 데이터 유형을 사용하면
IfElse
구조로 데이터 유형을 적절하게 선택할 수 있다는 것이다. 다양한 데이터 유형에 관한 지식도 활용하여 중복정의 멤버 함수를 정의할 수 있다. 그러면 이 중복정의 멤버의 구현은 다양한 데이터 유형으로 초기화될 수 있다. 프로그램에서는 이 대안 함수들 중의 하나만 구체화될 것이다 (실제로 사용된 데이터 유형으로 최적화된 함수만 구체화된다).
비밀 add
함수들은 같은 매개변수들을 공개 add
포장 함수로 정의한다. 그러나 특정한 IntType
유형을 추가한다. 컴파일러는 템플릿의 비-유형 선택자 매개변수에 맞게 중복정의 버전을 적절하게 선택할 수 있다.
for
또는 while
서술문에 해당하는 것이 없다. 그렇지만 반복은 언제나 재귀로 재작성할 수 있다. 템플릿은 재귀를 지원한다. 그래서 반복은 언제나 (꼬리) 재귀로 구현할 수 있다.
(꼬리) 재귀로 반복을 구현하려면 다음과 같이 한다.
enum
값으로 저장한다.
아마도 느낌표(!
)로 나타내는 수학의 `계승' 연산자의 재귀 구현에 익숙할 것이다. n
계승이면 (n!
) 연속적으로 n * (n - 1) * (n - 2) * ... * 1
까지의 곱을 돌려준다. 이것은 n
개의 객체를 나열한 순열의 갯수를 나타낸다. 흥미롭게도 계승 연산자 자체는 재귀적으로 정의되는 것이 보통이다.
n! = (n == 0) ? 1 : n * (n - 1)!템플릿에서
n!
을 계산하기 위해 int n
템플릿 비-유형 매개변수를 사용하여 Factorial
템플릿을 정의할 수 있다. n == 0
사례를 처리하기 위하여 특정화를 정의한다. 총칭적 구현은 계승의 정의에 맞게 재귀를 사용한다. 그리고 Factorial
템플릿은 열거 값 `value
'를 정의한다. 안에 계승 값이 들어 있다. 다음은 총칭적 정의이다.
template <int n> struct Factorial { enum { value = n * Factorial<n - 1>::value }; };값을 `
value
'에 할당하는 표현식을 눈여겨보자. 컴파일러가 결정할 수 있는 상수 값을 표현식이 어떻게 사용하는지 주목하라. n
값을 준다. 그리고 템플릿 메타 프로그래밍을 사용하여 Factorial<n - 1>
을 계산한다. Factorial<n-1>
은 이어서 컴파일러가 결정할 수 있는 값을 생산한다 (정확하게는 Factorial<n-1>::value
). Factorial<n-1>::value
는 Factorial<n - 1>
유형이 정의한 value
를 나타낸다. 그 유형의 객체가 돌려준 값이 아니다. 여기에는 객체가 없다. 그저 유형이 정의한 값만 있을 뿐이다.
재귀는 특정화에서 끝난다. 컴파일러는 (종료 값이 0이라면) 가능하면 총칭 구현 대신에 특정화를 선택한다. 다음은 특정화의 구현이다.
template <> struct Factorial<0> { enum { value = 1 }; };
Factorial
템플릿을 사용하면 고정 갯수의 객체를 나열한 순열이 몇 개인지 컴파일 시간에 알 수 있다. 예를 들어,
int main() { cout << "The number of permutations of 5 objects = " << Factorial<5>::value << "\n"; }역시
Factorial<5>::value
는 실행 시간이 아니라 컴파일 시간에 평가된다. 그러므로 위의 cout
서술문의 실행시간 버전은 다음과 같다.
int main() { cout << "The number of permutations of 5 objects = " << 120 << "\n"; }
template <char ...Chars> Type operator "" _identifier()이 비-유형 매개변수 함수 템플릿은 매개변수를 하나도 정의하지 않고 그저 가변의 비-유형 매개변수 리스트만 선언한다.
인자는 int 상수이어야 한다. 왜냐하면 unsigned long long int
매개변수를 정의한 기호상수 연산자도 기대하기 때문이다. int 상수의 문자는 모두 하나씩 char
비-유형 템플릿 인자로 기호상수 연산자에 건네진다.
예를 들어 _NM2km
가 기호상수 연산자 함수 템플릿이라면 80_NM2km
으로 호출할 수 있다. 그러면 실제로 _NM2km<'8', '0'>()
으로 함수 템플릿을 호출할 수 있다. 이 함수 템플릿이 템플릿 메타 프로그래밍 테크닉만 사용하여 정수형 데이터만 처리한다면 그의 행위는 컴파일 시간에 완벽하게 수행할 수 있다. 이를 보여주기 위하여 NM2km
가 부호 없는 값을 처리하여 돌려준다고 가정하자.
_NM2km
함수 템플릿은 인자를 클래스 템플릿에 전달할 수 있다. 이 클래스 템플릿은 열거체 상수 value
를 정의하고 필요한 계산을 수행한다. 다음은 가변의 기호상수 연산자 함수 템플릿 _NM2km
의 구현이다.
template <char ... Chars> size_t constexpr operator "" _NM2km() { return static_cast<size_t>( // Chars를 NM2km에 전달한다. NM2km<0, Chars ...>::value * 1.852); }클래스 템플릿
NM2km
는 비-유형 매개변수를 세 가지 정의한다. acc
는 값을 축적하고 c
는 가변 비-유형 매개변수의 첫 번째 문자이다. 반면에 ...Chars
는 나머지 비-유형 매개변수를 나타낸다. 비-유형 매개변수 팩 안에 담겨 있다. c
는 재귀 호출마다 원래의 비-유형 매개변수 팩으로부터 공급되는 다음 문자이기 때문에 지금까지 10을 곱하고 다음 문자의 값을 더해 축적된 값이 다음 재귀 호출에 건네진다. 그와 함께 Chars ...
로 나타낸 매개변수 팩의 나머지 원소들도 건네진다.
template <size_t acc, char c, char ...Chars> struct NM2km { enum { value = NM2km<10 * acc + c - '0', Chars ...>::value }; };마침내 매개변수 팩은 비게 된다. 이 경우를 위해
NM2km
의 부분적 특정화를 사용할 수 있다.
template <size_t acc, char c> // 빈 매개변수 팩 struct NM2km<acc, c> { enum { value = 10 * acc + c - '0' }; };
이것은 잘 작동하지만 이진 값이나 팔진 값 또는 십육진 값을 번역해야 하는 경우라면 작동하지 않는다. 이 경우라면 먼저 첫 번째 문자가 특별한 숫자 시스템을 나타내는지 결정해야 한다. 이제 _NM2km
기호상수 연산자로부터 호출되는 NM2kmBase
로 이를 결정할 수 있다.
template <char ... Chars> size_t constexpr operator "" _NM2km() { return static_cast<size_t>( // Chars를 NM2kmBase에 전달한다. NM2kmBase<Chars ...>::value * 1.852); }
NM2kmBase
클래스 템플릿은 보통 십진수 시스템이라고 가정하고, 밑수를 10으로 해서 최초 값 0을 NM2km
에 건넨다. NM2km
클래스 템플릿에 추가로 (첫 번째) 비-유형 매개변수를 제공한다. 이 매개변수는 사용할 숫자 시스템의 밑수를 대표한다. 다음은 NM2kmBase
이다.
template <char ...Chars> struct NM2kmBase { enum { value = NM2km<10, 0, Chars ...>::value }; };부분적 특정화는 다양한 숫자 시스템을 처리한다. 앞쪽 (한 두) 문자를 검사해 본다.
template <char ...Chars> struct NM2kmBase<'0', Chars ...> // "0..." { enum { // 8진 값: 밑수 8 value = NM2km<8, 0, Chars ...>::value }; }; template <char ...Chars> struct NM2kmBase<'0', 'b', Chars ...> // "0b..." { enum { // 이진 값: 밑수 2 value = NM2km<2, 0, Chars ...>::value }; }; template <char ...Chars> struct NM2kmBase<'0', 'x', Chars ...> // "0x..." { enum { // 16진 값: 밑수 16 value = NM2km<16, 0, Chars ...>::value }; };
NM2km
는 앞과 같이 구현되었다. 단, 이제는 다양한 숫자 시스템을 처리할 수 있다. 문자를 숫자로 변환하는 일은 작은 지원 함수 템플릿 cVal
에게 맡긴다.
template <char c> int constexpr cVal() { return '0' <= c <= '9' ? c - '0' : 10 + c - 'a'; } template <size_t base, size_t acc, char c, char ...Chars> struct NM2km { enum { value = NM2km<base, base * acc + cVal<c>(), Chars ...>::value }; }; template <size_t base, size_t acc, char c> struct NM2km<base, acc, c> { enum { value = base * acc + cVal<c>() }; };
Storage
저장 클래스를 설계해 달라고 부탁했다. Storage
객체에 저장된 데이터는 데이터의 사본을 만들어 저장하거나 아니면 받은 그대로 저장한다. Storage
객체는 벡터나 연결 리스트도 아래에 깔린 저장 매체로 사용할 수 있다. 프로그래머는 이 요구조건을 어떻게 처리해야 할까? 네가지 다른 Storage
클래스를 설계해야 할까?
프로그래머의 첫 번째 대응은 만능 Storage
클래스를 개발하는 것이다. 두 개의 데이터 멤버로 리스트와 벡터를 가질 수 있고 생성자는 아마도 열거 값으로 제공할 수 있을 것이다. 데이터 자체를 저장할지 아니면 새 사본을 저장할지 알려준다. 열거 값을 사용하면 일련의 포인터를 멤버 함수로 초기화할 수 있다. 요청된 과업을 수행한다 (예를 들어 벡터를 사용하여 데이터를 저장하거나 리스트를 사용하여 사본을 저장한다).
복잡하기는 하지만 실행할 수 있다. 그런데 또 프로그래머에게 클래스를 변경해 달라고 요구한다. 새 사본의 경우에 표준 new
연산자 말고 맞춤 할당 전략을 사용해야 한다. 또 이미 설계에 포함된 벡터와 리스트 말고도 또다른 유형의 컨테이너를 사용할 수 있도록 만들어 달라고 요구한다. 아마도 데크가 좋을 것이다. 스택이면 더 좋다.
한 클래스 안에 모든 기능을 구현하고 가능한 모든 조합을 구현하려는 시도는 유연하지 못한 것이 확실하다. Storage
클래스는 조만간 뚱보가 되어 버린다. 이해하고 유지 관리하고 테스트하고 전개하기가 몹시 어려워진다.
단번에 모든 것을 갖춘 거대한 클래스가 이해하기도 어렵고 전개하기도 어려운 이유 중 하나는 설계가 잘 된 클래스는 제약을 강제해야 한다는 것이다. 클래스의 설계는 자체적으로 어떤 연산을 허용하면 안되며 그것을 위반하는 행위는 컴파일러가 탐지해야 한다. 프로그램이 탐지하게 되면 심각한 에러로 종료해 버리기 때문이다.
위의 요청에 대하여 생각해 보자. 벡터 데이터 저장소에 접근하는 인터페이스와 리스트 데이터 저장소에 접근하는 인터페이스를 둘 다 클래스가 갖추고 있다면 중복정의 operator[]
멤버로 벡터에 있는 원소에 접근할 가능성이 매우 높다. 그렇지만 리스트 데이터 저장소를 선택한다면 이 멤버는 구문적으로 존재하겠지만 의미구조적으로 무효이다. operator[]
를 지원하지 않기 때문이다.
얼마 못 가 이 뚱보 Storage
클래스의 사용자는 함정에 빠질 것이다. 그 아래의 데이터 저장소로 리스트를 선택했음에도 불구하고 operator[]
를 사용하는 함정에 빠질 가능성이 매우 높다. 컴파일러는 사용자를 혼란시키는 그 에러를 탐지할 수 없다. 이 에러는 프로그램이 실행중일 경우에만 나타나기 때문이다.
여전히 의문이 남는다. 위의 난관을 마주했을 때 프로그래머는 어떻게 처리해야 하는가? 정책을 소개할 시간이다.
앞 절에서 일련의 배당 전략을 사용하는 클래스를 생성하는 문제를 소개했다. 이 배당 전략은 모두 사용할 실제 데이터 유형에 따라 달라진다. 그래서 `템플릿 반영(template reflex)'을 도입해야 한다.
배당 정책은 아마도 템플릿 클래스로 정의해야 할 것이다. 당면한 데이터 유형에 적절한 배당 절차를 적용한다. 그런 배당 정책을 (std::vector, std::stack
, 등등과 같은) 친숙한 STL 컨테이너들이 사용할 때 요구 조건을 만족시키기 위해 자체적 배당 전략은 아마도 std::allocator
로부터 물려받아야 할 것이다. std::allocator
클래스 템플릿은 <memory>
헤더 파일에 선언되어 있다. 그리고 여기에 개발된 세 가지 배당 정책은 모두 std::allocator
으로부터 상속받았다.
간략하게 인-클래스 구현을 사용하면 다음 배당 클래스를 정의할 수 있다.
Data
는 `있는 그대로' 사용된다.
template <typename Data> class PlainAlloc: public std::allocator<Data> { template<typename IData> friend std::ostream &operator<<(std::ostream &out, PlainAlloc<IData> const &alloc); Data d_data; public: PlainAlloc() {} PlainAlloc(Data const &data) : d_data(data) {} PlainAlloc(PlainAlloc<Data> const &other) : d_data(other.d_data) {} };
new
연산자를 사용하여 데이터의 새 사본을 배당한다.
template <typename Data> class NewAlloc: public std::allocator<Data> { template<typename IData> friend std::ostream &operator<<(std::ostream &out, NewAlloc<IData> const &alloc); Data *d_data; public: NewAlloc() : d_data(0) {} NewAlloc(Data const &data) : d_data(new Data(data)) {} NewAlloc(NewAlloc<Data> const &other) : d_data(new Data(*other.d_data)) {} ~NewAlloc() { delete d_data; } };
new
연산자를 사용한다 (9.1.5항). 공용 풀에다 메모리를 요청한다 (요구한 메모리를 얻는 request
멤버의 구현은 독자 여러분에게 연습 문제로 남긴다.):
template<typename Data> class PlacementAlloc: public std::allocator<Data> { template<typename IData> friend std::ostream &operator<<(std::ostream &out, PlacementAlloc<IData> const &alloc); Data *d_data; static char s_commonPool[]; static char *s_free; public: PlacementAlloc() : d_data(0) {} PlacementAlloc(Data const &data) : d_data(new(request()) Data(data)) {} PlacementAlloc(PlacementAlloc<Data> const &other) : d_data(new(request()) Data(*other.d_data)) {} ~PlacementAlloc() { d_data->~Data(); } private: static char *request(); };
Storage
의 사용자가 선택할 수 있다. 이 클래스 외에도 사용자는 배당 전략을 더 구현할 수 있다.
적절한 배당 전략을 Storage
클래스에 적용하려면 클래스 템플릿으로 설계해야 한다. 또 사용자가 데이터 유형을 지정할 수 있도록 템플릿 유형 매개변수가 필요하다.
물론 특정한 배당 전략을 지정할 때 데이터 유형도 함께 지정해야 한다. Storage
클래스는 하나는 데이터 유형에 대하여 또 하나는 배당 전략에 대하여 두 개의 템플릿 유형 매개변수를 가질 것이다.
template <typename Data, typename Scheme> class Storage ...
Storage
클래스를 사용하려면 다음과 같이 작성한다. 예를 들어:
Storage<string, NewAlloc<string>> storage;이런 식으로
Storage
를 사용하는 것은 상당히 복잡하고 잠재적으로 에러를 일으킬 가능성이 매우 높다. 사용자가 데이터 유형을 두 번이나 지정해야 하기 때문이다. 대신에 배당 전략은 새 유형의 템플릿 매개변수를 사용하여 지정해야 한다. 사용자는 배당 전략에 필요한 데이터 유형을 지정할 필요가 없다. 새로운 이 템플릿 매개변수를 (유명한 템플릿 유형 매개변수와 템플릿 비-유형 매개변수와 더불어) 템플릿 템플릿 매개변수라고 부른다.
C++14 표준부터 class 키워드는 템플릿 템플릿 매개변수의 구문적 형태에서 더 이상 필수가 아니다. 앞으로는 typename
키워드도 사용할 수 있다
template <parameter specifications> class Name
template <parameter specifications> typename Name
(C++14)
Storage
클래스에 대하여 유형(type
) 배당이 아니라 정책을 배당하기 위해 클래스 템플릿 헤더를 변경해 보자. 다음과 같이 정의를 시작한다.
template <typename Data, template <typename> class Policy> class Storage...두 번째 템플릿 매개변수는 처음 소개하는 템플릿 템플릿 매개변수이다. 다음과 같은 요소를 가진다.
template
키워드로 템플릿 템플릿 매개변수를 시작한다.
template
키워드 다음에 템플릿 템플릿 매개변수에 대하여 지정해야 하는 (옆꺽쇠 사이에) 템플릿 매개변수 리스트가 온다. 이 매개변수들은 이름을 줄 수도 있지만 보통은 생략된다. 그런 이름들은 이어서 나오는 템플릿 정의에 사용할 수 없기 때문이다. 반면에 정식으로 이름을 주면 그 템플릿을 사용하는 독자가 템플릿 템플릿 매개변수로 지정해야 하는 템플릿의 종류를 이해하는 데 도움이 될 수 있다.
프로그래머는 이 기본값이 존재한다는 사실을 곧바로 깨닫지 못하고 템플릿 템플릿 매개변수로 그 템플릿들을 건네려고 시도할 가능성이 있다. 그러면 컴파일러가 그런 템플릿을 거부하기 때문에 혼란에 빠진다. 이 추가 (기본) 매개변수가 지정되지 않았기 때문이다. 그렇지만 이 문제에 대한 해결책이 있다. 템플릿 별칭이라는 형태를 23.5절에 소개한다.
class
키워드를 지정해야 한다. 이 경우는 typename
키워드를 사용할 수 없다. 그렇지만 앞으로 C++17 표준에서 이 제한은 풀릴 가능성이 아주 높다.
template < template < typename = std::string, int = 12, template <typename = int> class Inner = std::vector > class Policy > class Demo { ... };여기에서
Demo
클래스 템플릿은 이름이 Policy
인 템플릿 템플릿 매개변수 세 개를 기대한다. 템플릿 유형 매개변수(기본값 std::string
)와 템플릿 비-유형 매개변수(기본 값 12) 그리고 Policy
자체는 Inner
라고 부르는 템플릿 템플릿 매개변수를 기대한다. 기본 값으로 int
를 자신의 템플릿 유형 매개변수로 사용한다.
Policy
를 Storage
의 바탕 클래스로 사용한다.
정책은 Storage
클래스의 데이터 유형에 작동한다. 그러므로 정책은 데이터 유형에 관해서도 알고 있다. Storage
클래스는 이제 다음과 같이 시작한다.
template <typename Data, template <typename> class Policy> class Storage: public Policy<Data>이렇게 하면
Storage
클래스의 멤버들을 구현할 때 자동으로 Policy
의 멤버를 사용할 수 있다.
우리의 자족적 배당 클래스는 실제로는 유용한 멤버가 별로 없다. 추출 연산자를 제외하면 데이터에 직접 접근도 불가능하다. 이것은 멤버를 추가하면 쉽게 고칠 수 있다. 예를 들어 NewAlloc
클래스를 추가하고 저장된 데이터에 접근하고 수정하도록 그의 연산자에다 허용하면 된다.
operator Data &() // 선택적으로 `const' 멤버도 추가할 수 있다. { return *d_data; } NewAlloc &operator=(Data const &data) { *d_data = data; }다른 배당 클래스에도 비슷한 멤버들을 줄 수 있다.
실제 코드에 배당 전략을 사용해 보자. 다음 예제는 약간의 데이터 유형과 배당 전략을 사용하여 Storage
클래스를 정의하는 법을 보여준다. 다시 Storage
클래스로 시작하자.
template <typename Data, template <typename> class Allocate> class Storage: public std::vector<Data, Allocate<Data>> {};이것이 다이다.
std::vector
는 형식적으로 템플릿 매개변수가 두 개인 것을 눈여겨보자. 첫 번째는 벡터의 데이터 유형으로서 언제나 지정된다. 두 번째는 벡터가 사용하는 배당이다. 배당자는 지정하지 않고 그대로 두는 것이 보통이다 (이 경우 기본 STL 배당자가 사용된다). 그러나 여기에서는 명시적으로 언급한다. 그래서 독자적인 배당 정책을 Storage
클래스에 건넬 수 있다.
필요한 모든 편의기능은 vector
바탕 클래스로부터 상속받는다. 반면에 정책은 템플릿 템플릿 매개변수의 사용을 `고려한다'. 다음은 그 과정을 보여주는 예제이다.
Storage<std::string, NewAlloc> storage; copy(istream_iterator<std::string>(cin), istream_iterator<std::string>(), back_inserter(storage)); cout << "Element index 1 is " << storage[1] << '\n'; storage[1] = "hello"; copy(storage.begin(), storage.end(), ostream_iterator<NewAlloc<std::string> >(cout, "\n"));
Storage
객체는 std::vector
객체이므로 STL copy
함수를 꼬리 삽입 반복자와 함께 사용하면 데이터를 storage
객체에 추가할 수 있다. 그의 원소들은 인덱스 연산자를 사용하여 접근하고 변경한다. 다음 NewAlloc<std::string>
객체를 cout
으로 삽입한다 (역시 copy
함수를 사용한다.).
흥미롭게도 이것이 이야기의 끝이 아니다. 유념하자. 우리의 의도는 저장소 유형도 지정할 수 있도록 해주는 클래스를 만드는 것이었다. 벡터를 사용하고 싶지 않고 대신에 리스트를 사용하고 싶다면 어떻게 될까?
쉽게 Storage
클래스의 설정을 바꿀 수 있다. 예를 들어 요청만 하면 데크와 같이 완전히 다른 저장 유형을 사용할 수 있다. 이를 구현하기 위해 저장 클래스를 매개변수화한다. 또다른 템플릿 템플릿 매개변수를 사용한다.
template <typename Data, template <typename> class AllocatonPolicy, template <typename, typename> class Container = std::vector> class Storage: public Container<Data, AllocationPolicy<Data>> {};
Storage
객체를 사용하는 이전 예제를 (위의 정의만 빼고) 전혀 변경하지 않고 그대로 다시 사용할 수 있다. 확실히 리스트 컨테이너에는 사용할 수 없다. 리스트는 operator[]
가 없기 때문이다. 리스트에 operator[]
를 사용하여 접근을 시도하면 즉시 컴파일러가 알아채고 에러를 일으킨다 (배당 클래스와 Storage
클래스의 재정의와 더불어 그 사용법을 보여주는 완전한 예제는 소스 배포본 yo/advancedtemplates/examples/storage.cc
에 있다.). 그렇지만 리스트 컨테이너는 여전히 지정해 사용할 수 있다. 그 경우 Storage
는 리스트로 구현되었으므로 벡터 인터페이스가 아니라 리스트 인터페이스를 제공한다.
이런 상황은 합법적이기는 하지만 여러가지 이유 때문에 피하는 것이 좋다.
vtable
이 요구될 뿐만 아니라 그것을 가리키는 데이터 멤버도 요구된다.
vtable
과 그의 모든 가상 멤버도 구현해야 하기 때문이다.
정의가 잘된 인터페이스를 제공함으로써 정책 클래스로부터 파생된 클래스는 다른 구조의 정책 클래스를 활용하여 멤버 특정화를 정의할 수 있다. 예를 들어 평범한 포인터-기반의 정책 클래스는 C-스타일의 포인터 조작에 의존함으로써 자신의 기능을 제공할 수 있다. 반면에 벡터-기반의 정책 클래스는 벡터의 멤버를 직접적으로 사용할 수 있다.
다음 예제에서 컨테이너-류의 정책을 기대하는 총칭 Size
클래스 템플릿을 설계할 수 있다. 컨테이너에서 흔히 발견되는 특징들을 사용하며 정책에 지정된 컨테이너의 데이터를 (그러므로 그 구조를) 정의한다. 예를 들어:
template <typename Data, template <typename> class Container> struct Size: public Container<Data> { size_t size() { // 컨테이너의 `size()'에 의존한다. // 주의: `this->size()' 사용 불가 return Container<Data>::size(); } };이제 평범한 포인터를 사용하여 저장 클래스를 훨씬 더 단순하게 특정화할 수 있다 (구현은
std::pair
의 first
와 second
데이터 멤버를 이용한다. 이 절 마지막 예제 참고).
template <typename Data> struct Size<Data, Plain>: public Plain<Data> { size_t size() { // 포인터 데이터 멤버에 의존한다. return this->second - this->first; } };템플릿 저자의 의도에 따라 다른 멤버들도 구현할 수 있다.
위의 템플릿을 실제로 간단하게 사용하기 위해 총칭 포장 클래스를 구성할 수 있다. 실제로 사용된 저장 유형에 (예를 들어 std::vector
또는 평범한 저장 클래스에) 부합하는 Size
템플릿을 사용하여 그의 구조를 정의한다.
template <typename Data, template <typename> class Store> class Wrapper: public Size<Data, Store> {};
위의 클래스는 이제 다음과 같이 사용할 수 있다 (부언하면 지극히 기본적인 Plain
클래스를 보여준다.):
#include <iostream> #include <vector> template <typename Data> struct Plain: public std::pair<Data *, Data *> {}; int main() { Wrapper<int, std::vector> wiv; std::cout << wiv.size() << "\n"; Wrapper<int, Plain> wis; std::cout << wis.size() << "\n"; }
wiv
객체는 이제 벡터-데이터를 정의한다. wis
객체는 그저 std::pair
객체의 데이터 멤버들을 정의할 뿐이다.
템플릿 별칭을 템플릿 템플릿 매개변에 건네는 인자로 사용할 수 있다. 이렇게 하면 템플릿 템플릿 매개변수를 사용할 때 마주하기도 하는 `예상치 못한 기본 매개변수(unexpected default parameters)' 에러를 피할 수 있다. 예를 들어 template <typename> class Container
템플릿을 정의하는 것은 좋지만 벡터나 집합 같은 컨테이너를 템플릿 인자로 지정하는 것은 불가능하다. 벡터와 집합도 자신의 배당 정책을 지정하는 두 번째 템플릿 매개변수를 정의하기 때문이다.
템플릿 별칭은 using
선언을 사용하여 정의한다. 기존의 (부분적으로 또는 완전하게 특정화된) 템플릿 유형에 별칭을 지정한다. 다음 예제에서 Vector
는 vector
에 대한 별칭으로 정의된다.
template <typename Type> using Vector = std::vector<Type>; Vector<int> vi; // std::vector<int>와 동일 std::vector<int> vi2(vi); // 복사 생성: OK그래서 이렇게 하는 요점은 무엇인가? 벡터 컨테이너를 보면 매개변수가 한 개가 아니라 두 개가 정의되어 있다. 두 번째 매개변수는 배당 정책
_Alloc
으로서 기본값으로 std::allocator<_Tp>
가 설정된다.
template<typename _Tp, typename _Alloc = std::allocator<_Tp> > class vector: ...이제
Generic
클래스 템플릿에 템플릿 템플릿 매개변수를 정의해 보자:
template <typename Type, template <typename> class Container> class Generic: public Container<Type> { ... };대부분의 경우,
Generic
템플릿 클래스는 자신의 객체를 생성하는 데 실제로 사용된 컨테이너의 멤버를 제공한다. 거기에다 자신만의 멤버를 추가로 제공한다. 그렇지만 std::vector
와 같이 간단한 컨테이너는 사용할 수 없다. std::vector
는 template <typename> class Container>
매개변수에 부합하지 않기 때문이다. template <typename, typename> class Container>
템플릿 템플릿 매개변수를 요구한다.
그렇지만 Vector
템플릿의 별칭은 한 개의 유형 매개변수를 가진 템플릿으로 정의된다. 그리고 벡터의 기본 배당자를 사용한다. 결론적으로 Vector
를 Generic
으로 건네면 잘 작동한다.
Generic<int, Vector> giv; // OK Generic<int, std::vector> err; // 컴파일되지 않는다. 두 번째 인자가 일치하지 않기 때문이다.
템플릿 별칭의 작은 도움으로 Generic
에 map
과 같이 완전히 다른 종류의 컨테이너를 사용하는 것도 가능하다.
template <typename Type> using MapString = std::map<Type, std::string>; Generic<int, MapString> gim; // map<int, string> 사용
std
이름 공간 곳곳에서 유형속성 클래스(Trait)를 볼 수 있다. C++ 프로그래머라면 대부분 컴파일러가 `std::char_traits<char>
'를 언급하는 것을 보셨을 것이다. std::string s(1)
처럼 std::string
객체를 불법적으로 처리할 경우에 그렇다.
유형속성 클래스는 컴파일 시간에 유형을 결정한다. 유형속성 클래스를 사용하면 적절한 코드를 적절한 데이터 유형에 적용할 수 있다. 포인터나 참조 또는 평범한 값, 이 모든 것을 const
와 조합해 사용할 수 있다. 템플릿을 사용할 때 지정된 (또는 암시된) 실제 유형으로부터 사용할 특정한 유형의 데이터를 추론할 수 있다. 완전히 자동화할 수 있고 템플릿의 작성자가 결정을 내릴 필요가 없다.
유형속성 클래스로 template <typename Type1, typename Type2, ...>
를 개발할 수 있다. 값이나 (const) 포인터 또는 (const) 참조의 모든 조합을 포괄하는 많은 특정화를 지정할 필요가 없다. 그러면 얼마 못 가 유지관리가 불가능할 정도로 템플릿 특정화가 폭발적으로 증가하게 될 것이다 (예를 들어 템플릿 유형 매개변수를 사용할 때 각 템플릿 매개변수에 대하여 이 다섯 가지 유형을 허용하는 것만으로도 이미 25개의 조합을 만들어 낸다. 템플릿마다 잠재적으로 다른 특정화로 다루어야 하기 때문이다.).
유형속성 클래스를 사용할 수 있으면 실제 유형을 컴파일 시간에 추론할 수 있다. 컴파일러는 실제 유형이 포인터인지 멤버를 가리키는 포인터인지 아니면 상수 포인터인지 추론할 수 있으며 실제 유형이 lvalue 참조 또는 rvalue 참조 유형인 경우에도 비슷하게 추론할 수 있다. 이 덕분에 argument_type
, first_argument_type
, second_argument_type
그리고 result_type
와 같은 유형을 정의한 템플릿을 작성할 수 있다. 이 템플릿들은 (count_if()와 같은) 여러 총칭 알고리즘이 요구한다.
유형속성 클래스는 아무 행위도 하지 않는다. 즉, 생성자도 없고 호출할 멤버도 없다. 대신에 일련의 유형과 열거 값들을 정의한다. 이 값들은 이 유형속성 클래스 템플릿에 건네어진 실제 유형에 따라 다르게 구체화된다. 컴파일러는 특정화 집합 중 하나를 사용하여 실제 템플릿 유형 매개변수에 대하여 하나를 적절하게 고른다.
평범한 구조체부터 유형속성 템플릿의 정의를 시작한다. int
와 같은 평범한 값 유형의 특징을 정의한다. 이렇게 하면 구체적인 특정화를 위한 무대가 준비된다. 템플릿으로 지정할 수 있는 유형의 특징을 변경한다.
평범한 유형인지 포인터 유형인지 아니면 lvalue 참조 유형인지 rvalue 참조 유형인지 구체적으로 알려주는 BasicTraits
유형속성 클래스를 만들고 싶다고 가정하자 (이 모든 것들은 const
유형일 수도 아닐 수도 있다.).
실제로 제공된 유형이 무엇이든 `평범한' 유형 (즉, 수식자나 포인터 또는 참조가 없는 유형)과 `포인터 유형' 그리고 `참조 유형'을 결정할 수 있으면 좋겠다. 그러면 모든 경우에 내장 유형에 rvalue 참조를 정의해 넣을 수 있기 때문이다. 비록 그 유형에 대하여 상수 포인터를 건넬지라도 말이다.
우리의 출발점은 언급한 바와 같이 평범한 구조체이다. 필요한 매개변수를 모두 이 구조체에 정의한다. 아마도 다음과 같을 것이다.
template <typename T> struct Basic { typedef T Type; enum { isPointer = false, isConst = false, isRef = false, isRRef = false }; };
다양한 열거 값을 결합하여 몇 가지 결론을 내릴 수 있지만 (예를 들어 평범한 유형은 포인터나 참조 또는 rvalue 참조나 const가 아니지만) 좋은 관례는 유형속성 클래스를 완전하게 구현하는 것이다. 사용자에게 이 논리적 표현식을 직접 생성하도록 요구하면 좋지 않다. 그러므로 유형속성 클래스에서 기본적인 결정은 내포된 유형속성 클래스에서 내린다. 적절한 논리 표현식을 생성하는 것은 둘레 유형속성 클래스에게 맡긴다.
그래서 Basic
구조체는 내부 유형속성 클래스의 총칭 형태를 정의한다. 특정화는 특정한 세부사항들을 처리한다. 예를 들어 포인터 유형은 다음 특정화로 인지된다.
template <typename T> struct Basic<T *> { typedef T Type; enum { isPointer = true, isConst = false, isRef = false, isRRef = false }; };
반면에 상수 유형을 가리키는 포인터는 다음 특정화에 부합한다.
template <typename T> struct Basic<T const *> { typedef T Type; enum { isPointer = true, isConst = true, isRef = false, isRRef = false }; };
const
값 유형 또는 (rvalue) 참조 유형을 인지하도록 다른 여러 특정화도 정의해야 한다 결국 이 모든 특정화는 공개 유형속성 클래스 인터페이스를 제공하는 둘레 BasicTraits
클래스에 구조체를 내포하여 구현한다. 둘레 유형속성 클래스의 윤곽은 다음과 같다.
template <typename TypeParam> class BasicTraits { // 여기에 `Base' 템플릿의 특정화를 정의한다. public: BasicTraits(BasicTraits const &other) = delete; typedef typename Basic<TypeParam>::Type ValueType; typedef ValueType *PtrType; typedef ValueType &RefType; typedef ValueType &&RvalueRefType; enum { isPointerType = Basic<TypeParam>::isPointer, isReferenceType = Basic<TypeParam>::isRef, isRvalueReferenceType = Basic<TypeParam>::isRRef, isConst = Basic<TypeParam>::isConst, isPlainType = not (isPointerType or isReferenceType or isRvalueReferenceType or isConst) }; };유형속성 클래스의 공개 인터페이스는 명시적으로 복사 생성자를 제거한다. 생성자가 전혀 없기 때문에 그리고 정적 멤버도 전혀 없기 때문에 실행 시간 코드는 전혀 없다. 그러므로 유형속성 클래스의 모든 편의기능은 컴파일 시간에 사용된다.
유형속성 클래스 템플릿은 템플릿 유형 인자에 상관없이 적절한 유형을 얻을 수 있다. 템플릿 유형의 const
-성질에 따라 특정화를 적절하게 선택할 수도 있다. 예를 들어,
cout << "int: plain type? " << BasicTraits<int>::isPlainType << "\n" "int: ptr? " << BasicTraits<int>::isPointerType << "\n" "int: const? " << BasicTraits<int>::isConst << "\n" "int *: ptr? " << BasicTraits<int *>::isPointerType << "\n" "int const *: ptr? " << BasicTraits<int const *>::isPointerType << "\n" "int const: const? " << BasicTraits<int const>::isConst << "\n" "int: reference? " << BasicTraits<int>::isReferenceType << "\n" "int &: reference? " << BasicTraits<int &>::isReferenceType << "\n" "int const &: ref ? " << BasicTraits<int const &>::isReferenceType << "\n" "int const &: const ? " << BasicTraits<int const &>::isConst << "\n" "int &&: r-reference? " << BasicTraits<int &&>::isRvalueReferenceType << "\n" "int &&: const? " << BasicTraits<int &&>::isConst << "\n" "int const &&: r-ref ? "<< BasicTraits<int const &&>:: isRvalueReferenceType << "\n" "int const &&: const ? "<< BasicTraits<int const &&>::isConst << "\n" "\n"; BasicTraits<int *>::ValueType value = 12; BasicTraits<int const *>::RvalueRefType rvalue = int(10); BasicTraits<int const &&>::PtrType ptr = new int(14); cout << value << ' ' << rvalue << ' ' << *ptr << '\n';
TypeTrait
유형속성 클래스를 개발했다. 특수 버전의 내포된 struct Type
수식자와 포인터 그리고 참조와 값을 구분할 수 있었다.
유형이 클래스 유형인지 아니면 원시 유형인지 알 수 있으면 템플릿 개발자에게 유용한 정보가 될 수 있다. 클래스 템플릿 개발자는 템플릿 유형 매개변수가 클래스 유형이면 (아마도 멤버 함수를 사용하여) 특정화를 정의하고 싶을 수 있다. 그리고 비-클래스 유형에 대하여 또다른 특정화를 정의하고 싶을 것이다.
이 절은 어떻게 유형속성 클래스가 클래스 유형과 비-클래스 유형을 구분할 수 있는지에 관한 의문을 다룬다.
클래스를 비-클래스 유형과 구분하려면 컴파일 시간에 구별할 수 있어야 한다. 구분을 위한 특징을 찾으려면 약간 생각이 필요하지만 좋은 후보는 결국 멤버를 가리키는 포인터 구문에 있다. 멤버를 가리키는 포인터는 클래스에만 사용할 수 있다. 구분을 위한 특징으로서 멤버 구조를 가리키는 포인터를 사용하면 특정화를 개발할 수 있다. 사용할 수 없으면 또다른 특정화는 (즉, 총칭 템플릿은) 다른 일을 한다.
멤버를 가리키는 포인터를 `총칭적 상황'과 어떻게 구분할 수 있을까? 다행스럽게도 구별할 방법이 있다. 함수 템플릿 특정화는 멤버 함수를 가리키는 포인터인 매개변수를 가진 것으로 정의할 수 있다. 총칭 함수 템플릿은 어떤 인자도 받는다. 클래스 유형을 건네면 컴파일러는 앞의 (특정화된) 함수를 선택한다. 클래스 유형은 멤버를 가리키는 포인터를 지원할 수도 있기 때문이다. 여기에서 흥미로운 부분은 `할수도 있다'에 있다. 클래스는 멤버를 가리키는 포인터를 반드시 정의할 필요는 없다.
컴파일러는 실제로 아무 함수도 호출하지 않는다. 여기에서는 컴파일 시간에 관하여 언급하고 있는 중이다. 컴파일러가 하는 일은 상수 표현식을 평가함으로써 적절한 함수를 선택하는 것이 다이다.
그래서 우리가 의도한 함수 템플릿은 이제 다음과 같이 보인다.
template <typename ClassType> static `some returntype' fun(void (ClassType::*)());함수의 반환 유형은 잠시 후에 정의하겠다 (`
some returntype
'). 먼저 함수의 매개변수를 더 자세히 살펴보자. 함수의 매개변수는 void
를 돌려주는 멤버를 가리키는 포인터를 정의한다. 그런 함수는 그 함수가 사용될 때 지정되는 구체적인 클래스-유형에 대하여 존재할 필요가 없다. 사실, 구현도 전혀 없다. fun
함수는 유형속성 클래스에 정적 멤버로 선언만 될 뿐이다. 전혀 구현되어 있지 않으며 호출하기 위하여 유형속성 클래스 객체를 요구하지 않는다. 그럼 어디에 사용하는가?
질문에 대답하기 위하여 이제 템플릿의 인자가 클래스 유형이 아닐 경우에 사용해야 하는 총칭 함수 템플릿을 살펴 보자. `최악의 경우'를 대비해 매개변수 리스트에 생략기호를 지정한다. 생략기호는 모든 것이 실패할 때 컴파일러가 기댈 최후의 보루이다. 총칭 함수 템플릿은 평범한 생략기호를 매개변수 리스트에 지정한다.
template <typename NonClassType> static `some returntype' fun(...);총칭 대안 함수를
int
를 기대하는 함수로 정의하면 에러가 일어날 것이다. 컴파일러는 선택에 직면하면 복잡하고 총칭적인 함수보다 가장 간단하고 가장 많이 지정된 대안 함수를 선호한다. 그래서 fun
에 인자를 제공하면 컴파일러는 되도록이면 int
를 선택한다. fun(void (ClassType::*)())
를 선택하지 않는다. fun(void (ClassType::*)())
과 fun(...)
사이에 선택을 해야 한다면 컴파일러는 되도록이면 fun(void (ClassType::*)())
를 선택한다.
이제 다음과 같은 질문에 이른다. 멤버를 가리키는 포인터와 생략기호에 어떤 인자를 사용할 수 있을까? 실제로 `한 사이즈로 모든 크기에 맞는' 그런 인자가 존재한다. 0이 바로 그것이다. 생략기호 매개변수를 정의한 함수에 그리고 멤버를 가리키는 포인터 매개변수를 정의한 함수에 값 0을 인자로 건넬 수 있다.
그러나 0은 특별한 클래스를 지정하지 않는다. 그러므로 fun
은 템플릿 인자를 명시적으로 지정해야 한다. 우리의 코드에서는 fun<Type>(0)
처럼 나타나는데, 여기에서 Type
은 유형속성 클래스의 템플릿 유형 매개변수이다.
이제 반환 유형을 알아보자. 이 함수의 반환 유형은 간단한 값이 될 수 없다 (true
또는 false
). 우리의 궁극적인 의도는 유형속성 클래스의 템플릿 인자가 클래스 유형인지 아닌지 나타내는 열거체를 유형속성 클래스에게 주는 것이다. 그 열거체는 다음과 같이 된다.
enum { isClass = 클래스인지 아닌지 구별하는 표현식 } ;구분 표현식이 다음과 같이 될 수는 없다.
enum { isClass = fun<Type>(0) } ;
fun<Type>(0)
은 상수 표현식이 아니고
컴파일 시간에 결정할 수 있으려면 열거 값들은 상수 표현식으로 정의해야 하기 때문이다.
isClass
의 값을 결정하기 위해 fun<Type>(...)
과 fun<Type>(void (Type::*)())
사이를 컴파일 시간에 구별할 수 있는 표현식을 찾아야 한다.
이와 같은 상황에 선택할 도구는 sizeof
연산자이다. 컴파일 시간에 평가되기 때문이다. 다양한 크기의 반환 유형을 두 개의 fun
함수 선언에 정의함으로써 컴파일 시간에 두 fun
함수 중 어느 함수를 컴파일러가 선택하는지 구별할 수 있다.
char
유형은 정의상 크기가 1인 유형이다. 연속적으로 두 개의 char
값을 가진 또다른 유형을 정의함으로써 더 큰 유형을 얻는다. char[2]
는 물론 유형이 아니다. 그러나 char[2]
는 구조체의 데이터 멤버로 정의할 수 있다. 그리고 구조체는 유형을 정의한다. 그러면 그 구조체는 크기가 1을 초과한다. 예를 들어,
struct Char2 { char data[2]; };
Char2
는 유형속성 클래스에 내포된 유형으로 정의할 수 있다. 두 개의 fun
함수 템플릿 선언은 다음과 같이 된다.
template <typename ClassType> static Char2 fun(void (ClassType::*)()); template <typename NonClassType> static char fun(...);
sizeof
표현식은 컴파일 시간에 평가되므로 이제 isClass
의 값을 결정할 수 있다.
enum { isClass = sizeof(fun<Type>(0)) == sizeof(Char2) };이 표현식은 여러 흥미로운 사실들이 연루되어 있다.
fun
함수 템플릿은 절대로 구체화되지 않는다.
Type
을 보고 클래스 유형이면 fun
의 함수 템플릿 특정화를 선택하고 그렇지 않으면 총칭 함수 템플릿을 선택한다.
isClass
를 평가한다.
<type_traits>
헤더를 포함해야 한다.
type_traits
클래스가 제공하는 모든 편의기능은 std
이름공간에 정의되어 있다 (아래 예제에서는 생략됨). 그래서 프로그래머는 유형과 값의 다양한 특징을 결정할 수 있다.
유형속성을 기술하다 보면 다음 개념들을 만난다.
void
또는 객체를 돌려주는 함수;
void
, 객체, 함수, 또는 비-정적 클래스 멤버를 가리키는 포인터;
bool
은 물론이고 (유니코드) 문자를 나타내는 모든 내장 유형;
noexcept(true)
를 사용하지 않는 한, 이 유형속성 유형은 false
를 돌려준다. 예를 들어,
struct NoThrow { NoThrow &operator=(SomeType const &rhs) noexept(true); };
constexpr
생성자가 있는 클래스 유형이다. 정적 데이터 멤버가 있으면 안되고 바탕 클래스는 순수한 유형이어야 한다.
default
를 빼고) 클래스 인터페이스에 선언되지 않는다. 그리고 (기본 생성자와 기본 할당 연산자에 대하여) 바이트별 조치만 수행한다. 다음은 두 가지 예제이다. struct Pod
는 평범한 멤버만 있다. 명시적으로 어떤 멤버 함수도 선언하고 있지 않으며 그의 데이터 멤버는 평범한 구형 데이터이기 때문이다. struct Nonpod
는 평범한 구형 데이터가 아니다. 역시 멤버 함수를 명시적으로 선언하고 있지는 않지만 데이터 멤버는 std::string
이며 이것 자체가 평범한 구형 데이터가 아니다. std::string
은 생성자가 평범하지 않기 때문이다.
struct Pod { int x; }; struct Nonpod { std::string s; };
유형-조건(type-condition)을 유형에 적용할 때 그 유형은 완전한 유형이나 void
또는 크기를 모르는 배열이어야 한다.
다음의 유형속성(traits)이 제공된다.
add_const<typename Type>::type
const
를 Type
에 추가한다.
add_cv<typename Type>::type
const volatile
을 Type
에 추가한다.
add_lvalue_reference<typename Type>::type
Type
에 추가한다.
add_pointer<typename Type>::type
Type
에 추가한다.
add_rvalue_reference<typename Type>::type
Type
에 추가한다.
add_volatile<typename Type>::type
volatile
을 Type
에 추가한다.
conditional<bool cond, typename TrueType, typename FalseType>::type
cond
가 참이면 TrueType
을 사용하고 그렇지 않으면 FalseType
을 사용한다.
enable_if<bool cond, typename Type>::type
cond
가 참이면 Type
을 정의한다.
has_nothrow_assign<typename Type>::value
Type
이 예외를 던지지 않는 할당 연산자를 가지는지 질의한다.
has_nothrow_copy_constructor<typename Type>::value
Type
이 예외를 던지지 않는 복사 생성자를 가지는지 질의한다.
has_nothrow_default_constructor<typename Type>::value
Type
이 예외를 던지는 생성자를 가졌는지 질의한다.
has_nothrow_destructor<typename Type>::value
Type
이 예외를 던지지 않는 소멸자를 가지는지 질의한다.
has_trivial_assign<typename Type>::value
Type
이 간이 할당 연산자를 가지는지 질의한다.
has_trivial_copy_constructor<typename Type>::value
Type
이 간이 복사 생성자를 가지는지 질의한다.
has_trivial_default_constructor<typename Type>::value
Type
이 간이 기본 생성자를 가지는지 질의한다.
has_trivial_destructor<typename Type>::value
Type
이 간이 소멸자를 가지는지 질의한다.
is_abstract<typename Type>::value
Type
이 추상 유형인지 (예를 들어 추상 바탕 클래스인지) 질의한다.
(유형-조건 적용).
is_arithmetic<typename Type>::value
Type
이 산술 유형인지 질의한다.
is_array<typename Type>::value
Type
이 배열 유형인지 질의한다.
is_assignable<typename To, typename From>::value
From
유형의 객체를 To
유형의 객체에 할당할 수 있는지 질의한다 (유형-조건 적용).
is_base_of<typename Base, typename Derived>::value
Base
가 Derived
유형의 바탕 클래스인지 질의한다.
is_class<typename Type>::value
Type
이 클래스 유형인지 질의한다.
is_compound<typename Type>::value
Type
이 복합 유형인지 질의한다.
is_const<typename Type>::value
Type
이 상수 유형인지 질의한다.
is_constructible<typename Type, typename ...Args>::value
Args
매개변수 팩에 있는 인자로부터 Type
를 생성할 수 있는지 질의한다
(Args
에 있는 모든 유형에 유형-조건 적용).
is_convertible<typename From, typename To>::value
From
유형을 To
유형으로 static_cast
를 사용하여 변환할 수 있는지 질의한다.
is_copy_assignable<typename Type>::value
Type
이 복사 할당을 지원하는지 질의한다 (유형-조건 적용).
is_copy_constructible<typename Type>::value
Type
이 복사 생성을 지원하는지 질의한다 (유형-조건 적용).
is_default_constructible<typename Type>::value
Type
이 기본 생성자를 지원하는지 질의한다 (유형-조건 적용).
is_destructible<typename Type>::value
Type
이 비-소멸 소멸자를 가졌는지 질의한다 (유형-조건 적용).
is_empty<typename Type>::value
Type
이 (공용체 유형이 아니라) 클래스 유형인지 질의한다. 비어 있다면 그 클래스는 정적 데이터 멤버도 없고 가상 멤버도 없으며 (비어 있지 않은) 가상 바탕 클래스도 없다 (유형-조건 적용).
is_enum<typename Type>::value
Type
이 열거체 유형인지 질의한다.
is_floating_point<typename Type>::value
Type
이 부동 소수점 유형인지 질의한다.
is_function<typename Type>::value
Type
이 함수 유형인지 질의한다.
is_fundamental<typename Type>::value
Type
이 기본 유형인지 질의한다.
is_integral<typename Type>::value
Type
이 정수 유형인지 질의한다.
is_literal_type<typename Type>::value
Type
이 기호상수 유형인지 질의한다
(유형-조건 적용).
is_lvalue_reference<typename Type>::value
Type
이 lvalue 참조인지 질의한다.
is_member_function_pointer<typename Type>::value
Type
이 비-정적 멤버 함수를 가리키는 포인터인지 질의한다.
is_member_object_pointer<typename Type>::value
Type
이 비-정적 데이터 멤버를 가리키는 포인터인지 질의한다.
is_member_pointer<typename Type>::value
Type
이 멤버 함수를 가리키는 포인터인지 질의한다.
is_move_assignable<typename Type>::value
Type
이 이동 할당을 지원하는지 질의한다 (유형-조건 적용).
is_move_constructible<typename Type>::value
Type
이 이동 생성을 지원하는지 질의한다 (유형-조건 적용).
is_nothrow_assignable<typename To, typename From>::value
Type
이 예외를 던지지 않는 할당 연산자를 지원한다 (유형-조건 적용).
is_nothrow_constructible<typename Type, typename ...Args>::value
Type
객체를 생성할 수 있는지 질의한다 (유형-조건 적용).
is_nothrow_copy_assignable<typename Type>::value
Type
이 예외를 던지지 않는 복사-할당 연산자를 지원하는지 질의한다 (유형-조건 적용).
is_nothrow_copy_constructible<typename Type>::value
Type
이 예외를 던지지 않는 복사 생성을 지원하는지 질의한다 (유형-조건 적용).
is_nothrow_default_constructible<typename Type>::value
Type
이 예외를 던지지 않는 기본 생성자를 지원하는지 질의한다 (유형-조건 적용).
is_nothrow_destructible<typename Type>::value
Type
이 예외를 던지지 않는 소멸자를 지원하는지 질의한다 (유형-조건 적용).
is_nothrow_move_assignable<typename Type>::value
Type
이 예외를 던지지 않는 이동 할당을 지원하는지 질의한다 (유형-조건 적용).
is_nothrow_move_constructible<typename Type>::value
Type
이 예외를 던지지 않는 이동 생성자를 지원하는지 질의한다 (유형-조건 적용).
is_object<typename Type>::value
Type
이 (스칼라 유형과 대조적으로) 객체 유형인지 질의한다.
is_pod<typename Type>::value
Type
이 평범한 구형 데이터인지 질의한다 (유형-조건 적용).
is_pointer<typename Type>::value
Type
이 포인터 유형인지 질의한다.
is_polymorphic<typename Type>::value
Type
이 다형적 유형인지 질의한다.
(유형-조건 적용).
is_reference<typename Type>::value
Type
이 (lvalue 또는 rvalue) 참조인지 질의한다.
is_rvalue_reference<typename Type>::value
Type
이 rvalue 참조인지 질의한다.
is_same<typename First, typename Second>::value
First
유형과 Second
유형이 동일한지 질의한다.
is_scalar<typename Type>::value
Type
이 (객체 유형과 대조적으로) 스칼라 유형인지 질의한다.
is_signed<typename Type>::value
Type
이 부호 있는 유형인지 질의한다.
is_standard_layout<typename Type>::value
Type
이 표준 레이아웃을 제공하는지 질의한다 (유형-조건 적용).
is_trivial<typename Type>::value
Type
이 간이 유형인지 질의한다 (유형-조건 적용).
is_trivially_assignable<typename Dest, typename Src>::value
Src
유형의 객체나 값을 Dest
유형의 객체에 간단하게 할당할 수 있는지 질의한다 (유형-조건 적용).
is_trivially_constructible<typename Type, typename ...Args>::value
Args
매개변수 팩에 있는 인자들로부터 Type
을 간단하게 생성할 수 있는지 질의한다 (Args
안의 모든 유형에 유형-조건 적용).
is_trivially_copy_assignable<typename Type>::value
Type
이 간이 할당 연산자를 지원하는지 질의한다 (유형-조건 적용).
is_trivially_copy_constructible<typename Type>::value
Type
을 간단하게 복사 생성할 수 있는지 질의한다 (유형-조건 적용).
is_trivially_copyable<typename Type>::value
Type
을 간단하게 복사할 수 있는지 질의한다 (유형-조건 적용).
is_trivially_default_constructible<typename Type>::value
Type
이 간이 기본 생성자를 지원하는지 질의한다 (유형-조건 적용).
is_trivially_default_destructible<typename Type>::value
Type
이 간이 기본 소멸자를 지원하는지 질의한다 (유형-조건 적용).
is_trivially_move_assignable<typename Type>::value
Type
이 간이 할당 연산자를 지원하는지 질의한다 (유형-조건 적용).
is_trivially_move_constructible<typename Type>::value
Type
을 간단하게 이동 생성할 수 있는지 질의한다 (유형-조건 적용).
is_union<typename Type>::value
Type
이 공용체 유형인지 질의한다.
is_unsigned<typename Type>::value
Type
이 부호없는 유형인지 질의한다.
is_void<typename Type>::value
Type
이 void
인지 질의한다.
is_volatile<typename Type>::value
Type
이 volatile
로 수식된 유형인지 질의한다.
make_signed<typename Type>::type
make_unsigned<typename Type>::type
remove_all_extents<typename Type>::type
Type
이 ElementType
값이나 객체로 구성된 (다차원) 배열이라면 typedef type
은 ElementType
과 동등하다.
remove_const<typename Type>::type
Type
으로부터 const
를 제거한다.
remove_cv<typename Type>::type
Type
으로부터 const
그리고/또는 volatile
을 제거한다.
remove_extent<typename Type>::type
Type
이 ElementType
값이나 객체로 이루어진 배열이라면 typedef type
는 ElementType
과 동등하다. 다차원 배열이라면 ElementType
는 그 배열에서 첫 차원이 제거된 유형이다.
remove_pointer<typename Type>::type
Type
으로부터 포인터를 제거한다.
remove_reference<typename Type>::type
Type
으로부터 참조를 제거한다.
remove_volatile<typename Type>::type
Type
으로부터 volatile
을 제거한다.
std::error_code
클래스를 소개했다. 생성자 중 하나가 ErrorCodeEnum
를 인자로 받는다. 열거체를 손수 정의하여 ErrorCodeEnum
으로 `승격시킬 수 있다'. 그러면 error_code
클래스와 그 비슷한 클래스들이 사용할 수 있다.
(errno
값 같은) 표준 에러 코드는 또는 enum class Errc
으로 정의된 값들은 stat(2) 같은 낮은 수준의 시스템 함수가 사용한다. 여러분이 만든 함수나 클래스에서 만나는 에러에 사용하기에는 적합하지 않을 수 있다. 예를 들어 상호대화 계산기를 설계할 때 사용자가 입력하는 표현식에 관련하여 여러 에러를 만날 수 있다. 그렇다면 여러분 만의 ErrorCodeEnum
을 설계하고 싶을 것이다. 그러나 system_error
예외를 사용하는 클래스의 조직은 몹시 복잡하다. 개발자마다 손수 정의한 열거체와 클래스를 사용하면 상황은 더 악화된다. 에러 조건을 열거체에 나열하고 싶지만 프로그램이 라이브러리에 링크되어 있다면 열거체는 유지관리하기가 어렵다. 그 라이브러에 다른 개발자들이 따로 열거체를 정의하고 있기 때문이다. 새로운 에러 조건을 사용하고 싶겠지만 그에 맞게 구현을 갱신해야 하는 것은 별로 마음에 들지 않는다.
이 항과 다음 항에서 개발하는 접근법은 (여전히 복잡하기는 해도...) 더 유연한 코드를 결과로 내어줄 것이다. 먼저 두 개의 ErrorCodeEnum
을 개발한다. 이를 시발점으로 하여 관련 에러 값을 정의한다. error_category
클래스로부터 상속받아 부합하는 클래스를 정의함으로써 관련된 이 값들을 더욱 개발한다. 에러 조건을 사용하여 에러 값을 에러의 총칭 원인에 연관짓는다 (14.9절). 에러 조건은 이미 (다른 개발자들에 의하여) 정의되어 있을 수 있으므로 어떻게 여러 에러 조건이 하나의 프로그램에 유연하게 조합되어 들어갈 수 있는지 알아내는 것은 그 자체로 재미있는 퍼즐이다. 이 퍼즐은 ErrorCondition
클래스를 정의하면 해결된다. 이 클래스는 모든 에러 조건을 관리할 수 있다.
아래에 사용된 예제는 비행 시연에 초점을 둔다. 비행 시연을 할 때 여러 에러를 만난다 (예, 내비게이션 신호가 범위를 벗어난다). 조종석 안 시스템에 계산기가 있다. 여기에서 너무 구체적인 에러가 나타날 수도 있다. 존재하지 않는 함수를 요구할 가능성이 있다. 또는 복잡한 표현식의 괄호가 짝이 맞지 않을 수도 있다. 앞의 에러는 특정한 계산기에 관련된 에러이지만 뒤의 에러는 사용자의 엉터리 입력에 관련된 에러이다. 그리하여 크게 세 가지 에러 범주로 구분된다. 시연기 에러와 계산기 에러 그리고 사용자 에러로 구분된다.
enum class CalculatorError { // 0은 없는데, 관례적으로 0은 에러가 없음을 의미하기 때문이다. // 값들이 연속적으로 있을 필요는 없다. NoLvalue = 1, // 할당의 lhs는 lvalue이다. TypeError, // 올바르지 못한 표현식 유형이다. RangeError, // 예, sqrt(-1) ArityError, // 함수에 인자가 너무 적거나 너무 많다. UnknownFunction, // 정의되지 않은 함수 MissingParentheses, // ( 그리고 ) 짝이 맞지 않는다. };
enum class SimulatorError { // 0은 없다. // 값들이 연속적으로 있을 필요는 없다. EngineFailure = 1, // 엔진 고장 ComFailure, // 무선 통신 실패 RangeError, // 내비게이션 시스템이 범위를 벗어남 UnknownFunction, // 자동 비행 기능이 작동하지 않음 // ... // 기타 등등. };
std::error_code
클래스는 에러 값과 범주라는 두 가지 정보를 사용할 수 있도록 설계되어 있다. (int
) 에러 코드는 value()
멤버를 통하여 얻는다. 에러 범주는 category()
멤버를 통하여 얻는다.
error_code(ErrorCodeEnum)
생성자를 사용하여 error_code
가 CalculatorError
와 SimulatorError
값을 받도록 하는 것이다. 이렇게 하면 독자적인 에러 코드 열거체를 사용하여 에러 값과 범주를 열람할 수 있기 때문이다.
그러려면 우리의 에러 열거체로부터 값을 제공받을 때 is_error_code_enum
유형속성 클래스의 value
정적 멤버는 true
를 돌려주어야 한다. 흥미롭게도 std::is_error_code_enum
의 특정화를 정의하려면 코드를 std
이름공간에 추가해야 한다. 보통은 허용되지 않지만 이 경우만큼은 허용된다. C++ 표준에 의하면:
20.5.4.2.1 std 이름공간다음은선언이나 정의를 std 이름공간에 추가하거나 std 이름공간 안의 이름공간에 추가할 때 C++ 프로그램의 행위는 따로 특기하지 않는 한, 정의되어 있지 않다.
프로그램은 표준 라이브러리 템플릿을 위해 std 이름공간에 템플릿 특정화를 추가할 수 있다. 선언이 사용자 정의 유형에 달려 있고 특정화가 원래의 템플릿용 표준 라이브러리 요구 조건을 만족하고 그리고 명시적으로 금지하지 않는 경우에만 그렇다.
CalculatorError
를 위한 특정화이다. SimulatorError
를 위한 특정화도 비슷하게 정의된다.
namespace std { template <> struct is_error_code_enum<CalculatorError>: public true_type {}; }
이 쯤에서 독자적인 에러 열거체 정의를 완료한다. 이 열거체는 이제 ErrorCodeEnum
으로 `승격된다'.
error_code
객체의 생성자중 하나가 에러 값(int
)과 더불어 error_category
참조를 기대한다. 멤버 템플릿 생성자는 그저 ErrorCodeEnum
값을 요구할 뿐이지만, 사실 error_category
객체도 필요하다. 그 생성자는 ErrorCodeEnum
값을 int
로 형변환하고 적절한 error_category
를 지정함으로써 error_code
를 생성한다. 그러므로 ErrorCodeEnum
에 부합하는 클래스는 error_category
클래스로부터 파생된다. 클래스를 파생시키는 법은 다음 항에 다룬다.
ErrorCodeEnum
은 error_category
클래스로부터 상속받아 두 개의 클래스를 더 개발하기 위한 토대이다 (14.9절).
error_category
으로부터 상속받은 클래스는 싱글턴 객체로 설계되어야 하고 자신만의 name
멤버와 message
멤버 그리고 equivalent
멤버 함수만 단순하게 구현해야 한다.
ErrorCodeEnum
에 정의된 에러에 두 개의 (NTBS) 텍스트가 연관되어 있다. 에러를 자세히 기술하는 텍스트와 열거 값이 속한 에러 조건의 이름이 그것이다. 예를 들어 CalculatorError:::MissingParentheses
이라면 "parentheses don't match"
(괄호 짝이 맞지 않음)이라는 기술이 있고 에러 조건 이름은 "InputCond"
가 될 것이다. 그렇게 연관된 집합을 CatMap
클래스 템플릿에 모았고, std::unordered_map
으로부터 파생된다. CatMap
의 설계는 눈에 보이는 그대로 std::initializer_list
를 받는 생성자를 제공한다.
template <class Enum> class CatMap: public std::unordered_map< Enum, std::tuple<char const *, char const *> > // 기술 조건 이름 { typedef std::tuple<char const *, char const *> Tuple; typedef std::unordered_map<Enum, Tuple> Map; public: CatMap(std::initializer_list<typename Map::value_type> const &list); }; template <class Enum> CatMap<Enum>::CatMap( std::initializer_list<typename Map::value_type> const &list) : Map{ list } {}
ErrorCodeEnum
값은 무작위로 할당되기 때문에 (0은 사용하지 않음) CapMap
객체로 에러의 설명과 조건 이름에 빠르게 접근할 수 있다. CatMap
객체가 에러 조건을 이름으로 제공하는 것을 눈여겨보라. 범주 클래스가 설계되기 전까지는 에러 조건만 이름으로 알 수 있었다. 그래서 이름으로 참조할 수 있다. 실제로 에러 조건을 정의하는 법은 다음 항에 다룬다.
error_category
로부터 클래스를 파생시킬 때 message(int ev)
멤버를 반드시 정의해야 한다. error_category
의 메시지를 재정의하고 에러 열거 값 ev
에 부합하는 기술을 돌려주어야 한다. 열거 값과 기술 사이의 연관 관계는 CatMap
객체에 정의된다. message
멤버의 구현은 그저 CatMap
객체를 사용하면 된다.
그러므로 메시지 처리 자체는 CategoryBase
클래스 템플릿에다 요소화할 수 있다. CategoryBase
객체는 ErrorCodeEnum
열거체 유형으로 실체화되고 안에 static CatMap<Enum> const s_errors
객체와 더불어 message(int ev, char const *noEnumValue) const
멤버를 담고 있다. ev
에 부합하는 기술을 돌려주거나 정의된 열거 값을 ev
가 나타내지 않으면 noEnumValue
에러 메시지를 돌려준다. 다음은 그의 구현이다.
template <class Enum> std::string CategoryBase<Enum>::message(int ev, char const *noEnumValue) const { auto iter = s_errors.find(static_cast<Enum>(ev)); return iter == s_errors.end() ? noEnumValue : std::get<0>(iter->second); }
그러나 이것이 CategoryBase
클래스가 할 수 있는 모든 것은 아니다. 안에 s_errors
객체가 있으므로 에러 열거 값과 범주 그리고 조건 사이의 연관 관계도 얻을 수 있다. 에러 범주 클래스는 bool equivalent(std::error_code const &ec, int condNr)
멤버도 정의한다. 이 멤버들은 똑같은 이름의 ErrorCondition
멤버로부터 호출되어 에러 코드가 실제로 에러 조건 번호 condNr
에 연관되어 있는지 점검한다. CatMap s_errors
를 사용할 수 있고 숫자가 주어지면 조건의 이름을 열람할 편의기능이 ErrorCondition
클래스에 있으므로 에러 범주의 equivalent
멤버 구현은 동일하다. 단, 실제로 사용된 에러 코드 열거체만 제외하고 말이다. 그러나 그 열거체는 템플릿 인자로 CategoryBase
에 건네지므로 필수 테스트를 수행할 equivalent
멤버 함수를 정의할 수 있다. 구현은 단순하다. 조건 번호가 주어지면 s_errors
객체에서 error_code
에 저장된 에러 열거체의 값을 찾아내어, 저장된 그 에러 조건의 이름을 (싱글턴) error_condition
객체가 제공하는 이름과 비교한다. 다음은 그의 구현이다.
template <class Enum> bool CategoryBase<Enum>::equivalent(std::error_code const &ec, int condNr) const noexcept { auto iter = s_errors.find(static_cast<Enum>(ec.value())); return iter != s_errors.end() and std::get<1>(iter->second) == ErrorCondition::instance()[condNr]; }
게다가, std::error_category
클래스로부터 파생시킬 때 단일 상속을 허용하기 위해 CategoryBase
클래스 자체를 std::error_category
클래스로부터 상속받는다.
그래서 CategoryBase
클래스로부터 상속받음으로써 CatMap
을 얻는다. 에러의 기술을 돌려주는 함수 하나를 얻는다. std::error_category
으로부터 파생된 모든 클래스가 사용할 수 있는 equivalent
구현을 얻는다. 그리고 마지막으로 자체가 error_category
클래스이기도 한 클래스를 얻는다. 다음은 CategoryBase
의 인터페이스이다.
template <class Enum> class CategoryBase: public std::error_category { public: bool equivalent(std::error_code const &ec, int condNr) const noexcept override; protected: static CatMap<Enum> const s_errors; std::string message(int ev, char const *noEnumValue) const; };
독자적으로 에러 범주 클래스를 정의할 준비가 되었다. 범주 클래스를 정의하기 위하여 다음 단계를 밟는다.
CategoryBase
를 사용할 수 있으므로 쉽게 개발할 수 있다. CategoryBase
클래스로부터 상속받는다. 싱글턴이므로 (기본) 생성자는 비밀이다. instance
정적 멤버는 클래스의 유일한 실체를 참조로 돌려준다. 그리고 name
멤버와 message
멤버를 선언한다 (equivalent
멤버는 CategoryBase
에 있다). 다음은 CalculatorCategory
클래스의 인터페이스이다.
class CalculatorCategory: public CategoryBase<CalculatorError> { static CalculatorCategory *s_instance; public: static CalculatorCategory &instance(); char const *name() const noexcept override; std::string message(int ce) const override; private: CalculatorCategory() = default; };
instance
멤버는 싱글턴 객체를 참조로 돌려주고, 맨 처음 호출될 때 그것을 초기화한다.
CalculatorCategory &CalculatorCategory::instance() { if (s_instance == 0) s_instance = new CalculatorCategory; return *s_instance; }
name
멤버는 범주의 이름을 그냥 짧은 문자열로 돌려준다 (계산기 범주라면 "calculator"
).
message
구현도 아주 단순하다. CategoryBase::member
의 덕분인데, 뒤의 멤버가 돌려주는 것을 그냥 돌려줄 뿐이다.
std::string CalculatorCategory::message(int ev) const { return CategoryBase<CalculatorError>::message( ev, "CalculatorError not recognized"); }
CatMap
을 순서 없는 맵으로 초기화할 때 에러 코드 열거 값과 그의 기술 그리고 에러 조건 이름 사이의 관계가 정의된다. 그 맵은 에러 범주의 바탕 클래스의 데이터 멤버이므로 그 범주 클래스를 정의할 때 초기화할 수 있다. 다음은 CalculatorCategory
를 초기화하는 방법이다.
template <> CatMap<CalculatorError> const CategoryBase<CalculatorError>::s_errors = { { CalculatorError::NoLvalue, {"lhs of assignment is not a variable", "UnavailCond" } }, { CalculatorError::TypeError, {"type of expression incorrect", "UnavailCond" } }, { CalculatorError::RangeError, {"argument value not allowed", "UnavailCond" } }, { CalculatorError::ArityError, {"incorrect number of arguments", "UnavailCond" } }, { CalculatorError::UnknownFunction, {"function not defined", "UnavailCond" } }, { CalculatorError::MissingParentheses, {"parentheses don't match", "InputCond" } }, };
CalculatorError
열거 값으로부터 실제로 error_code
객체를 만들 수 있는 위치에 오게 되었다. 이를 위하여 make_error_code(CalculatorError ce)
자유 함수를 정의한다:
std::error_code make_error_code(CalculatorError ce) { return { static_cast<int>(ce), CalculatorCategory::instance() }; }
CalculatorError
값을 std::error_code
로 변환할 수 있으므로 프로그램에 이용할 수 있다. 다음은 작은 데모 프로그램으로서 그 사용법을 보여준다.
#include <iostream> #include <system_error> #include "../calculatorerror/calculatorerror.h" #include "../calculatorcategory/calculatorcategory.h" int main() try { std::error_code ec = CalculatorError::ArityError; std::cout << ec << ' ' << ec.message() << '\n'; throw std::system_error{ ec, "For demonstration purposes" }; } catch (std::system_error &se) { std::cout << se.what() << ":\n" " " << se.code() << '\n'; } /* 출력: calculator:4 incorrect number of arguments For demonstration purposes: incorrect number of arguments: calculator:4 */
다음 항에 에러 조건을 정의하고 사용하는 방법을 더 자세하게 다룬다.
ErrorCodeEnum
열거체를 사용한다. 하나는 시연기 자체에 관련된 에러를 다루고 또 하나는 온-보드 계산기에 관련된 에러를 다룬다. 라이브러리가 제공하는 편의기능을 사용하고 있다면 여러 열거 값을 우리의 시연기에 더 사용할 수도 있을 것이다. 예를 들어 ApproachChartError
열거체를 정의한 공항 접근 데이터 베이스가 있을 수 있다. 그리고 시연기의 GPS에 따로 GPSError
열거체가 따라 올 수도 있다.
에러는 범주화할 수 있다. 시연기에서 사용자 입력 에러나 무리한 요청 또는 시스템 실패 등등을 인지할 수도 있다.
에러 조건(error_condition
)으로 ErrorCodeEnum
에러를 범주화할 수 있다. 특정한 열거 값을 테스트하기 위해 if
-조건문을 사용할 필요가 없다. 에러 조건은 에러 범주처럼 구현할 수 있다. 열거체를 하나 정의하고 그 다음에 std::error_condition
으로 `업그레이드' 한다.
개발자마다 열거체를 사용하면 열거체를 적용하고 조합하며 그리고 확장하기가 어렵다는 단점이 있다. 한 라이브러리에 InputError
조건이 정의되어 있지만 다른 라이브러리에도 역시 그렇게 정의되어 있을 수 있다. 열거체는 달라도 되지만 어떻게 그런 열거체가 동일한 값을 사용하지 못하도록 방지할까?
그런 문제를 방지하려면 고정된 열거 값을 피하면 된다. 대신에 에러 조건을 나타내는 라벨을 사용한다. 그래서 다음과 같이 지정하지 않고
enum class ErrorCondition { InputCond, UnavailCond, SystemCond };텍스트 라벨로
"InputCond", "UnavailCond",
이나 "SystemCond"
처럼 사용할 수 있다. 고정 크기의 ErrorConditionEnum
을 사용하는 대신에 싱글턴 ErrorCondition
객체가 사용된다. 이 객체는 그런 열거체를 감싸 제공하고 동시에 텍스트 라벨로 식별되는 에러 조건의 유일성을 보장한다.
ConditionCategory
클래스는 std::error_condition
클래스가 부과하는 요구 조건을 구현한다. 그리하여 ErrorCondition
클래스가 관리하는 에러 조건으로 맞춤 재단된다. 싱글턴으로 설계된 ConditionCategory
에 담긴 벡터의 원소마다 다양한 에러 조건의 기술과 그 이름을 보유한다. 눈에 익은 name
멤버와 message
멤버 그리고 equivalent
멤버 말고도 addCondition
멤버를 제공한다. 이 멤버는 ErrorCondition
클래스가 새로운 에러 조건을 현재 집합에 추가하기 위해 사용한다. 다음은 그의 인터페이스이다.
class ConditionCategory: public std::error_category { static ConditionCategory *s_instance; typedef std::tuple< std::string, // 0: 조건 이름 char const * // 1: 설명 > Info; std::vector<Info> d_conditionInfo; public: static ConditionCategory &instance(); char const *name() const noexcept override; std::string message(int ev) const override; bool equivalent(std::error_code const &code, int condition) const noexcept override; // 조건의 idx-번째 이름을 돌려준다. std::string const &operator[](size_t idx) const; void addCondition(char const *name, char const *description); size_t size() const; private: ConditionCategory(); };
name
멤버와 message
멤버는 간이 구현이다. equivalent
멤버는 error_code
객체와 에러 조건 열거 값을 (int
로) 받는다. 받은 에러 조건 열거 값에 그의 error_code
가 연관되어 있다면 이 함수는 true
를 돌려주어야 한다. equivalent
멤버 자체는 그런 점검을 수행하지 않는다. 그 보다는 error_code
의 error_category
객체를 받아 그의 equivalent
멤버로 테스트를 수행한다.
bool ConditionCategory::equivalent(std::error_code const &ec, int condNr ) const noexcept { return ec.category().equivalent(ec, condNr); }
다음으로 ErrorCondition
클래스를 살펴보자. 이 클래스에 ConditionCategory
객체와 unsorted_map
이 담겨 있다. 이 맵은 조건 이름을 ConditionCategory
벡터의 인덱스에 짝짓는다. 그 말고는 평범한 클래스이다. 다양한 에러 조건의 열거 값을 받을 멤버를 제공하고 숫자가 주어지면 그에 맞는 조건의 이름을 돌려준다. 에러 조건 열거체 값은 ConditionCategory
의 벡터 인덱스로 단순하게 정의된다. 그 다음 ErrorCondition::Enum
열거체로 정적으로 형변환된다.
열거체 자체는 단순히 열거 이름으로 정의되고, std::is_error_condition_enum
유형속성 클래스 특정화에 의하여 ErrorConditionEnum
으로 승격된다 (is_error_code_enum
유형속성 클래스가 CalculatorError
같은 우리의 열거체에 수행하던 것과 비슷하다). 다음은 is_error_condition_enum
유형속성 클래스 특정화이다. 다음에 ErrorCondition
클래스 인터페이스가 따른다.
namespace std { template <> struct is_error_condition_enum<ErrorCondition::Enum>: public true_type {}; }
class ErrorCondition { static ErrorCondition *s_instance; ConditionCategory &d_ec; typedef std::unordered_map<std::string, size_t> ConditionMap; ConditionMap d_condition; public: enum Enum // 에러 조건의 값을 돌려주는 열거체 {}; static ErrorCondition &instance(); // error_condition에 사용되는 이름 void addCondition(char const *name, char const *description); Enum operator()(char const *condName) const; // 조건 이름이 주어질 때 // 열거체 std::string const &operator[](size_t nr) const; // nr이 주어질 때 // 조건 이름 private: ErrorCondition(); // 싱글턴이다. instance.cc 참고 };
make_error_condition
함수에 의하여 에러 조건 객체 자체가 반환된다. 인자로 ErrorConditionEnum
값을 기대한다. 다양한 에러 조건 열거 값에 관련된 std::error_category
를 내부적으로 사용한다. CalculatorCategory
가 CalculatorError
열거체에 연관되는 것과 무척 비슷하다. ConditionCategory
는 싱글턴이기 때문에 직접적으로 그 싱글턴 객체를 std::error_condition
객체에 건넬 수 있다.
std::error_condition make_error_condition(ErrorCondition::Enum ec) { return { static_cast<int>(ec), ConditionCategory::instance() }; }
error_code, error_category, error_condition
그리고 관련 클래스가 제공하는 편의기능들을 보여주는 데모 프로그램으로 이 항을 마친다. 완전한 구현은 C++ 주해서의 소스 저장소 yo/advancedtemplates/examples/errocde
디렉토리에 있다.
int main() try { ErrorCondition &errorCond = ErrorCondition::instance(); std::cerr << CalculatorCategory::instance().name() << '\n' << SimulatorCategory::instance().name() << '\n'; errorCond.addCondition("InputCond", "error in user request"); errorCond.addCondition("UnavailCond", "function not available"); errorCond.addCondition("SystemCond", "system failure"); // ec는 에러 열거체에 속해 있는 실제 에러 코드이다. // assert 서술문은 지정된 에러 코드가 // 지정된 에러 조건에 속하는지 점검한다. // // 역시 OK: ErrorCondition::Enum{}; // std::error_condition cond = errorCond("InputCond"); std::error_code ec = CalculatorError::TypeError; std::cerr << "Enum value of UnavailCond = " << errorCond("UnavailCond") << '\n'; assert(ec != ErrorCondition::Enum{}); assert(ec == errorCond("UnavailCond")); assert(ec != errorCond("SystemCond")); ec = CalculatorError::MissingParentheses; assert(ec == errorCond("InputCond")); ec = CalculatorError::ArityError; std::cout << ec << ' ' << ec.message() << '\n'; throw std::system_error{ ec, "For demonstration purposes" }; } catch (std::system_error const &se) { std::cout << "System Error: " << se.what() << ":\n" " " << se.code() << '\n'; } /* 출력: calculator simulator Enum value of UnavailCond = 2 calculator:4 incorrect number of arguments System Error: For demonstration purposes: incorrect number of arguments: calculator:4 */
강력 보장을 얻으려고 시도하는 동안 예외를 던지면 함수의 대응은 두 갈래가 된다.
첫 단계의 조치는 (예를 들어 소스의 값을 (임시의) 목적지에 할당하기 위하여) std::move
를 사용하여 이동을 인지하도록 만들 수도 있다. 그렇지만 std::move
를 사용하면 (예를 들어 소스의 메모리를 확장하고 기존의 데이터를 새 위치로 이동하면) 소스에 너무 쉽게 영향을 미칠 수 있다. 이렇게 되면 첫 단계의 가정이 깨진다. 목표 객체가 이미 변경되었기 때문이다.
이 경우 (일반적으로) 이동 연산이 예외를 던지도록 허용하면 안된다. 즉, 이동 생성자가 통제할 수 없는 (외부) 데이터 유형을 사용한다면 예외를 던지지 않는 이동 생성자를 코딩하기가 몹시 어렵다는 뜻이다. 예를 들어,
template <typename Type> class MyClass { Type d_f; public: MyClass() = default; MyClass(MyClass &&tmp) : d_f(move(tmp.d_f)) {} };여기에서
MyClass
의 저자는 Type
의 설계를 통제하지 못한다. Foreign
이 단순히 (예외를 던질 가능성이 있는) 복사 생성자만 있다면 다음 코드는 이동 생성자 아래에서는 예외를 던지지 않는다는 가정에 어긋난다.
MyClass<Foreign> s2(move(MyClass<Foreign>()));
템플릿이 Type
이 예외를 던지지 않는 이동 생성자를 가졌는지 여부를 탐지할 수 있다면 비싼 복사 생성자를 사용하지 않고 이동 생성자를 호출하여 (이미 강력보장을 제공하는 앞 부분의 코드에서 목표를 변경해 버린) 구현을 최적화할 수 있다.
템플릿이 그런 최적화를 수행할 수 있도록 noexcept
키워드를 도입했다. throw
리스트에서 noexcept
를 실행 시간에 점검하지만 noexept
선언을 범한 결과는 throw
리스트를 범한 것보다 더 심각하다. noexcept
를 범하면 std::terminate
를 호출하게 되고 프로그램을 끝내버리며 스택을 원상태로 복구하지 않은 채로 끝날 가능성이 높다. 이전 예제의 문맥에서 컴파일러는 다음 코드를 흠없이 받아들인다. 컴파일 시간에 noexcept
를 점검하지 않는다는 것을 보여준다.
class Foreign { public: Foreign() = default; Foreign(Foreign const &other) noexcept { throw 1; } };그렇지만, 이 클래스의 복사 생성자를 호출하면 다음 메시지와 함께 실행이 멈춘다.
terminate called after throwing an instance of 'int' Abort 'int' 유형의 실체를 던진 후에 terminate 함수가 호출됨. 프로그램이 중단됨
noexcept
의 목적은 템플릿에게 코드를 최적화하도록 허용하는 것이다. 절대 예외 보장을 제공하면 이동 연산을 사용할 수 있다. noexcept
도 조건적인 noexcept(condition)
구문을 제공할 수 있기 때문에 (noexcept(true)
와 noexcept
는 의미구조가 동일하다), 템플릿 유형의 `예외불허' 본성에 대하여 noexcept
를 조건적으로 만들 수 있다. 이것은 throw
리스트로는 불가능하다.
다음 규칙을 사용하여 코드에 noexcept
를 사용할지 말지 결정한다.
noexcept
를 사용하지 않는 것이다 (이것은 throw
리스트에 대하여 제시한 조언과 동일하다);
noexcept(true)
를 제공하기 때문에 가능하면 이동 연산을 사용하여 템플릿 최적화를 허용한다는 것을 컴파일러가 추론할 수 있다. 그렇다면 생성자와 복사 할당 연산자 그리고 이동 할당 연산자와 소멸자의 기본 구현에 noexcept(true)
를 제공한다.
noexcept
가 선언된 함수라도 여전히 예외를 던질 수 있다 (위 예제 참고). 결국 noexcept
의 의미는 단순하다. noexcept
가 지정된 함수가 예외를 던지면 std::unexpected
가 아니라 std::terminate
가 호출된다는 뜻에 불과하다.
throw()
) 함수에 이제는 noexcept(true)
를 건네야 한다.
noexcept
지정이 필요하다 (<type_traits>
헤더 파일에 선언되어 있다).
is_nothrow_constructible
is_nothrow_default_constructible
is_nothrow_move_constructible
is_nothrow_copy_constructible
is_nothrow_assignable
is_nothrow_move_assignable
is_nothrow_copy_assignable
is_nothrow_destructible
value
상수 멤버를 제공한다. 클래스가 (그리고 그의 인자 유형 리스트도) 그 유형속성의 이름에 따른 특징에 부합하면 이 값은 true
이다. 예를 들어 MyClass(string const &) noexcept
가 생성자라면 다음은 true
와 같다.
std::is_nothrow_constructible<MyClass, string>::value(
is_nothrow_move_constructible
같이) 이름 붙은 멤버인 매개변수 유형에 대해서는 지정할 필요가 없다. 묵시적으로 지정되기 때문이다. 예를 들어,
std::is_nothrow_move_constructible<MyClass>::value이동 생성자에
noexcept
식별자가 있으면 true
를 돌려준다.
transform
총칭 알고리즘에 사용할 수 있다 (19.1.63항):
template <typename Return, typename Argument> Return chop(Argument const &arg) { return Return(arg); }
Return
에 std::string
이 반환될 경우에 위의 구현을 사용하면 안된다고 가정하자. 대신에 std::string
이라면 두 번째 인자로 언제나 1
을 건네야 한다. Argument
가 C++ 문자열이라면 arg
의 사본을 앞에서 첫 문자를 잘라내고 돌려줄 수 있다.
chop
은 함수이기 때문에 다음과 같이 부분적으로 특정화를 정의하는 것은 불가능하다.
template <typename Argument> // 컴파일되지 않는다! std::string chop<std::string, Argument>(Argument const &arg) { return string(arg, 1); }함수 템플릿은 부분적으로 특정화할 수는 없지만 중복정의하는 것은 가능하다. 두 번째로 더미
string
매개변수를 정의할 수 있다.
template <typename Argument> std::string chop(Argument const &arg, std::string) { return string(arg, 1); }이제 두 경우를 구분할 수 있다. 그러나 좀 더 복잡하게 함수를 호출해야 하는 희생은 감수해야 한다. 코드에서 이 함수는 두 번째 더미 인자를 제공하기 위하여
bind2nd
바인더의 사용을 요구할 수 있다 (18.1.4항). 또는 컴파일러에게 중복정의 함수 템플릿 두 가지 중에 하나를 고를 수 있도록 하기 위해 (생성하기에는 좀 비싼) 더미 인자를 요구할 수도 있다.
string
더미 인자를 제공하는 대신에 함수는 IntType
템플릿을 사용하여 적절한 중복정의 버전을 선택할 수도 있다 (23.2.1.1목). 예를 들어 IntType<0>
를 첫 번째 중복정의 chop
함수의 두 번째 인자의 유형으로 정의할 수 있다. 그리고 IntType<1>
를 두 번째 중복정의 함수에 사용할 수 있다. 프로그램의 효율성이라는 관점에서 보면 이것은 매력적인 선택이다. IntType
객체는 지극히 가볍기 때문이다. IntType
객체는 데이터가 전혀 없다. 그러나 확실하게 단점도 있다. 사용된 int
값과 의도한 유형이 직관적으로 명료하게 연관되지 않는다.
임의의 IntType
유형을 정의하는 대신에 또다른 가벼운 해결책을 사용하는 것이 더 바람직하다. 자동으로 유형과 유형을 짝지어 보자. struct TypeType
은 가벼운 유형 포장자로서 IntType
를 많이 닮았다. 다음은 그 정의이다.
template <typename T> struct TypeType { typedef T Type; };
TypeType
도 가벼운 유형이다. 데이터 필드가 전혀 없기 때문이다. TypeType
구조체를 사용하여 자연스럽게 chop
의 두 번째 인자에 유형을 연관지을 수 있다. 예를 들어 중복정의 함수는 이제 다음과 같이 정의할 수 있다.
template <typename Return, typename Argument> Return chop(Argument const &arg, TypeType<Argument> ) { return Return(arg); } template <typename Argument> std::string chop(Argument const &arg, TypeType<std::string> ) { return std::string(arg, 1); }위의 구현을 사용하면 어떤 유형이든
Result
에 지정할 수 있다. 어쩌다가 std::string
이 되더라도 적절한 중복정의 버전이 자동으로 선택된다. 다음으로 추가된 chop
함수의 중복정의 버전은 이를 이용한다.
template <typename Result> Result chop(char const *txt) // char const *는 두 번째 { // 템플릿 유형 매개변수가 될 수도 있다. return chop(std::string(txt), TypeType<Result>()); }세 번째
chop
함수를 사용하면 다음 서술문은 텍스트 `ello world
'를 출력한다.
cout << chop<string>("hello world") << '\n';템플릿 함수는 부분적 특정화를 지원하지 않는다. 그러나 중복정의는 가능하다. 매개변수에 따라 다르게 더미 유형의 인자를 가지고 중복정의를 제공하고 그리고 그 더미 유형 인자를 요구하지 않는 중복정의 함수로부터 이 중복정의를 호출함으로써 클래스 템플릿의 부분적 특정화와 같은 상황을 실현할 수 있다.
struct NullType {};
T
를 또다른 유형 U
를 `대신하여' 사용할 수 있을까? C++는 강력하게 유형이 정의되는 언어이므로 그 해답은 놀랍게도 단순하다. U
를 요구하더라도 T
를 인자로 받으면 된다. 그러면 U
대신에 T
를 사용할 수 있다.
이 추론은 다음 클래스 뒤에 숨어 있다. 이 클래스는 유형 U
가 기대되는 곳에 T
를 사용할 수 있는지 결정할 수 있다. 흥미로운 부분은 실제로 전혀 코드가 생성되거나 실행되지 않는다는 것이다. 모든 결정은 컴파일러가 내린다.
이 절의 후반부에서는 전반부에서 개발된 코드를 사용하여 B
클래스가 또다른 D
클래스의 바탕 클래스인지를 어떻게 탐지하는지 보여주겠다 (is_base_of
템플릿도 이 질문에 응답한다 (23.6.2항)). 여기에서 개발된 코드는 알렉산드레스쿠(Alexandrescu)가 제시한 예제를 거의 그대로 따른다 (2001, p. 35).
먼저, U
유형을 받는 test
함수를 설계한다. test
함수는 아직 미지의 유형 Convertible
의 값을 돌려준다.
Convertible test(U const &);
test
함수는 구현되지 않는다. 선언만 될 뿐이다. 유형 U
대신에 유형 T
를 사용할 수 있다면 T
를 인자로 위의 test
함수에 건넬 수 있다.
반면에 U
를 기대하는 곳에 대안 유형 T
를 사용할 수 없으면 컴파일러는 위의 test
함수를 사용할 수 없을 것이다. 대신에 선택에서 우선순위는 높지 않지만 어떤 T
유형에도 언제나 사용할 수 있는 대안 함수가 선택된다.
C는 (그리고 C++는) 언제나 받을 수 있다고 간주되는 아주 일반적인 매개변수 리스트를 제공한다. 이 매개변수 리스트는 친숙한 생략기호(ellipsis)인데, 이것은 컴파일러가 마주하는 최악의 상황을 나타낸다. 다른 모든 것이 실패하면 생략기호를 매개변수 리스트로 정의한 함수가 선택된다.
생산적인 대안은 아니지만 현재 상황에서는 정확하게 필요한 대안이다. 두 개의 후보 함수를 마주할 때 생략기호 매개변수를 정의한 함수가 있으면 컴파일러는 다른 대안이 없을 경우에 그 함수를 선택한다.
위의 추론을 따라 test(...)
대안 함수도 선언한다. 이 대안 함수는 Convertible
값이 아니라 NotConvertible
값을 돌려준다.
NotConvertible test(...);
test
의 인자가 T
유형이고 T
를 U
로 변환할 수 있으면 test
의 반환 유형은 Convertible
이다. 그렇지 않으면 NotConvertible
이 반환된다.
이 상황은 23.6.1항에서 마주했던 상황과 유사함을 확실히 보여준다. 거기에서 isClass
의 값은 컴파일 시간에 결정해야 했다. 여기에서 두 가지 관련 문제를 해결해야 한다.
T
인자를 얻을까? 이것은 언뜻 보면 생각보다 더 어렵다. T
를 정의하기가 불가능할 수도 있기 때문이다. 유형 T
가 어떤 생성자도 정의하지 않으면 어떤 T
객체도 정의되지 않는다.
Convertible
을 NotConvertible
과 구분할 수 있을까?
T
를 전혀 정의할 필요가 없다는 사실을 깨닫으면 해결된다. 어쨌거나, 의도는 컴파일 시간에 유형을 변환할 수 있는지 없는지 알아내는 것이다. T
값이나 객체를 정의하는 것이 의도는 아니다. 객체를 정의하는 것은 컴파일 시간이 아니라 실행 시간의 문제이다.
간단하게 T
를 돌려주는 함수를 선언함으로써 어디엔가 T
가 있다고 간주하라고 컴파일러에게 알릴 수 있다.
T makeT();이 신비한 함수는
T
객체가 튀어 나온다고 컴파일러를 속이는 마법의 힘이 있다. 그렇지만 우리의 필요에 실제로 맞추려면 먼저 이 함수를 조금 변경할 필요가 있다. 어떤 이유로든 T
가 어쩌다가 배열이면 컴파일러는 T makeT()
때문에 질식사할 것이다. 함수는 배열을 돌려줄 수 없기 때문이다. 그렇지만 이것은 쉽게 해결된다. 함수는 배열에 대한 참조를 돌려줄 수 있기 때문이다. 그래서 위의 선언을 다음과 같이 변경한다.
T const &makeT();
다음 코드와 같이 T const &
를 test
에 건넨다.
test(makeT())이제 컴파일러는
test
가 T const &
인자로 호출된다는 것을 알기 때문에 실제로 변환이 가능하면 그의 반환 값을 Convertible
하다고 결정하고 그렇지 않으면 NotConvertible
하다고 결정한다. 즉, 컴파일러는 test(...)
함수를 선택한다.
Convertible
을 NotConvertible
으로부터 구분하는 두 번째 문제는 정확하게 23.6.1항에서 isClass
를 결정하는 것과 같은 방식으로 해결된다. 다시 말해 크기를 다르게 만들어서 해결한다. 그러면 다음 표현식은 T
를 U
으로부터 변경할 수 있는지 없는지 결정한다.
isConvertible = sizeof(test(makeT())) == sizeof(Convertible);
char
를 Convertible
에 그리고 Char2
를 NotConvertible
에 사용함으로써 구별할 수 있다 (23.6.1항).
위의 문제는 두 개의 템플릿 유형 매개변수를 가지는 LconvertibleToR
클래스 템플릿에 요약할 수 있다.
template <typename T, typename U> class LconvertibleToR { struct Char2 { char array[2]; }; static T const &makeT(); static char test(U const &); static Char2 test(...); public: LconvertibleToR(LconvertibleToR const &other) = delete; enum { yes = sizeof(test(makeT())) == sizeof(char) }; enum { sameType = 0 }; }; template <typename T> class LconvertibleToR<T, T> { public: LconvertibleToR(LconvertibleToR const &other) = delete; enum { yes = 1 }; enum { sameType = 1 }; };
클래스 템플릿은 복사 생성자를 제거하기 때문에 어떤 객체도 생성되지 않는다. enum
값만 들여다 볼 수 있을 뿐이다. 다음 예제를 main
함수에서 실행하면 1 0 1 0
을 출력한다.
cout << LconvertibleToR<ofstream, ostream>::yes << " " << LconvertibleToR<ostream, ofstream>::yes << " " << LconvertibleToR<int, double>::yes << " " << LconvertibleToR<int, string>::yes << "\n";
Base
가 Derived
유형의 (공개) 바탕 클래스인지 아닌지 쉽게 결정할 수 있다.
상속은 (상수) 포인터의 변환 가능성을 조사함으로써 결정된다. 다음과 같은 경우 Derived const *
는 Base const *
로 변환될 수 있다.
Base
는 Derived
의 공개 그리고 명확한 바탕 클래스이다.
Base
가 void
이다.
LBaseRDerived
유형속성 클래스를 사용하여 결정할 수 있다. LBaseRDerived
는 yes
열거 값을 제공한다. 이 값은 왼쪽 유형이 오른쪽 유형의 바탕 클래스이고 두 유형이 다르면 1이다.
template <typename Base, typename Derived> struct LBaseRDerived { LBaseRDerived(LBaseRDerived const &) = delete; enum { yes = LconvertibleToR<Derived const *, Base const *>::yes && not LconvertibleToR<Base const *, void const *>::sameType }; };
코드가 클래스를 자신의 바탕 클래스로 간주하면 안될 경우에 LBaseRtrulyDerived
유형속성 클래스를 사용하여 엄격하게 테스트할 수 있다. 또한 이 유형속성 클래스는 유형이 같은지 테스트한다.
template <typename Base, typename Derived> struct LBaseRtrulyDerived { LBaseRtrulyDerived(LBaseRtrulyDerived const &) = delete; enum { yes = LBaseRDerived<Base, Derived>::yes && not LconvertibleToR<Base const *, Derived const *>::sameType }; };
다음 서술문을 main
함수에서 실행하면 1: 0, 2: 1, 3: 0, 4: 1, 5: 0
을 화면에 보여준다.
cout << "\n" << "1: " << LBaseRDerived<ofstream, ostream>::yes << ", " << "2: " << LBaseRDerived<ostream, ofstream>::yes << ", " << "3: " << LBaseRDerived<void, ofstream>::yes << ", " << "4: " << LBaseRDerived<ostream, ostream>::yes << ", " << "5: " << LBaseRtrulyDerived<ostream, ostream>::yes << "\n";
이 절 자체는 안드레이 알렉산드레스쿠(Andrei Alexandrescu)의 책 Modern C++ design(2001)에 영감을 받았다. 그러나 우리는 가변 템플릿을 사용한 점에서 다른데, 그가 책을 쓸 때는 아직 없던 기능이다. 그렇지만 그가 사용한 알고리즘은 가변 템플릿을 사용할 때에도 여전히 유용하다.
C++는 터플을 제공한다. 터플로 여러 유형의 값을 저장하고 열람할 수 있다. 여기에서는 유형의 처리에만 초점을 둔다. 간단한 TypeList
구조체를 다음 항에 연구 과제로 사용하겠다. 다음은 그의 정의이다.
template <typename ...Types> struct TypeList { TypeList(TypeList const &) = delete; enum { size = sizeof ...(Types) }; };
TypeList
에 얼마든지 유형을 저장할 수 있다. 다음은 char
와 short
그리고 int
유형 세 가지를 TypeList
에 저장하는 예이다.
TypeList<char, short, int>
sizeof
연산자를 사용하여 얻을 수 있으므로 TypeList
에 지정된 유형의 갯수를 손쉽게 얻을 수 있다 (22.5절). 예를 들어 다음 서술문은 값 3을 보여준다.
std::cout << TypeList<int, char, bool>::size << '\n';
그렇지만 sizeof
연산자를 사용할 수 없으면 TypeList
에 지정된 유형의 갯수가 어떻게 결정되는지 알아 보자.
TypeList
에 지정된 유형의 갯수를 얻으려면 다음 알고리즘이 사용된다.
TypeList
에 유형이 없으면 그 크기는 0이다.
TypeList
에 유형이 있으면 그 크기는 첫 유형 다음에 오는 유형의 갯수에 1을 더한 것이다.
TypeList
의 길이를 정의한다. 비슷하게 실행 파일에도 C++ 재귀를 사용할 수 있다. 재귀를 사용하여 NTBS의 길이를 결정할 수 있다.
size_t c_length(char const *cp) { return *cp == 0 ? 0 : 1 + c_length(cp + 1); }C++ 함수는 재귀보다는 반복을 사용한다. 그러나 반복은 템플릿 메타 프로그래밍 알고리즘에 사용할 수 없다. 템플릿 메타 프로그래밍에서 반복은 재귀를 사용하여 구현해야 한다. C++ 실행 시간 코드는 조건을 사용하여 다음 재귀를 시작할지 말지 결정할 수 있지만 템플릿 메타 프로그래밍은 그렇게 할 수 없다. 템플릿 메타 프로그래밍 알고리즘은 (부분적) 특정화에 호소해야 한다. 특정화를 사용하여 대안을 선택한다.
TypeList
에 지정된 유형의 갯수는 다음 TypeList
의 대안 구현을 사용하여 계산할 수 있다. 총칭 struct
선언과 그리고 비어 있는 TypeList
와 비어 있지 않은 TypeList
에 대하여 두 개의 특정화를 사용한다 (위의 알고리즘 기술 참고):
template <typename ...Types> struct TypeList; template <typename Head, typename ...Tail> struct TypeList { enum { size = 1 + TypeList<Tail...>::size }; }; template <> struct TypeList<> { enum { size = 0 }; };
SearchType
이) 주어진 TypeList
에 존재하는지 결정한다. (SearchType
이 TypeList
의 원소가 아니면) `index'를 -1로 정의하고 아니면 `index'를 TypeList
에서 첫 번째 나타나는 원소의 인덱스로 정의한다. 다음 알고리즘이 사용된다.
TypeList
가 비어 있으면 `index'는 -1이다.
TypeList
의 첫 번째 원소가 SearchType
이라면 `index'는 0이다.
TypeList
의 꼬리에서 SearchType
을 검색해 결과가 `index' == -1이면 -1이다.
SearchType
이 TypeList
의 꼬리에서 발견되었다는 뜻으로서) TypeList
의 꼬리에서 SearchType
을 검색할 때 인덱스는 획득한 인덱스에 1을 더해 설정된다.
ListSearch
를 사용하여 알고리즘을 구현한다.
template <typename ...Types> struct ListSearch { ListSearch(ListSearch const &) = delete; };
특정화는 알고리즘에 언급된 대안들을 처리한다.
TypeList
가 비어 있으면 `index'는 -1이다.
template <typename SearchType> struct ListSearch<SearchType, TypeList<>> { ListSearch(ListSearch const &) = delete; enum { index = -1 }; };
TypeList
의 머리가 SearchType
과 같다면 `index'는 0이다. SearchType
가 TypeList
의 첫 인자로 명시적으로 언급되어 있는 점에 주목하라:
template <typename SearchType, typename ...Tail> struct ListSearch<SearchType, TypeList<SearchType, Tail...>> { ListSearch(ListSearch const &) = delete; enum { index = 0 }; };
TypeList
의 꼬리를 검색한다. 이 검색이 돌려 준 인덱스 값은 tmp
열거 값에 저장된다. 그러면 이 값을 사용하여 인덱스의 값을 결정한다.
template <typename SearchType, typename Head, typename ...Tail> struct ListSearch<SearchType, TypeList<Head, Tail...> > { ListSearch(ListSearch const &) = delete; enum {tmp = ListSearch<SearchType, TypeList<Tail...>>::index}; enum {index = tmp == -1 ? -1 : 1 + tmp}; };
다음은 ListSearch
를 어떻게 사용할 수 있는지 보여주는 예이다.
std::cout << ListSearch<char, TypeList<int, char, bool>>::index << "\n" << ListSearch<float, TypeList<int, char, bool>>::index << "\n";
TypeList
의 특정 유형의 인덱스에 전도 연산을 하면 주어진 인덱스에 있는 유형을 열람할 수 있다. 이 전도 연산이 이 절의 주제이다.
알고리즘은 TypeAt
구조체를 사용하여 구현된다. TypeAt
는 typedef
를 사용하여 주어진 인덱스에 부합하는 유형을 정의한다. 그러나 인덱스는 경계를 넘어설 수도 있다. 그 경우 다음의 선택이 있다.
TypeList
에 유형으로 사용하면 안 되는 지역 유형을 정의한다 (예를 들어 Null
). 인덱스가 경계를 벗어나면 이 유형이 반환될 것이다. 이 지역 유형을 TypeList
안에 유형으로 사용하면 에러로 간주된다. 무효한 인덱스에서 반환된 유형으로서의 특별한 의미의 Null
과 충돌하기 때문이다. Null
이 TypeAt
구조체로부터 반환되지 않도록 하려면 TypeAt
를 평가할 때 Null
유형을 만날 경우에 static_assert
를 사용하여 잡으면 된다.
TypeAt
구조체는 validIndex
열거 값을 정의할 수 있다. 인덱스가 유효하면 true
로 설정된다. 그렇지 않으면 false
로 설정된다.
TypeAt
이 작동하는 방식이다.
TypeAt
구조체가 토대이다. 인덱스 하나와 TypeList
를 기대한다.
template <size_t index, typename Typelist> struct TypeAt;
Typelist
가 비어 있다면 static_assert
는 컴파일을 끝낸다.
template <size_t index> struct TypeAt<index, TypeList<>> { static_assert(index < 0, "TypeAt index out of bounds"); typedef TypeAt Type; };
Type
을 TypeList
에 있는 첫 유형으로 정의한다.
template <typename Head, typename ...Tail> struct TypeAt<0, TypeList<Head, Tail...>> { typedef Head Type; };
Type
는 TypeList
의 꼬리에 작동하는 TypeAt<index - 1>
가 정의한 Type
으로 정의된다.
template <size_t index, typename Head, typename ...Tail> struct TypeAt<index, TypeList<Head, Tail...>> { typedef typename TypeAt<index - 1, TypeList<Tail...>>::Type Type; };
TypeAt
구조체를 사용하는 방법이다. 첫 번째 변수 정의에서 주석 표시를 없애면 'TypeAt 인덱스가 경계를 넘음(TypeAt index out of bounds
)' 컴파일 에러가 일어난다.
typedef TypeList<int, char, bool> list3; // TypeAt<3, list3>::Type invalid; TypeAt<0, list3>::Type intVariable = 13; TypeAt<2, list3>::Type boolVariable = true; cout << "The size of the first type is " << sizeof(TypeAt<0, list3>::Type) << ", " "the size of the third type is " << sizeof(TypeAt<2, list3>::Type) << "\n"; if (typeid(TypeAt<1, list3>::Type) == typeid(char)) cout << "The typelist's 2nd type is char\n"; if (typeid(TypeAt<2, list3>::Type) != typeid(char)) cout << "The typelist's 3nd type is not char\n";
TypeList
의 앞과 뒤에 쉽게 추가할 수 있다. 재귀적인 템플릿 메타 프로그래밍을 요구하지 않는다. Append
와 Prefix
가변 템플릿 구조체 두 개 그리고 두 개의 특정화이면 모든 일이 해결된다.
다음은 두 개의 가변 템플릿 구조체의 선언이다.
template <typename ...Types> struct Append; template <typename ...Types> struct Prefix;
TypeList
에 새 유형을 앞 또는 뒤에 추가하기 위해 특정화는 TypeList
와 추가할 유형을 기대한다. 그러면 간단하게 새로운 TypeList
를 정의해 주기만 하면 그 안에 새로운 유형이 포함된다. 또다른 가변 템플릿 유형을 정의할 때 Append
특정화는 템플릿 팩을 사용할 필요가 없다는 것을 보여준다.
template <typename NewType, typename ...Types> struct Append<TypeList<Types...>, NewType> { typedef TypeList<Types..., NewType> List; }; template <typename NewType, typename ...Types> struct Prefix<NewType, TypeList<Types...>> { typedef TypeList<NewType, Types...> List; };
TypeList
으로부터 유형을 제거하는 것도 가능하다. 역시, 여러 가능성이 있다. 각 가능성마다 서로 다른 알고리즘이 탄생한다.
TypeList
으로부터 제거한다.
TypeList
으로부터 제거한다.
TypeList
으로부터 제거한다.
TypeList
으로부터 제거하되, 각 유형마다 딱 하나씩은 남기고 싶다.
TypeList
로부터 유형을 지우는 다른 방법들이 있다. 결국 어떤 방법을 구현할 것인가는 환경에 따라 달라진다. 모든 알고리즘을 구현할 수는 없겠지만 템플릿 메타 프로그래밍은 매우 강력하다. 이제 위에 언급된 알고리즘들을 다음 목에서 개발해 보자.
EraseType
을 TypeList
으로부터 제거하기 위해 재귀 알고리즘을 다시 사용한다. 템플릿 메타 프로그램은 총칭 Erase
구조체와 여러 특정화를 사용한다. 특정화는 List
유형을 정의한다. 삭제 후에 결과 TypeList
를 담고 있다. 다음은 그 알고리즘이다.
Erase
구조체 템플릿이 알고리즘의 토대이다. 삭제할 유형과 TypeList
를 기대한다.
template <typename EraseType, typename TypeList> struct Erase;
Typelist
가 비어 있으면 삭제할 것이 없다. 결과는 빈 TypeList
리스트이다.
template <typename EraseType> struct Erase<EraseType, TypeList<>> { typedef TypeList<> List; };
TypeList
의 머리가 삭제할 유형에 일치하면 List
는 TypeList
가 된다. 안에 원래의 TypeList
의 꼬리 유형을 담고 있다.
template <typename EraseType, typename ...Tail> struct Erase<EraseType, TypeList<EraseType, Tail...>> { typedef TypeList<Tail...> List; };
TypeList
의 꼬리에 적용된다. 이 결과로 TypeList
의 앞에 원래 TypeList
의 머리를 배치해야 한다. 전치 연산이 돌려준 TypeList
는 Erase::List
으로 반환된다.
template <typename EraseType, typename Head, typename ...Tail> struct Erase<EraseType, TypeList<Head, Tail...>> { typedef typename Prefix<Head, typename Erase<EraseType, TypeList<Tail...>>::List >::List List; };
Erase
로 어떻게 지우는지 보여주는 예이다.
cout << Erase<int, TypeList<char, double, int>>::List::size << '\n' << Erase<char, TypeList<int>>::List::size << '\n' << Erase<int, TypeList<int>>::List::size << '\n' << Erase<int, TypeList<>>::List::size << "\n";
TypeList
으로부터 제거하기 위해 다시 재귀 템플릿 메타 프로그래밍을 사용한다. EraseIdx
는 size_t
인덱스 값 그리고 TypeList
를 기대한다. idx
번째 (0-기반) 유형이 제거된다. EraseIdx
는 List
유형을 정의한다. 안에 결과 TypeList
를 담는다. 다음은 그 알고리즘이다.
EraseIdx
가 알고리즘의 토대이다. 삭제할 유형의 인덱스와 TypeList
를 기대한다.
template <size_t idx, typename TypeList> struct EraseIdx;
Typelist
가 비어 있으면 삭제할 것이 없다. 결과는 빈 TypeList
리스트이다.
template <size_t idx> struct EraseIdx<idx, TypeList<>> { typedef TypeList<> List; };
idx
가 0이 되는 순간에 재귀가 끝난다. 그 시점에 TypeList
의 첫 번째 유형은 무시되고 List
는 TypeList
로 초기화된다. 안에 원래의 TypeList
의 꼬리에 있는 유형이 담긴다.
template <typename EraseType, typename ...Tail> struct EraseIdx<0, TypeList<EraseType, Tail...>> { typedef TypeList<Tail...> List; };
EraseIdx
는 TypeList
의 꼬리에 적용된다. 거기에 idx
를 하나 줄인 값을 공급한다. 결과 TypeList
의 앞에 원래 TypeList
의 머리가 배치된다. 그러면 전치 연산이 돌려준 TypeList
는 EraseIdx::List
으로 반환된다.
template <size_t idx, typename Head, typename ...Tail> struct EraseIdx<idx, TypeList<Head, Tail...>> { typedef typename Prefix< Head, typename EraseIdx<idx - 1, TypeList<Tail...>>::List >::List List; };
EraseIdx
를 사용하는지 보여준다.
if ( typeid(TypeAt<2, EraseIdx<1, TypeList<int, char, size_t, double, int>>::List >::Type ) == typeid(double) ) cout << "the third type is now a double\n";
EraseType
의 모든 유형을 TypeList
으로부터 쉽게 삭제할 수 있다. 삭제 절차를 TypeList
의 머리부터 TypeList
의 꼬리까지 적용하면 된다.
다음은 그 알고리즘이다. Erase
의 알고리즘과 순서를 약간 다르게 기술한다.
TypeList
가 비어 있으면 지울 것이 없다. 결과는 빈 TypeList
이다. 이것은 정확하게 Erase
로 하는 일이다. 그래서 상속을 사용하여 템플릿 메타 프로그램의 중복 요소들을 방지할 수 있다.
template <size_t idx> struct EraseIdx<idx, TypeList<>> { typedef TypeList<> List; };
EraseAll
구조체 템플릿이 알고리즘의 토대이다. 삭제할 유형과 TypeList
를 기대한다. Erase
로부터 상속을 받았고 그래서 이미 특정화를 처리하는 빈 TypeList
를 제공한다.
template <typename EraseType, typename TypeList> struct EraseAll: public Erase<EraseType, TypeList> {};
TypeList
의 머리가 EraseType
에 일치하면 EraseAll
은 또 TypeList
의 꼬리에 적용된다. 그래서 TypeList
으로부터 EraseType
이 나타나는대로 족족 제거한다.
template <typename EraseType, typename ...Tail> struct EraseAll<EraseType, TypeList<EraseType, Tail...>> { typedef typename EraseAll<EraseType, TypeList<Tail...>>::List List; };
TypeList
의 머리가 EraseType
에 일치하지 않으면) EraseAll
은 TypeList
의 꼬리에 적용된다. 반환된 TypeList
는 원래의 TypeList
의 최초 유형과 그리고 EraseAll
재귀 호출이 반환한 TypeList
로 구성된다.
template <typename EraseType, typename Head, typename ...Tail> struct EraseAll<EraseType, TypeList<Head, Tail...>> { typedef typename Prefix< Head, typename EraseAll<EraseType, TypeList<Tail...>>::List >::List List; };
EraseAll
을 사용하는지 보여준다.
cout << "After erasing size_t from " "TypeList<char, int, size_t, double, size_t>\n" "it contains " << EraseAll<size_t, TypeList<char, int, size_t, double, size_t> >::List::size << " types\n";
TypeList
으로부터 중복을 모두 없애려면 TypeList
의 첫 원소를 TypeList
의 꼬리로부터 제거하고 그 절차를 TypeList
의 꼬리에 재귀적으로 적용해야 한다. 아래에 보여주는 알고리즘은 단순히 TypeList
를 기대한다.
EraseDup
구조체 템플릿을 선언한다. EraseDup
구조체는 TypeList
를 대표하는 List
유형을 정의한다. EraseDup
호출은 템플릿 유형 매개변수로 TypeList
를 기대한다.
template <typename TypeList> struct EraseDup;
TypeList
가 비어 있으면 반환할 수가 있고 일은 끝난다.
template <> struct EraseDup<TypeList<>> { typedef TypeList<> List; };
EraseDup
가 먼저 원래의 TypeList
의 꼬리에 적용된다. 정의상 이 결과로 모든 중복이 제거된 TypeList
가 남는다.
TypeList
는 원래 TypeList
의 최초 유형이 담겨 있을 수 있다. 그렇다면 그 유형을 제거할 유형으로 지정하고 Erase
를 적용해 제거한다.
TypeList
는 원래의 TypeList
의 최초 유형에다 이전 단계에서 생성된 TypeList
의 유형이 덧붙어 구성된다.
이 특정화는 다음과 같이 구현된다.
template <typename Head, typename ...Tail> struct EraseDup<TypeList<Head, Tail...>> { typedef typename EraseDup<TypeList<Tail...>>::List UniqueTail; typedef typename Erase<Head, UniqueTail>::List NewTail; typedef typename Prefix<Head, NewTail>::List List; };
다음은 EraseDup
를 어떻게 사용하는지 보여주는 예제이다.
cout << "After erasing duplicates from " "TypeList<double, char, int, size_t, int, double, size_t>\n" "it contains " << EraseDup< TypeList<double, char, int, size_t, int, double, size_t> >::List::size << " types\n";
이전 절에 TypeList
의 정의와 특징을 소개했다. 대부분의 C++ 프로그래머는 TypeList
를 신나고 흥미로운 지적 모험으로 생각한다. 재귀적 프로그래밍의 분야에서 기술을 연마할 기회로 생각한다.
그러나 TypeList
에는 단순한 지적 모험보다 더 많은 것이 있다. 이 장의 마지막 절에 다음 주제를 다룬다.
TypeList
로부터 클래스 만들기.TypeList
에 언급된 각 유형에 대하여 기존의 기본 템플릿의 객체들로 구성된다.
Multi
템플릿 클래스를 개발해 보자. Multi
는 Policy
템플릿 템플릿 매개변수로부터 새 클래스를 생성한다. 이 클래스에 데이터 저장 정책과 마침내 Multi
로부터 파생시킨 일련의 유형을 정의한다. 자신의 템플릿 매개변수를 MultiBase
바탕 클래스에 건네어 파생시킨다. 이어서 최종적으로 클래스 상속 트리를 만들어 낸다. 얼마나 많은 유형을 사용하게 될지 알지 못하기 때문에 Multi
는 ...Types
템플릿 팩을 사용하여 가변 클래스 템플릿으로 정의한다.
실제로는 Multi
로 지정된 유형들은 그렇게 흥미롭지 않다. 주로 Policy
클래스를 위한 `씨값으로' 기여할 뿐이다. 그러므로 Multi
의 유형들은 MultiBase
에 전달되지 않고 Policy
에 전달된다. 그리고 Policy<Type>
유형들은 연속적으로 MultiBase
에 전달된다. Multi
의 생성자는 다양한 Policy<Type>
유형에 대하여 초기 값을 기대한다. 이 값들은 완벽하게 MultiBase
로 전달된다.
Multi
클래스는
템플릿 팩을 어떻게 정책에 싸 넣을 수 있는지 보여준다 (간결하게 생성자를 인-클래스로 구현함). 다음은 Multi
의 정의이다.
template <template <typename> class Policy, typename ...Types> struct Multi: public MultiBase<0, Policy<Types>...> { typedef TypeList<Types...> PlainTypes; typedef MultiBase<0, Policy<Types>...> Base; enum { size = PlainTypes::size }; Multi(Policy<Types> &&...types) : MultiBase<0, Policy<Types>...>( std::forward<Policy<Types>>(types)...) {} };
불행하게도 지금까지 기술한 디자인은 몇 가지 결함이 있다.
Policy
템플릿 매개변수는 template <typename> class Policy
으로 정의되어 있기 때문에 한가지 유형의 인자를 기대하는 정책만을 받는다. 이와 반대로 std::vector
는 두 개의 템플릿 인자를 기대하는 템플릿이다. 두 번째 인자는 std::vector
가 사용하는 할당 전략을 정의한다. 이 할당 전략은 거의 바뀌지 않는다. 그리고 대부분의 어플리케이션은 vector<int>
나 vector<string>
등등과 같은 유형의 객체를 정의할 뿐이다. 그렇지만 템플릿 템플릿 매개변수는 요구된 템플릿 매개변수의 갯수와 유형을 올바르게 지정해야 한다. 그래서 vector
는 Multi
에 대하여 정책으로 지정할 수 없다. 이 문제의 해결방법은 복잡한 템플릿을 간단한 포장 템플릿 안에 다음과 같이 포장해 넣는 것이다:
template <class Type> struct Vector: public std::vector<Type> { Vector(std::initializer_list<Type> iniValues) : std::vector<Type>(iniValues) {} };
이제 Vector
는 자신의 기본 템플릿 인자를 사용하여 std::vector
의 두 번째 인자를 제공한다. 다른 방법으로, 선언을 사용한 템플릿을 사용할 수 있다.
TypeList
에 int
와 double
의 두 가지 유형이 들어 있고 정책 클래스는 Vector
라면 MultiBase
클래스는 결국 vector<int>
와 vector<double>
으로부터 상속을 받는다. 그러나 TypeList
에 int
유형이 두 개 지정되어 있다면 MultiBase
는 유형이 같은 두 개의 vector<int>
클래스로부터 상속을 받을 것이다. 클래스는 동일한 바탕 클래스로부터 상속받을 수 없다. 그렇게 되면 멤버 사이를 구별할 수 없게 되어 버리기 때문이다. 이에 관하여 알렉사드레스쿠(Alexandrescu)는 (2001)
이렇게 썼다 (p.67):
한 가지 큰 고민거리가 있다...:TypeList
에 유형이 중복되어 있다면 그것을 사용할 수 없다.
.... 이 모호성을 손쉽게 해결할 방법은 없다. [마침내 파생된 클래스/FBB]는 [같은 바탕 클래스/FBB]를 두 번 상속받는다.
이 유일한 유형-정의 포장 클래스들은 단지 `실제' 바탕 클래스로부터 파생된 클래스일 뿐이므로, 바탕 클래스의 기능을 상속받는다 (그래서 제공한다). 유일한 유형 정의 포장 클래스는 이전에 정의한 IntType 클래스를 따라 설계할 수 있다. 이 포장 클래스는 클래스 파생을 IntType
이 제공하는 유일성과 결합한다.
UWrap
클래스 템플릿은 템플릿 매개변수가 두 개 있다. 하나는 비-유형 매개변수 idx
이고 또 하나는 유형 매개변수이다. UWrap
구현이 유일한 idx
값을 사용하도록 보장함으로써 유일한 클래스 유형을 생성한다. 이 유일한 클래스 유형들은 그러면 MultiBase
파생 클래스의 바탕 클래스로 사용된다.
template <size_t nr, typename Type> struct UWrap: public Type { UWrap(Type const &type) : Type(type) {} };
UWrap
을 사용하면 두 개의 vector<int>
클래스를 쉽게 구별할 수 있다. UWrap<0, vector<int>>
은 첫 번째 vector<int>
를 참조하는 것이고 UWrap<1, vector<int>>
이면 두 번째 벡터를 참조하는 것이다.
다양한 UWrap
유형의 유일성은 다음 절에 연구하듯이 MultiBase
템플릿이 보증한다.
Multi
클래스를 초기화하는 것도 가능하다. 그러므로 그의 생성자는 모든 Policy
값에 대하여 초기화 값을 기대한다. 그래서 Multi
를 Vector
와 int
그리고 string
에 대하여 정의하면 생성자는 그에 부합하는 초기화 값을 받을 수 있다. 예를 들어,
Multi<Vector, int, string> mvis({1, 2, 3}, {"one", "two", "three"});
MultiBase
클래스 템플릿은 Multi
클래스의 바탕 클래스이다. 멀티 클래스는 건네어진 추가 유형을 사용하여 이 Policy
유형들을 생성한다. 이어서 바탕 클래스는 결국 Policy
유형의 리스트로부터 파생되는 클래스를 정의한다.
MultiBase
자체는 Policy
의 개념이 없다. MultiBase
에게 Policy
는 그의 유형을 사용하여 클래스를 정의하는 그냥 간단한 템플릿 팩으로 구성된 것처럼 보일 뿐이다. PolicyTypes
템플릿 팩 말고도 MultiBase
는 size_t nr
비-유형 매개변수도 정의한다. 이 매개변수는 유일한 UWrap
유형을 생성한다. 다음은 MultiBase
의 총칭 클래스 선언이다.
template <size_t nr, typename ...PolicyTypes> struct MultiBase;
특정화가 가능한 모든 MultiBase
의 요청을 두 가지 처리한다. 그 중 하나는 재귀 템플릿이다. 이 템플릿은 MultiBase
의 템플릿 매개변수 팩의 첫 번째 유형을 처리하고 재귀적으로 자신을 이용해 나머지 유형을 처리한다. 두 번째 특정화는 템플릿 매개변수 팩이 고갈되면 요청되고 아무 일도 하지 않는다. 다음은 두 번째 특정화의 정의이다.
template <size_t nr> struct MultiBase<nr> {};
재귀적으로 정의된 특정화는 흥미롭다. 다음의 일을 수행한다.
UWrap
유형으로부터 파생된다. 유일성은 UWrap
을 정의할 때 MultiBase
의 nr
매개변수를 사용하여 보장된다. nr
외에도 UWrap
클래스는 MultiBase
가 사용할 수 있게 된 템플릿 매개변수 팩의 첫 번째 유형을 받는다.
MultiBase
유형은 자신의 첫 템플릿 인자로 증가되는 nr
값을 사용하여 정의된다 (그래서 재귀적인 MultiWrap
유형으로 정의된 UWrap
유형의 유일성을 보장한다). 그의 두 번째 템플릿 인자는 MultiBase
가 사용할 수 있게 된 템플릿 매개변수 팩의 꼬리이다.
MultiBase
클래스 계통도의 조감은 그림 28에 보여준다.
MultiBase
의 생성자는 단순히 (원래) Multi
객체에 건네졌던 초기화 값을 받는다. 이를 위해 완벽한 전달이 사용된다. MultiBase
의 생성자는 첫 매개변수 값을 UWrap
바탕 클래스에 건넨다. 이 역시 완벽한 전달을 사용한다. MultiBase
의 재귀적인 정의는 다음과 같다.
template <size_t nr, typename PolicyT1, typename ...PolicyTypes> struct MultiBase<nr, PolicyT1, PolicyTypes...> : public UWrap<nr, PolicyT1>, public MultiBase<nr + 1, PolicyTypes...> { typedef PolicyT1 Type; typedef MultiBase<nr + 1, typename PolicyTypes...> Base; MultiBase(PolicyT1 && policyt1, PolicyTypes &&...policytypes) : UWrap<nr, PolicyT1>(std::forward<PolicyT1>(policyt1)), MultiBase<nr + 1, PolicyTypes...>( std::forward<PolicyTypes>(policytypes)...) {} };
Multi
클래스 템플릿은 PlainTypes
를 TypeList
로 정의한다. 이 리스트는 매개변수 팩의 모든 유형을 담고 있다. UWrap
유형으로부터 파생된 각각의 MultiBase
마다 Type
유형과 Base
유형을 정의한다. Type
유형은 UWrap
유형을 정의하는 데 사용되었던 정책 유형을 가리키고 Base
유형은 내포된 MultiBase
클래스의 유형을 나타낸다.
이 세가지 유형 정의 덕분에 Multi
객체를 생성한 유형에 접근할 수 있으며 또한 그런 유형의 값에도 접근할 수 있다.
typeAt
은 순수한 메타 클래스 템플릿이다 (실행시간 실행 코드가 전혀 없다). typeAt
은 size_t idx
템플릿 인자를 기대한다. 이 인자는 Multi
유형과 더불어 Multi
유형의 객체 안에 있는 정책 유형의 인덱스를 지정한다. Type
을 Multi
의 MultiBase<idx, ...>
바탕 클래스가 정의하는 Type
으로 정의한다. 예를 들어:
typeAt<0, Multi<Vector, int, double>>::Type // Type은 vector<int>이다.
클래스 템플릿 typeAt
는 모든 일을 처리하는 내포된 클래스 템플릿 PolType
를 (정의하고) 사용한다. PolType
의 총칭 정의는 두 개의 템플릿 매개변수를 지정한다. index는 요청된 유형의 인덱스를 지정하고 typename은 MultiBase
유형 인자로 초기화된다. PolType
의 재귀적 정의는 재귀적으로 인덱스 비-유형 매개변수를 줄여가면서 MultiBase
의 상속 트리에서 다음 바탕 클래스를 재귀 호출에 건넨다. 마침내 PolType
은 Type
유형을 요청된 정책 유형으로 정의한다. 재귀 호출에 의해 정의되는 유형처럼 재귀 정의에 의해 Type
유형이 정의된다. 마지막 (비-재귀) 특정화는 MultiBase
유형의 최초 유형을 Type
으로 정의한다. 다음은 typeAt
의 정의이다.
template <size_t index, typename Multi> class typeAt { template <size_t idx, typename MultiBase> struct PolType; template <size_t idx, size_t nr, typename PolicyT1, typename ...PolicyTypes> struct PolType<idx, MultiBase<nr, PolicyT1, PolicyTypes...>> { typedef typename PolType< idx - 1, MultiBase<nr + 1, PolicyTypes...>>::Type Type; }; template <size_t nr, typename PolicyT1, typename ...PolicyTypes> struct PolType<0, MultiBase<nr, PolicyT1, PolicyTypes...>> { typedef PolicyT1 Type; }; public: typeAt(typeAt const &) = delete; typedef typename PolType<index, typename Multi::Base>::Type Type; };
Multi
의 매개변수 팩에 지정된 유형은 두 번째 도움자 plainTypeAt
클래스 템플릿을 사용하여 열람할 수 있다. 예를 들어:
plainTypeAt<0, Multi<Vector, int, double>>::Type // 유형은 int이다.
plainTypeAt
클래스 템플릿은 typeAt
과 비슷하게 (그러나 더 단순하게) 구현된다. 역시 순수한 메타 클래스 템플릿이다. 내포된 클래스 템플릿 At
를 정의한다. At
는 typeAt
과 비슷하게 구현되었지만 Multi
에 건네어진 원래의 템플릿 팩의 유형들을 방문한다. Multi
가 그의 PlainTypes
유형으로 사용가능하도록 만들어 준다. 다음은 plainTypeAt
의 정의이다.
template <size_t index, typename Multi> class plainTypeAt { template <size_t idx, typename List> struct At; template <size_t idx, typename Head, typename ...Tail> struct At<idx, TypeList<Head, Tail...>> { typedef typename At<idx - 1, TypeList<Tail...>>::Type Type; }; template <typename Head, typename ...Tail> struct At<0, TypeList<Head, Tail...>> { typedef Head Type; }; public: plainTypeAt(plainTypeAt const &) = delete; typedef typename At<index, typename Multi::PlainTypes>::Type Type; };
가장 깔끔한 지원 템플릿은 get
이다. 이 함수 템플릿은 size_t idx
를 첫 템플릿 매개변수로 정의하고 typename Multi
를 두 번째 템플릿 매개변수로 정의한다. get
함수 템플릿은 함수 매개변수를 하나 정의한다. Multi
에 대한 참조이므로 Multi
의 유형을 그 자체로 추론할 수 있다. Multi
라는 사실을 알고 있기 때문에 당연히 UWrap<nr, PolicyType>
이며 그러므로 역시 PolicyType
이라고 추론한다. 이 클래스는 UWrap
의 바탕 클래스로 정의되어 있기 때문이다.
클래스 유형의 객체는 참조를 바탕 클래스로 초기화할 수 있으므로 PolicyType &
는 적절한 UWrap
참조로 초기화할 수 있다. 이 참조는 이번에는 Multi
객체로 초기화할 수 있다. (typename typeAt<idx, Multi>::Type
을 평가하는 것은 순전히 컴파일 시간에 관련된 문제라는 사실을 주목하면) TypeAt
를 사용하여 PolicyType
유형을 결정할 수 있기 때문에 get
함수는 인라인으로 잘 구현할 수 있다. return
서술문 하나면 된다.
template <size_t idx, typename Multi> inline typename typeAt<idx, Multi>::Type &get(Multi &multi) { return static_cast< UWrap<idx, typename typeAt<idx, Multi>::Type> &>(multi); }
중간에 UWrap
유형 변환이 요구된다. (두 개의 vector<int>
유형처럼) 동일한 정책 유형 사이를 구별하기 위해서이다. UWrap
는 nr
템플릿 인자로 유일하게 구별되므로 그리고 이것은 get
에 건넨 인자가 숫자이므로 모호성을 손쉽게 방지할 수 있다.
Multi
와 그의 지원 템플릿들을 개발하였으므로 어떻게 Multi
를 사용할 수 있는가?
경고 한 마디를 덧붙인다. 개발된 클래스의 크기를 줄이기 위해 최소한으로 설계되었다. 예를 들어 get
함수 템플릿은 Multi const
객체와 함께 사용할 수 없으며 Multi
유형에 대하여 기본 생성자나 이동 생성자도 전혀 사용할 수 없다. Multi
는 템플릿 메타 프로그래밍의 가능성을 보여주기 위하여 설계되었다. 그런 목적에 기여하기를 바라는 마음으로 Multi
를 구현했다. 그렇다면 어떻게 사용하는가?
이 절은 예제에 주해를 붙였다. 주해는 모여서 일련의 서술문을 정의하게 되고 이를 main
함수의 몸체에 배치하면 그 결과로 작동하는 프로그램이 탄생할 수도 있다.
Policy
는 다음과 같이 정의할 수 있다.
template <typename Type> struct Policy { Type d_type; Policy(Type &&type) : d_type(std::forward<Type>(type)) {} };
Policy
는 데이터 멤버를 정의한다. 그리고 Multi
객체를 정의할 수 있다.
Multi<Policy, string> ms(Policy<string>("hello")); Multi<Policy, string, string> ms2s(Policy<string>("hello"), Policy<string>("world")); typedef Multi<Policy, string, int> MPSI; MPSI mpsi(string("hello"), 4);
Multi
클래스 또는 객체가 정의한 유형의 갯수를 얻으려면 (Multi
클래스를 사용하여) ::size
열거 값을 사용하거나 또는 (Multi
객체를 사용하여)
.size
멤버를 사용하라:
cout << "There are " << MPSI::size << " types in MPSI\n" "There are " << mpsi.size << " types in mpsi\n";
plainTypeAt
를 사용하여 정의할 수 있다.
plainTypeAt<0, MPSI>::Type sx = "String type"; plainTypeAt<1, MPSI>::Type ix = 12;
cout << static_cast<Policy<string> &>(mpsi).d_type << '\n' << static_cast<Policy<int> &>(mpsi).d_type << '\n';
Policy<Type>
유형을 구별할 수 없기 때문이다. 그래도 get
은 여전히 작동한다.
typedef Multi<Policy, int, int> MPII; MPII mpii(4, 18); cout << get<0>(mpii).d_type << ' ' << get<1>(mpii).d_type << '\n';
std::vector
를 Vector
에 싸 넣는 예제이다.
typedef Multi<Vector, int, double> MVID; MVID mi({1, 2, 3}, {1.2, 3.4, 5.6, 7.8});
Multi
유형으로 정의할 수 있다.
typeAt<0, Multi<Vector, int>>::Type vi = {1, 2, 3};
Vector
는 std::vector
이므로 get
이 돌려주는 참조는 인덱스 연산자를 지원한다. 왼쪽 피연산자 또는 오른쪽 피연산자로 사용할 수 있다.
cout << get<0>(mi)[2] << '\n'; get<1>(mi)[3] = get<0>(mi)[0]; cout << get<1>(mi)[3] << '\n';
std::vector
객체를 처리한다고 가정해 보자. 벡터는 서로 할당이 가능하지만, 그게 다이다. 앞서 보았듯이 벡터의 멤버 함수는 현재 원소에는 작동을 하지만 덧셈 뺄셈 나눗셈 곱셈 등등의 산술 연산은 벡터 쌍에 적용할 수 없다 (12.4.2항).
다행스럽게도 벡터에 덧셈 연산을 구현하는 것은 특별히 어렵지는 않다. VecType
이 벡터 유형이라면 VecType &&operator+(VecType const &lhs, VecType const &rhs)
그리고
VecType &&operator+(VecType &&lhs, VecType const &rhs)
와 같이 덧셈을 수행하는 자유 함수를 구현하는 것은 여러분에게 간단한 연습문제로 남긴다 (제 11장).
이제 one + two + three + four
와 같은 표현식을 연구해 보자. 이 합을 계산하려면 네 단계가 걸린다. 먼저, tmp = one
를 계산하여 최종 반환 값을 만든다. 벡터 tmp
는 임시 변수이다 (실제로 이름이 없다). 다음 tmp += two
를 계산하고, 이어서 tmp += three
를 계산하고 마지막으로 tmp += four
를 계산하여 최종 결과를 얻는다 (물론 std::vector::operator+=
를 구현하면 안 된다. std 이름 공간은 우리에게 금지 구역이다. 리스코프 교체 원칙에 따라 operator+=
를 제공하는 std::vector
로부터 클래스를 상속받으면 안 된다 (14.7 절). 그러나 돌아가는 방법이 있다. 여기에서는 그냥 operator+=
을 사용할 수 있다고 간주한다.
다음은 VecType
에 대하여 operator+=
를 구현하는 방법이다.
VecType &VecType::operator+=(VecType const &rhs) { for (size_t idx = 0, end = size(); idx != end; ++idx) (*this)[idx] += rhs[idx]; return *this; }다음 구현을 연구해 보자:
VecType
객체를 추가하고 그런 객체에 n
개의 원소가 있다면 2 * n
회의 인덱스를 평가해야 한다. k
개의 VecType
객체를 추가하면 이 갯수는 대략 2 * k * n
회로 폭증한다. 벡터를 추가하는 비율보다 훨씬 더 많이 평가해야 한다.
대신에 `행 단위로' 평가할 수 있다면 각 벡터 원소에 한 번만 접근하면 된다. 이렇게 하면 극적으로 인덱스 평가 횟수를 줄일 수 있다. 대략 n * k
회의 인덱스 표현식을 평가하면 될 것이다.
표현식 템플릿으로 정확하게 이런 종류의 최적화를 달성할 수 있다. 다음 항에서 그 설계와 구현을 살펴 보겠다.
one + two + three + four
와 같이 표현식을 표준으로 구현할 때, 여기에서 객체들은 n
개의 원소를 가진 벡터이다. k
개의 벡터가 있다면 총 k * 2 * n
회의 인덱스를 평가해야 한다.
표현식 템플릿으로 이런 평가를 획기적으로 줄일 수 있다. 표현식 템플릿을 사용하면 이 템플릿들은 벡터에 접근할 수 있지만 추가 연산을 하는 동안에는 원소에 접근하지 않는다.
표현식 템플릿 이름이 ET라고 가정하자. 그리고 one + two + three
를 더하고 싶다. 그러면 첫 번째 +
연산자는 그냥 ET(one, two)
를 만든다. 실제로는 연산이 더 수행되지 않았음을 눈여겨보라. ET
는 단순히 (ET의 lhs
데이터 멤버가 되는) one
에 대한 참조와 그리고 ( ET의 rhs
데이터 멤버가 되는) two
에 대한 (상수) 참조를 저장할 뿐이다. 일반적으로 ET
는 생성자에 건네어진 두 개의 인자를 참조로 저장한다.
다음 덧셈 연산자에서 또다른 ET
가 만들어진다. 그의 생성자 인자는 각각 one
과 two
에 대하여 방금 생성된 ET
객체 그리고 벡터 three
이다. 역시 ET 객체들에 의하여 덧셈은 수행되지 않는다.
이 알고리즘은 쉽게 벡터의 갯수에 상관없이 일반화된다. 괄호를 사용할 수도 있다. 예를 들어 (one + two) + (three + four)
이라면 결과는 다음과 같다.
ET(ET(one, two), ET(three, four))
아마도 어느 시점에서는 벡터의 합을 얻고 싶을 것이다. 이를 위해 표현식 템플릿에 ET
객체를 벡터로 변환할 변환 연산자를 제공한다. 아니면 같은 일을 해 줄 할당 연산자를 제공한다.
변환 연산자는 다음과 같이 보인다.
operator ET::VecType() const { VecType retVal; retVal.reserve(size()); for (size_t ix = 0, end = size(); ix != end; ++ix) new(&retVal[ix]) value_type((*this)[ix]); return retVal; }효율성의 이유로 배치
new
를 사용한다. 기본 값으로 먼저 retVal
을 초기화할 필요가 없다. 그렇지만 정말 흥미로운 부분은 (*this)[ix]
표현식 뒤에 숨어 있다. 이 시점에서 실제 덧셈이 일어난다.
ET
의 인덱스 연산자는 그냥 lhs
와 rhs
데이터 멤버에 상응하는 인덱스 표현식이 돌려주는 값들을 더할 뿐이다. 데이터 멤버가 벡터를 참조하면 상응하는 벡터 원소가 사용되어, 그것을 다른 데이터 멤버의 값에 더한다. 데이터 멤버 자체가 ET 객체를 참조하면, 그 내포된 ET
객체의 인덱스 연산자가 자신의 데이터 멤버에 똑같은 덧셈을 수행하여 그 합을 돌려준다. 그래서 (*this)[0]
과 같은 표현식은 first[0] + second[0] + third[0]
을 돌려주고, 계산된 합은 배치 new
를 사용하여 retVal[0]
에 저장된다.
이 경우 인덱스 표현식 평가에 필요한 갯수는 (k개의 벡터의 n개의 원소에 대하여) n * k
더하기 (retVal
의 n개의 원소에 대하여) n
이다. 결론적으로 (k + 1) * n
이다.
k > 1
이라면 (k + 1) * n < 2 * k * n
이므로 표현식 템플릿은 전통적인 operator+
구현보다 더 효율적으로 덧셈을 평가한다. 표현식 템플릿을 사용하면 얻는 또다른 혜택은 괄호를 둘러 표현식을 사용할 때 따로 더 임시 벡터 객체를 만들지 않는다는 것이다.
typedef std::vector<int> IntVect
를 사용하여 표현식 템플릿을 만드는 방법을 보여준다.
시작은 간단한 main
함수이다. 이 안에 여러 IntVect
객체를 추가한다. 예를 들어,
int main() { IntVect one; IntVect two; IntVect three; IntVect four; // ... IntVects가 값을 받는다고 간주한다. four = one + two + three + four; }이 시점에서 코드는 표현식 템플릿이 사용될 것이라는 징후가 보이지 않는다. 그렇지만
operator+
의 구현이 특별하다. 단순히 operator+
으로 생성된 객체를 돌려주는 템플릿일 뿐이다.
template<typename LHS, typename RHS> BinExpr<LHS, RHS, plus> &&operator+(LHS const &lhs, RHS const &rhs) { return BinExpr<LHS, RHS, plus>(lhs, rhs); }
표현식 템플릿을 BinExpr
이라고 하자. 세 개의 템플릿 유형 매개변수가 있다. 두 개는 객체 유형이고 하나는 요청한 연산을 수행하는 템플릿 템플릿 매개변수이다. 선언은 다음과 같이 한다.
template<typename LHS, typename RHS, template<typename> class Operation> struct BinExpr;
LHS
와 RHS
는 표현식 템플릿으로 처리되는 데이터 유형이거나 아니면 두 개의 다른 유형 이름을 요구하는 BinExpr
이다. Operation
은 표현식 템플릿이 수행하는 연산이다. 템플릿 템플릿 매개변수를 사용하면 BinExpr
를 사용하여 단순한 덧셈 말고도 원하는 연산은 무엇이든 수행할 수 있다. 표준 산술 연산자에 대하여 std::plus
와 같이 미리 정의된 함수 템플릿을 사용할 수 있다. 다른 연산자에 대해서는 따로 함수 템플릿을 정의할 수 있다.
BinExpr
의 생성자는 lhs
와 rhs
에 대한 상수 참조를 초기화한다. 인-클래스 구현은 다음과 같다.
BinExpr(LHS const &lhs, RHS const &rhs) : d_lhs(lhs), d_rhs(rhs) {}
결과 IntVect
를 열람하기 위해 변환 연산자를 정의한다. 이미 그의 구현을 (이전 항에서) 만나 보았다. 다음이 바로 그 구현이다. BinExpr
멤버를 인-클래스로 구현하였다.
operator ObjType() const { ObjType retVal; retVal.reserve(size()); for (size_t idx = 0, end = size(); idx != end; ++idx) new(&retVal[idx]) value_type((*this)[idx]); return retVal; }아래에
ObjType
유형으로 돌아간다. 이 시점에서 IntVect
라고 간주할 수 있다. size()
멤버는 단순히 d_lhs.size()
를 돌려준다. 일련의 IntVect
덧셈에서 LHS
는 결국 IntVect
가 되고 그리고 BinExpr
마다 유효한 size()
가 다음과 같이 정의된다.
size_t size() const { return d_lhs.size(); }
구현할 나머지 멤버 하나는 operator[]
이다. 인덱스를 받기 때문에 요청된 연산을 d_lhs
와 d_rhs
데이터 멤버에 상응하는 인덱스의 원소에만 수행할 필요가 있다. 표현식 템플릿의 아름다움은 무엇이든 그 자체가 BinExpr
이라면 그 표현식 템플릿은 이번에는 operator[]
를 호출하고 그래서 결국 IntVect
객체의 상응하는 모든 원소에 대하여 요청된 연산을 수행한다는 것이다. 다음은 그의 구현이다.
value_type operator[](size_t ix) const { static Operation<value_type> operation; return operation(d_lhs[ix], d_rhs[ix]); }이 구현은 또다른 유형을 사용한다.
value_type
이 그것으로서 표현식 템플릿이 처리하는 벡터 유형의 원소 유형이다. 앞의 ObjType
처럼 그의 정의는 아래에 다룬다. 정적 operation
데이터 멤버는 단순히 ExprType
객체를 생성할 때 지정되는 Operation
유형을 구체화한 것이다.
그래서 ObjType
와 value_type
을 살펴볼 시간이다. 다음 절에 다루어 보겠다.
BinExpr
표현식 템플릿은 두 가지 유형을 미리 알아야 객체들을 구체화할 수 있다. 먼저 ObjType
를 알아야 한다. 이것은 표현식 템플릿이 처리하는 객체의 유형이다. ObjType
객체에는 값이 담겨 있다. 그 다음 이 값들의 유형을 ObjType::value_type
로 결정할 수 있어야 한다. 예를 들어 IntVect
에 대하여 value_type
은 int
이다.
one + two + three
와 같은 표현식에서 BinExpr
표현식 템플릿은 두 개의 IntVect
객체를 받는다. 이것은 언제나 참이다. 처음 생성된 BinExpr
객체는 두 개의 IntVect
객체를 받는다. 이 경우 ObjType
는 그냥 LHS
이고 ObjType::value_type
도 역시 사용할 수 있다. value_type
을 이미 LHS
가 정의하고 있거나 아니면 BinExpr
은 그것이 value_type
을 정의하고 있기를 요구한다.
BinExpr
객체에 건넨 인자들이 언제나 기본 ObjType
유형인 것은 아니기 때문에 (다음 내포 레벨의 BinExpr
객체는 BinExpr
인자를 최소한 하나는 받는다) BinExpr
으로부터 ObjType
을 결정할 방법이 필요하다. 이를 위해 유형속성 클래스를 사용한다. BasicType
유형속성 클래스는 typename
유형의 템플릿 인자를 받아 ObjType
유형의 템플릿 유형 인자와 같게 만든다.
template<typename Type> struct BasicType { typedef Type ObjType; };특정화는
Type
이 실제로 BinExpr
인 상황을 처리한다.
template<typename LHS, typename RHS, template<typename> class Operation> struct BasicType<BinExpr<LHS, RHS, Operation>> { typedef typename BinExpr<LHS, RHS, Operation>::ObjType ObjType; };
BinExpr
은 ObjType::value_type
가 정의되어 있는 유형이어야 하므로 value_type
이 자동으로 선택된다.
BinExpr
은 BasicType
를 참조하고 BasicType
는 어디에선가 BinExpr
를 참조하므로 전방 선언을 해야 한다. BinExpr
은 이미 선언되어 있으므로 그 선언을 가지고 시작하자. 다음은 그 결과이다.
BinExpr 선언 BasicType 선언 BasicType 특정화 (BinExpr에 대하여) template<typename LHS, typename RHS, template<typename> class Operation> class BinExpr { LHS const &d_lhs; RHS const &d_rhs; public: typedef typename BasicType<RHS>::DataType DataType; typedef typename DataType::value_type value_type; // 모든 BinExpr 멤버 함수 };