제 23 장: 템플릿 고급 사용법

템플릿의 주목적은 클래스와 함수를 총칭적으로 정의하는 것이다. 그 다음에 특정한 유형으로 맞춤 재단할 수 있다.

그러나 템플릿으로 그 보다 더 많은 일을 할 수 있다. 컴파일러의 구현에 제한만 없다면 템플릿으로 무슨 계산이든 컴파일 시간에 프로그램할 수 있다. 현대의 어떤 컴퓨터 언어도 제공하지 못하는 이 놀라운 특징은 컴파일 시간에 세 가지 일을 할 수 있다는 사실에 뿌리가 있다.

물론 컴파일러에게 소수를 계산하라고 요구하는 것이 그 하나이다. 그러나 최고의 속도를 얻기 위해 그렇게 하는 것과는 완전히 다른 일이다. 컴파일러가 복잡한 계산을 대신 수행해 주더라도 속도가 크게 빨라질 것이라고는 기대하지 마라. 그것은 요점을 멀리 벗어난 것이다. 어쨌든 컴파일러에게 C++의 템플릿 언어를 사용하여 사실상 무엇이든 계산하라고 요구할 수 있다. 물론 소수 계산을 포함해서 말이다....

이 장은 템플릿의 이 놀라운 특징들을 다룬다. 템플릿에 관련된 미묘한 점들을 짧게 살펴 본 후에 템플릿 메타 프로그래밍의 핵심 특징을 소개한다.

템플릿 유형 매개변수와 템플릿 비-유형 매개변수 외에도 제 삼의 템플릿 매개변수가 있다. 템플릿 템플릿 매개변수가 그것이다. 이 종류의 템플릿 매개변수를 다음에 소개한다. 이를 토대로 유형속성(trait) 클래스와 정책(policy) 클래스를 연구한다.

이 장은 템플릿의 여러 흥미로운 적용 방식을 연구하며 끝낸다. 컴파일러 에러 메시지 적용하기와 클래스 유형으로 변환하기 그리고 컴파일 시간에 리스트를 처리하는 방법을 보여주는 정교한 예제를 다루고 끝내겠다.

이 장은 두 권의 권장 도서로부터 영감을 받았다.

23.1: 중요한 세부 요소들

22.2.1항에서 typename 키워드의 특별한 적용 방법을 연구했다. 거기에서 typename 키워드는 (복합) 유형에 대하여 이름을 정의할 뿐만 아니라 클래스 템플릿으로 정의된 유형과 클래스 템플릿으로 정의된 멤버를 구분한다는 것도 배웠다. 이 절은 typename 키워드의 적용 방법을 두 가지 더 소개한다. typename 키워드의 특별한 적용 방법 말고도 23.1.3항에서 typename 키워드의 확장 사용법과 관련하여 몇 가지 새로운 구문을 소개한다. ::template.template 그리고 ->template은 템플릿 안에 사용된 이름이 클래스 템플릿이라는 사실을 컴파일러에게 알려준다.

23.1.1: 클래스 템플릿 안에 내포된 유형을 돌려주기

다음 예제를 보면 템플릿 매개변수에 의존하지 않는 내포 클래스가 클래스 템플릿 안에 정의되어 있다. 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가 되는데 이 유형은 확실히 템플릿 유형 매개변수에 따라 달라진다.

앞과 마찬가지로 typenameOuter<T>::Nested 앞에 쓰면 컴파일 에러가 사라진다. 그래서 올바르게 nested 함수를 구현하면 다음과 같다.

    template <typename T>
    typename Outer<T>::Nested Outer<T>::nested() const
    {
        return Nested();
    }

23.1.2: 바탕 클래스 멤버에 대한 유형 결정

아래에 BaseDerived 두 개의 클래스 템플릿이 있다. BaseDerived의 바탕 클래스이다.
    #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::Derivedmember를 호출하면 템플릿 유형 매개변수를 요구하지 않기 때문이다.

어느 유형을 사용해야 할지 결정하기 위하여 템플릿 유형 매개변수를 사용할 수 없으면 컴파일러에게 사용할 템플릿 유형 매개변수에 관하여 (그러므로 호출할 특별한 함수(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
    */

위의 예제는 또한 가상 멤버 템플릿의 사용법을 보여준다 (물론 가상 멤버는 자주 사용되지 않는다). 예제에서 Basevirtual void member를 선언하고 Derived는 자신의 재정의 member 함수를 선언한다. 이 경우 Derived::Derivedthis->member()member의 가상 함수적 성질 때문에 Derived::member를 호출한다. 그렇지만 서술문 Base<T>::member()는 언제나 Basemember 멤버 함수를 호출하며 그러므로 동적 다형성을 우회할 수 있다.

23.1.3: ::template과 .template 그리고 ->template

일반적으로 컴파일러는 이름의 진짜 유형을 결정할 수 있다. 그러나 연구한 바와 같이 언제나 그런 것은 아니다. 종종 컴파일러에게 조언을 해 줄 필요도 있다. 그 목적으로 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 클래스 템플릿을 정의한다. Usagecaller 멤버 함수를 제공한다. 위의 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
        };
    };

23.2: 템플릿 메타 프로그래밍

23.2.1: 템플릿에 따라 달라지는 값

템플릿 프로그래밍에서 값은 열거체(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를 사용하여 intdouble로 유형을 변환하면 컴파일러는 다음과 같이 에러를 일으킨다.
    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)을 요구하면 에러가 보고되지 않는다.

위의 예제는 열거체를 사용하여 (컴파일 시간에) 가정을 만족하지 못하면 불법인 값을 계산했다. 창조적인 부분은 적절한 표현식을 찾는 것이다.

열거 값들은 이런 상황에 잘 맞는다. 메모리를 소비하지 않으며 평가해도 실행 코드를 전혀 생산하지 않기 때문이다. 값을 추적하는 데에도 사용할 수 있다. 열거 값이 최종 결과가 된다. 실행 코드가 아니라 컴파일러가 계산한 값이다. 이는 다음 절에 보여준다. 일반적으로 컴파일 시간에 할 수 있다면 실행시간에 하지 말아야 한다. 상수 값이 결과인 복잡한 계산은 이 원칙의 좋은 예이다.

23.2.1.1: 정수 유형을 다른 유형으로 변환하기

템플릿 안에 감추어진 값의 또다른 사용법은 단순한 스칼라 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";
    }

23.2.2: 템플릿을 사용하여 대안 선택하기

프로그래밍 언어의 본질적인 특징은 코드를 조건적으로 실행할 수 있다는 것이다. 이를 위하여 C++ifswitch 서술문이 있다. `컴파일러를 프로그래밍'하고 싶다면 이 특징도 템플릿으로 제공해야 한다.

값을 저장하는 템플릿처럼 선택을 하는 템플릿은 실행 시간에 어떤 코드도 실행하기를 요구하지 않는다. 선택은 순수하게 컴파일러에 의해 컴파일 시간에 이루어진다. 템플릿 메타 프로그래밍의 정수는 어떠한 코드도 실행하거나 어떤 코드에도 의존하지 않는다는 것이다. 템플릿 메타 프로그래밍의 결과가 실행 코드일지라도 그 코드는 그냥 컴파일러가 만들어 낸 결정 함수일 뿐이다.

템플릿 (멤버) 함수는 실제로 사용될 때만 구체화된다. 결과적으로 서로 배타적인 특수 함수를 정의할 수 있다. 그리하여 이 상황에서는 컴파일되지만 저 상황에서는 컴파일되지 않는 그리고 저 상황에서는 컴파일되지만 이 상황에서는 컴파일되지 않는 특수 함수를 정의할 수 있다. 특정화를 사용하면 특정한 상황의 요구에 맞게 코드를 만들어 낼 수 있다.

이와 같은 특징은 실행 시간 코드로는 구현할 수 없다. 총칭 저장 클래스를 설계할 때 프로그래머는 최종 저장 클래스에 값 클래스 유형의 객체는 물론 다형적 클래스 유형의 객체도 저장하려고 할 것이다. 그리하여 그 프로그래머는 저장 클래스에 객체 자체가 아니라 객체를 가리키는 포인터가 있어야 한다고 결론을 내린다. 초기 구현 시도는 다음과 같을 것이다.

    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이 값 클래스에 대하여 전혀 호출되지 않으며 복사 생성자는 값 클래스에만 사용할 수 있고 다형적 클래스에는 사용할 수 없다는 사실에 컴파일러는 개의치 않는다. 컴파일러는 그저 코드를 컴파일할 뿐이며 멤버가 없기 때문에 그렇게 할 수도 없다. 아주 단순하다.

23.2.2.1: 중복정의 멤버 정의하기

템플릿 메타 프로그래밍이 구세주가 된다. 클래스 템플릿 멤버 함수는 사용될 때만 구체화된다는 사실을 알고 있으므로 우리의 계획은 중복정의 add 멤버 함수를 디자인하는 것이다. 그 중에 하나만 호출되고 구체화될 것이다. (Type 자체와 더불어) 추가 템플릿 비-유형 매개변수에 기반하여 선택할 것이다. 이 매개변수는 다형적 클래스인지 아니면 비-다형적 클래스인지에 대하여 Storage를 사용할지 말지를 나타낸다. Storage 클래스는 다음과 같이 시작한다.
    template <typename Type, bool isPolymorphic>
    class Storage
맨처음에 add 멤버의 중복정의 버전을 두 가지 정의한다. Storage 객체에 사용되는 버전 하나는 다형적 객체를 저장하고 (true를 템플릿 비-유형 인자로 사용) 다른 버전은 값 클래스 객체를 저장한다 (false를 템플릿 비-유형 인자로 사용).

불행하게도 작은 문제에 봉착한다. 함수는 인자 값으로 중복정의할 수 없고 인자 유형으로만 중복정의가 가능하다. 그러나 이 작은 문제는 해결할 수 있다. 유형이 템플릿의 이름과 인자로 정의된다는 사실을 알고 있다면 truefalse 값을 유형으로 변환할 수 있다. 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 멤버중 하나만 구체화된다. 다른 멤버들은 절대로 호출되지 않으므로 (그리하여 절대로 구체화되지 않으므로) 컴파일 에러가 방지된다.

23.2.2.2: 함수가 템플릿 매개변수인 클래스

어떤 프로그래머는 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;
    };
앞의 (총칭적) 정의는 FirstTypeIfElse::type 유형 정의에 연관짓고 (논리 값 false에 대하여 부분적으로 특정화된) 뒤의 정의는 SecondTypeIfElse::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을 사용한다. IfElseStorage의 벡터 데이터 유형에 대하여 사용할 실제 데이터 유형을 정의한다.

이 예제에서 놀라운 결과는 Storage 클래스의 데이터 조직이 이제 자신의 템플릿 인자에 따라 달라진다는 것이다. isPolymorphic == false일 때와 isPolymorphic == true일 때 서로 다른 데이터 유형을 사용하기 때문에 중복정의 비밀 add 멤버는 이 차이를 즉시 이용할 수 있다. 예를 들어 add(Type const &obj, IntType<false>)는 직접적으로 복사 생성하여 obj의 사본을 d_vector에 저장한다.

여러 유형으로부터 선택하는 것도 가능하다. IfElse 구조체는 내포가 가능하기 때문이다. IfElse를 사용하더라도 최종 실행 프로그램의 크기나 속도에 전혀 영향을 미치지 않는다는 것을 눈여겨보라. 최종 프로그램은 그냥 마침내 적절하게 선택된 유형을 가질 뿐이다.

23.2.2.3: 눈에 보이는 예제

다음 예제는 평범한 유형 또는 포인터를 키나 값 유형으로 가지는 맵으로 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 유형을 추가한다. 컴파일러는 템플릿의 비-유형 선택자 매개변수에 맞게 중복정의 버전을 적절하게 선택할 수 있다.

23.2.3: 템플릿: 재귀를 이용한 반복

템플릿 메타 프로그래밍에는 변수가 없으므로 템플릿에는 for 또는 while 서술문에 해당하는 것이 없다. 그렇지만 반복은 언제나 재귀로 재작성할 수 있다. 템플릿은 재귀를 지원한다. 그래서 반복은 언제나 (꼬리) 재귀로 구현할 수 있다.

(꼬리) 재귀로 반복을 구현하려면 다음과 같이 한다.

컴파일러는 총칭적인 템플릿 구현보다 특정화된 템플릿 구현을 선호한다. 컴파일러가 끝 조건에 다다를 쯤이면 재귀는 멈춘다. 특정화는 재귀를 사용하지 않기 때문이다.

아마도 느낌표(!)로 나타내는 수학의 `계승' 연산자의 재귀 구현에 익숙할 것이다. 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>::valueFactorial<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";
    }

23.3: 사용자-정의 기호상수

11.12절에서 연구한 기호상수 연산자 외에도 C++는 함수 템플릿 기호상수 연산자도 지원한다. 원형은 다음과 같다.
    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>() };
    };

23.4: 템플릿 템플릿 매개변수

다음 상황을 연구해 보자: 프로그래머에게 Storage 저장 클래스를 설계해 달라고 부탁했다. Storage 객체에 저장된 데이터는 데이터의 사본을 만들어 저장하거나 아니면 받은 그대로 저장한다. Storage 객체는 벡터나 연결 리스트도 아래에 깔린 저장 매체로 사용할 수 있다. 프로그래머는 이 요구조건을 어떻게 처리해야 할까? 네가지 다른 Storage 클래스를 설계해야 할까?

프로그래머의 첫 번째 대응은 만능 Storage 클래스를 개발하는 것이다. 두 개의 데이터 멤버로 리스트와 벡터를 가질 수 있고 생성자는 아마도 열거 값으로 제공할 수 있을 것이다. 데이터 자체를 저장할지 아니면 새 사본을 저장할지 알려준다. 열거 값을 사용하면 일련의 포인터를 멤버 함수로 초기화할 수 있다. 요청된 과업을 수행한다 (예를 들어 벡터를 사용하여 데이터를 저장하거나 리스트를 사용하여 사본을 저장한다).

복잡하기는 하지만 실행할 수 있다. 그런데 또 프로그래머에게 클래스를 변경해 달라고 요구한다. 새 사본의 경우에 표준 new 연산자 말고 맞춤 할당 전략을 사용해야 한다. 또 이미 설계에 포함된 벡터와 리스트 말고도 또다른 유형의 컨테이너를 사용할 수 있도록 만들어 달라고 요구한다. 아마도 데크가 좋을 것이다. 스택이면 더 좋다.

한 클래스 안에 모든 기능을 구현하고 가능한 모든 조합을 구현하려는 시도는 유연하지 못한 것이 확실하다. Storage 클래스는 조만간 뚱보가 되어 버린다. 이해하고 유지 관리하고 테스트하고 전개하기가 몹시 어려워진다.

단번에 모든 것을 갖춘 거대한 클래스가 이해하기도 어렵고 전개하기도 어려운 이유 중 하나는 설계가 잘 된 클래스는 제약을 강제해야 한다는 것이다. 클래스의 설계는 자체적으로 어떤 연산을 허용하면 안되며 그것을 위반하는 행위는 컴파일러가 탐지해야 한다. 프로그램이 탐지하게 되면 심각한 에러로 종료해 버리기 때문이다.

위의 요청에 대하여 생각해 보자. 벡터 데이터 저장소에 접근하는 인터페이스와 리스트 데이터 저장소에 접근하는 인터페이스를 둘 다 클래스가 갖추고 있다면 중복정의 operator[] 멤버로 벡터에 있는 원소에 접근할 가능성이 매우 높다. 그렇지만 리스트 데이터 저장소를 선택한다면 이 멤버는 구문적으로 존재하겠지만 의미구조적으로 무효이다. operator[]를 지원하지 않기 때문이다.

얼마 못 가 이 뚱보 Storage 클래스의 사용자는 함정에 빠질 것이다. 그 아래의 데이터 저장소로 리스트를 선택했음에도 불구하고 operator[]를 사용하는 함정에 빠질 가능성이 매우 높다. 컴파일러는 사용자를 혼란시키는 그 에러를 탐지할 수 없다. 이 에러는 프로그램이 실행중일 경우에만 나타나기 때문이다.

여전히 의문이 남는다. 위의 난관을 마주했을 때 프로그래머는 어떻게 처리해야 하는가? 정책을 소개할 시간이다.

23.4.1: Policy 클래스 - I

정책은 특별한 종류의 행위를 정의한다 (어떤 문맥에서는 규정한다). C++에서 정책(policy) 클래스는 클래스 인터페이스의 일정 부분을 정의한다. 내부 유형과 멤버 함수 그리고 데이터 멤버를 정의할 수도 있다.

앞 절에서 일련의 배당 전략을 사용하는 클래스를 생성하는 문제를 소개했다. 이 배당 전략은 모두 사용할 실제 데이터 유형에 따라 달라진다. 그래서 `템플릿 반영(template reflex)'을 도입해야 한다.

배당 정책은 아마도 템플릿 클래스로 정의해야 할 것이다. 당면한 데이터 유형에 적절한 배당 절차를 적용한다. 그런 배당 정책을 (std::vector, std::stack, 등등과 같은) 친숙한 STL 컨테이너들이 사용할 때 요구 조건을 만족시키기 위해 자체적 배당 전략은 아마도 std::allocator로부터 물려받아야 할 것이다. std::allocator 클래스 템플릿은 <memory> 헤더 파일에 선언되어 있다. 그리고 여기에 개발된 세 가지 배당 정책은 모두 std::allocator으로부터 상속받았다.

간략하게 인-클래스 구현을 사용하면 다음 배당 클래스를 정의할 수 있다.

위의 세 클래스는 정책을 정의한다. 이전 절에서 소개한 Storage의 사용자가 선택할 수 있다. 이 클래스 외에도 사용자는 배당 전략을 더 구현할 수 있다.

적절한 배당 전략을 Storage 클래스에 적용하려면 클래스 템플릿으로 설계해야 한다. 또 사용자가 데이터 유형을 지정할 수 있도록 템플릿 유형 매개변수가 필요하다.

물론 특정한 배당 전략을 지정할 때 데이터 유형도 함께 지정해야 한다. Storage 클래스는 하나는 데이터 유형에 대하여 또 하나는 배당 전략에 대하여 두 개의 템플릿 유형 매개변수를 가질 것이다.

    template <typename Data, typename Scheme>
    class Storage ...

Storage 클래스를 사용하려면 다음과 같이 작성한다. 예를 들어:

    Storage<string, NewAlloc<string>> storage;
이런 식으로 Storage를 사용하는 것은 상당히 복잡하고 잠재적으로 에러를 일으킬 가능성이 매우 높다. 사용자가 데이터 유형을 두 번이나 지정해야 하기 때문이다. 대신에 배당 전략은 새 유형의 템플릿 매개변수를 사용하여 지정해야 한다. 사용자는 배당 전략에 필요한 데이터 유형을 지정할 필요가 없다. 새로운 이 템플릿 매개변수를 (유명한 템플릿 유형 매개변수템플릿 비-유형 매개변수와 더불어) 템플릿 템플릿 매개변수라고 부른다.

C++14 표준부터 class 키워드는 템플릿 템플릿 매개변수의 구문적 형태에서 더 이상 필수가 아니다. 앞으로는 typename 키워드도 사용할 수 있다

23.4.2: Policy 클래스 - II: 템플릿 템플릿 매개변수

클래스 템플릿을 템플릿 매개변수로 지정할 수 있다. 그러면 기존의 클래스 템플릿에 정책이라고 부르는 행위를 추가할 수 있다.

Storage 클래스에 대하여 유형(type) 배당이 아니라 정책을 배당하기 위해 클래스 템플릿 헤더를 변경해 보자. 다음과 같이 정의를 시작한다.

    template <typename Data, template <typename> class Policy>
    class Storage...
두 번째 템플릿 매개변수는 처음 소개하는 템플릿 템플릿 매개변수이다. 다음과 같은 요소를 가진다. 정책 클래스는 고려중인 클래스의 필수 부분인 경우가 많다. 이 때문에 정책 클래스를 바탕 클래스로 배치한다. 예제에 PolicyStorage의 바탕 클래스로 사용한다.

정책은 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는 리스트로 구현되었으므로 벡터 인터페이스가 아니라 리스트 인터페이스를 제공한다.

23.4.2.1: Policy 클래스의 소멸자

이전 절에서 정책 클래스를 바탕 클래스로 사용했다. 이 때문에 흥미로운 결과가 관찰된다. 정책 클래스는 파생 클래스의 바탕 클래스로 기여할 수 있다. 정책 클래스가 바탕 클래스이므로 정책 클래스에 대한 참조나 포인터를 사용하면 파생 클래스를 가리킬 수 있다.

이런 상황은 합법적이기는 하지만 여러가지 이유 때문에 피하는 것이 좋다.

이런 단점들을 피하기 위해 좋은 관례는 정책 클래스에 대한 참조나 포인터를 사용하여 파생 클래스 객체를 참조하거나 가리키지 못하도록 방지하는 것이다. 이것은 정책 클래스에 비-가상 보호 소멸자를 제공하면 해결된다. 비-가상 소멸자는 수행성능의 저하가 없고 그의 소멸자는 보호되므로 사용자는 정책 클래스에 대한 참조나 포인터를 사용하여 정책 클래스로부터 파생된 클래스를 참조할 수 없다.

23.4.3: 정책으로 구조 정의하기

정책 클래스는 구조를 정의하는 것이 아니라 행위를 정의한다. 정책 클래스는 클래스의 어떤 행위를 매개변수화하기 위해 사용된다. 그렇지만 정책마다 데이터 멤버를 다르게 요구할 수 있다. 이런 데이터 멤버는 정책 클래스가 정의할 수 있다. 그러므로 정책 클래스는 행위와 구조를 모두 정의할 수 있다.

정의가 잘된 인터페이스를 제공함으로써 정책 클래스로부터 파생된 클래스는 다른 구조의 정책 클래스를 활용하여 멤버 특정화를 정의할 수 있다. 예를 들어 평범한 포인터-기반의 정책 클래스는 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::pairfirstsecond 데이터 멤버를 이용한다. 이 절 마지막 예제 참고).
    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 객체의 데이터 멤버들을 정의할 뿐이다.

23.5: 템플릿 별칭

함수와 클래스 템플릿 외에도 C++는 템플릿을 사용하여 유형 집합에 대하여 별칭을 정의하기도 한다. 이것을 템플릿 별칭이라고 부른다. 템플릿 별칭은 특정화할 수 있다. 템플릿 별칭의 이름은 유형 이름이다.

템플릿 별칭을 템플릿 템플릿 매개변에 건네는 인자로 사용할 수 있다. 이렇게 하면 템플릿 템플릿 매개변수를 사용할 때 마주하기도 하는 `예상치 못한 기본 매개변수(unexpected default parameters)' 에러를 피할 수 있다. 예를 들어 template <typename> class Container 템플릿을 정의하는 것은 좋지만 벡터나 집합 같은 컨테이너를 템플릿 인자로 지정하는 것은 불가능하다. 벡터와 집합도 자신의 배당 정책을 지정하는 두 번째 템플릿 매개변수를 정의하기 때문이다.

템플릿 별칭은 using 선언을 사용하여 정의한다. 기존의 (부분적으로 또는 완전하게 특정화된) 템플릿 유형에 별칭을 지정한다. 다음 예제에서 Vectorvector에 대한 별칭으로 정의된다.

    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::vectortemplate <typename> class Container> 매개변수에 부합하지 않기 때문이다. template <typename, typename> class Container> 템플릿 템플릿 매개변수를 요구한다.

그렇지만 Vector 템플릿의 별칭은 한 개의 유형 매개변수를 가진 템플릿으로 정의된다. 그리고 벡터의 기본 배당자를 사용한다. 결론적으로 VectorGeneric으로 건네면 잘 작동한다.

    Generic<int, Vector> giv;       // OK
    Generic<int, std::vector> err;  // 컴파일되지 않는다. 두 번째 인자가 일치하지 않기 때문이다.

템플릿 별칭의 작은 도움으로 Genericmap과 같이 완전히 다른 종류의 컨테이너를 사용하는 것도 가능하다.

    template <typename Type>
    using MapString = std::map<Type, std::string>;

    Generic<int, MapString> gim;    // map<int, string> 사용

23.6: 유형속성(Trait) 클래스

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';

23.6.1: 클래스 유형인지 아닌지 구별하기

이전 절에서 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) };
이 표현식은 여러 흥미로운 사실들이 연루되어 있다. 구체화를 전혀 요구하지 않으면서도 유형속성 클래스는 이제 템플릿 유형 인자가 클래스 유형인지 아닌지 그 질문에 대답한다. 멋지다!

23.6.2: 유형속성 정보(type_traits)

C++는 유형의 특징을 식별하고 변경하는 편의기능이 있다. 이런 편의기능을 사용하기 전에 <type_traits> 헤더를 포함해야 한다.

type_traits 클래스가 제공하는 모든 편의기능은 std 이름공간에 정의되어 있다 (아래 예제에서는 생략됨). 그래서 프로그래머는 유형과 값의 다양한 특징을 결정할 수 있다.

유형속성을 기술하다 보면 다음 개념들을 만난다.

유형-조건(type-condition)을 유형에 적용할 때 그 유형은 완전한 유형이나 void 또는 크기를 모르는 배열이어야 한다.

다음의 유형속성(traits)이 제공된다.

23.6.3: std::error_code로부터 클래스 상속받기

10.9.1항std::error_code 클래스를 소개했다. 생성자 중 하나가 ErrorCodeEnum를 인자로 받는다. 열거체를 손수 정의하여 ErrorCodeEnum으로 `승격시킬 수 있다'. 그러면 error_code 클래스와 그 비슷한 클래스들이 사용할 수 있다.

(errno 값 같은) 표준 에러 코드는 또는 enum class Errc으로 정의된 값들은 stat(2) 같은 낮은 수준의 시스템 함수가 사용한다. 여러분이 만든 함수나 클래스에서 만나는 에러에 사용하기에는 적합하지 않을 수 있다. 예를 들어 상호대화 계산기를 설계할 때 사용자가 입력하는 표현식에 관련하여 여러 에러를 만날 수 있다. 그렇다면 여러분 만의 ErrorCodeEnum을 설계하고 싶을 것이다. 그러나 system_error 예외를 사용하는 클래스의 조직은 몹시 복잡하다. 개발자마다 손수 정의한 열거체와 클래스를 사용하면 상황은 더 악화된다. 에러 조건을 열거체에 나열하고 싶지만 프로그램이 라이브러리에 링크되어 있다면 열거체는 유지관리하기가 어렵다. 그 라이브러에 다른 개발자들이 따로 열거체를 정의하고 있기 때문이다. 새로운 에러 조건을 사용하고 싶겠지만 그에 맞게 구현을 갱신해야 하는 것은 별로 마음에 들지 않는다.

이 항과 다음 항에서 개발하는 접근법은 (여전히 복잡하기는 해도...) 더 유연한 코드를 결과로 내어줄 것이다. 먼저 두 개의 ErrorCodeEnum을 개발한다. 이를 시발점으로 하여 관련 에러 값을 정의한다. error_category 클래스로부터 상속받아 부합하는 클래스를 정의함으로써 관련된 이 값들을 더욱 개발한다. 에러 조건을 사용하여 에러 값을 에러의 총칭 원인에 연관짓는다 (14.9절). 에러 조건은 이미 (다른 개발자들에 의하여) 정의되어 있을 수 있으므로 어떻게 여러 에러 조건이 하나의 프로그램에 유연하게 조합되어 들어갈 수 있는지 알아내는 것은 그 자체로 재미있는 퍼즐이다. 이 퍼즐은 ErrorCondition 클래스를 정의하면 해결된다. 이 클래스는 모든 에러 조건을 관리할 수 있다.

아래에 사용된 예제는 비행 시연에 초점을 둔다. 비행 시연을 할 때 여러 에러를 만난다 (예, 내비게이션 신호가 범위를 벗어난다). 조종석 안 시스템에 계산기가 있다. 여기에서 너무 구체적인 에러가 나타날 수도 있다. 존재하지 않는 함수를 요구할 가능성이 있다. 또는 복잡한 표현식의 괄호가 짝이 맞지 않을 수도 있다. 앞의 에러는 특정한 계산기에 관련된 에러이지만 뒤의 에러는 사용자의 엉터리 입력에 관련된 에러이다. 그리하여 크게 세 가지 에러 범주로 구분된다. 시연기 에러와 계산기 에러 그리고 사용자 에러로 구분된다.

23.6.4: std::error_category 클래스로부터 파생시키기

이전 항에서 개발한 ErrorCodeEnumerror_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;
    };

독자적으로 에러 범주 클래스를 정의할 준비가 되었다. 범주 클래스를 정의하기 위하여 다음 단계를 밟는다.

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
    */

다음 항에 에러 조건을 정의하고 사용하는 방법을 더 자세하게 다룬다.

23.6.5: std::error_condition 클래스로부터 파생시키기

그래서 우리의 비행 시연 프로그램은 두 개의 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_codeerror_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를 내부적으로 사용한다. CalculatorCategoryCalculatorError 열거체에 연관되는 것과 무척 비슷하다. 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
    */

23.7: `강력 보장'을 제공하면 `noexcept' 사용하기

강력 보장을 얻으려고 시도하는 동안 예외를 던지면 함수의 대응은 두 갈래가 된다.

첫 단계의 조치는 (예를 들어 소스의 값을 (임시의) 목적지에 할당하기 위하여) 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를 사용할지 말지 결정한다.

23.8: 클래스 유형 변환 심화 연구

23.8.1: 유형을 다른 유형으로 변환

클래스 템플릿은 부분적으로 특정화가 가능하지만 함수 템플릿은 그럴 수 없다. 이 때문에 짜증스러운 경우가 있다. 함수 템플릿이 있다고 가정하자. 단항 연산자를 구현하고 이 연산자는 transform 총칭 알고리즘에 사용할 수 있다 (19.1.63항):
    template <typename Return, typename Argument>
    Return chop(Argument const &arg)
    {
        return Return(arg);
    }
Returnstd::string이 반환될 경우에 위의 구현을 사용하면 안된다고 가정하자. 대신에 std::string이라면 두 번째 인자로 언제나 1을 건네야 한다. ArgumentC++ 문자열이라면 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';
템플릿 함수는 부분적 특정화를 지원하지 않는다. 그러나 중복정의는 가능하다. 매개변수에 따라 다르게 더미 유형의 인자를 가지고 중복정의를 제공하고 그리고 그 더미 유형 인자를 요구하지 않는 중복정의 함수로부터 이 중복정의를 호출함으로써 클래스 템플릿의 부분적 특정화와 같은 상황을 실현할 수 있다.

23.8.2: 빈 유형

구조체가 유용할 때가 있다 (23.9절). 마치 NTB-문자열에서 마지막이 0-바이트로 끝나는 유형처럼 사용할 수 있다. 그냥 단순히 다음과 같이 선언하면 된다.
    struct NullType
    {};

23.8.3: 유형 변환의 가능성

어떤 상황에서 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 유형이고 TU로 변환할 수 있으면 test의 반환 유형은 Convertible이다. 그렇지 않으면 NotConvertible이 반환된다.

이 상황은 23.6.1항에서 마주했던 상황과 유사함을 확실히 보여준다. 거기에서 isClass의 값은 컴파일 시간에 결정해야 했다. 여기에서 두 가지 관련 문제를 해결해야 한다.

첫 번째 문제는 T를 전혀 정의할 필요가 없다는 사실을 깨닫으면 해결된다. 어쨌거나, 의도는 컴파일 시간에 유형을 변환할 수 있는지 없는지 알아내는 것이다. T 값이나 객체를 정의하는 것이 의도는 아니다. 객체를 정의하는 것은 컴파일 시간이 아니라 실행 시간의 문제이다.

간단하게 T를 돌려주는 함수를 선언함으로써 어디엔가 T가 있다고 간주하라고 컴파일러에게 알릴 수 있다.

    T makeT();
이 신비한 함수는 T 객체가 튀어 나온다고 컴파일러를 속이는 마법의 힘이 있다. 그렇지만 우리의 필요에 실제로 맞추려면 먼저 이 함수를 조금 변경할 필요가 있다. 어떤 이유로든 T가 어쩌다가 배열이면 컴파일러는 T makeT() 때문에 질식사할 것이다. 함수는 배열을 돌려줄 수 없기 때문이다. 그렇지만 이것은 쉽게 해결된다. 함수는 배열에 대한 참조를 돌려줄 수 있기 때문이다. 그래서 위의 선언을 다음과 같이 변경한다.
    T const &makeT();

다음 코드와 같이 T const &test에 건넨다.

    test(makeT())
이제 컴파일러는 testT const & 인자로 호출된다는 것을 알기 때문에 실제로 변환이 가능하면 그의 반환 값을 Convertible하다고 결정하고 그렇지 않으면 NotConvertible하다고 결정한다. 즉, 컴파일러는 test(...) 함수를 선택한다.

ConvertibleNotConvertible으로부터 구분하는 두 번째 문제는 정확하게 23.6.1항에서 isClass를 결정하는 것과 같은 방식으로 해결된다. 다시 말해 크기를 다르게 만들어서 해결한다. 그러면 다음 표현식은 TU으로부터 변경할 수 있는지 없는지 결정한다.

    isConvertible = sizeof(test(makeT())) == sizeof(Convertible);
charConvertible에 그리고 Char2NotConvertible에 사용함으로써 구별할 수 있다 (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";

23.8.3.1: 상속 결정하기

이제 유형 변환을 결정할 수 있으므로 BaseDerived 유형의 (공개) 바탕 클래스인지 아닌지 쉽게 결정할 수 있다.

상속은 (상수) 포인터의 변환 가능성을 조사함으로써 결정된다. 다음과 같은 경우 Derived const *Base const *로 변환될 수 있다.

마지막 변환은 사용되지 않는다고 간주하면 상속은 다음 LBaseRDerived 유형속성 클래스를 사용하여 결정할 수 있다. LBaseRDerivedyes 열거 값을 제공한다. 이 값은 왼쪽 유형이 오른쪽 유형의 바탕 클래스이고 두 유형이 다르면 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";

23.9: TypeList 템플릿 처리 방법 연구

이 절은 두 가지 목적에 기여한다. 템플릿 메타 프로그래밍의 다양한 테크닉을 예시한다. 여러분만의 템플릿을 개발할 때 영감을 불어 넣어 줄 원천이 되어 줄 것이다. 이런 테크닉이 제공하는 파워를 구체적으로 보여주겠다.

이 절 자체는 안드레이 알렉산드레스쿠(Andrei Alexandrescu)의 책 Modern C++ design(2001)에 영감을 받았다. 그러나 우리는 가변 템플릿을 사용한 점에서 다른데, 그가 책을 쓸 때는 아직 없던 기능이다. 그렇지만 그가 사용한 알고리즘은 가변 템플릿을 사용할 때에도 여전히 유용하다.

C++터플을 제공한다. 터플로 여러 유형의 값을 저장하고 열람할 수 있다. 여기에서는 유형의 처리에만 초점을 둔다. 간단한 TypeList 구조체를 다음 항에 연구 과제로 사용하겠다. 다음은 그의 정의이다.

    template <typename ...Types>
    struct TypeList
    {
        TypeList(TypeList const &) = delete;
        enum { size = sizeof ...(Types) };
    };

TypeList에 얼마든지 유형을 저장할 수 있다. 다음은 charshort 그리고 int 유형 세 가지를 TypeList에 저장하는 예이다.

    TypeList<char, short, int>

23.9.1: TypeList의 길이

매개변수 팩의 유형의 갯수는 sizeof 연산자를 사용하여 얻을 수 있으므로 TypeList에 지정된 유형의 갯수를 손쉽게 얻을 수 있다 (22.5절). 예를 들어 다음 서술문은 값 3을 보여준다.
    std::cout << TypeList<int, char, bool>::size << '\n';

그렇지만 sizeof 연산자를 사용할 수 없으면 TypeList에 지정된 유형의 갯수가 어떻게 결정되는지 알아 보자.

TypeList에 지정된 유형의 갯수를 얻으려면 다음 알고리즘이 사용된다.

이 알고리즘은 재귀를 사용하여 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 };
    };

23.9.2: TypeList 검색하기

알고리즘을 사용하여 특정한 유형이 (즉, 아래 SearchType이) 주어진 TypeList에 존재하는지 결정한다. (SearchTypeTypeList의 원소가 아니면) `index'를 -1로 정의하고 아니면 `index'를 TypeList에서 첫 번째 나타나는 원소의 인덱스로 정의한다. 다음 알고리즘이 사용된다. 가변 템플릿 구조체 매개변수 팩을 기대하는 ListSearch를 사용하여 알고리즘을 구현한다.
    template <typename ...Types>
    struct ListSearch
    {
        ListSearch(ListSearch const &) = delete;
    };

특정화는 알고리즘에 언급된 대안들을 처리한다.

다음은 ListSearch를 어떻게 사용할 수 있는지 보여주는 예이다.

    std::cout <<
        ListSearch<char, TypeList<int, char, bool>>::index << "\n" <<
        ListSearch<float, TypeList<int, char, bool>>::index << "\n";

23.9.3: TypeList로부터 선택하기

TypeList의 특정 유형의 인덱스에 전도 연산을 하면 주어진 인덱스에 있는 유형을 열람할 수 있다. 이 전도 연산이 이 절의 주제이다.

알고리즘은 TypeAt 구조체를 사용하여 구현된다. TypeAttypedef를 사용하여 주어진 인덱스에 부합하는 유형을 정의한다. 그러나 인덱스는 경계를 넘어설 수도 있다. 그 경우 다음의 선택이 있다.

첫째 대안을 아래에 구현한다. 다른 대안은 구현하기에 어렵지 않으며 독자 여러분에게 연습문제로 남긴다. 다음은 TypeAt이 작동하는 방식이다. 다음은 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";

23.9.4: TypeList의 앞뒤에 추가하기

TypeList의 앞과 뒤에 쉽게 추가할 수 있다. 재귀적인 템플릿 메타 프로그래밍을 요구하지 않는다. AppendPrefix 가변 템플릿 구조체 두 개 그리고 두 개의 특정화이면 모든 일이 해결된다.

다음은 두 개의 가변 템플릿 구조체의 선언이다.

    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;
    };

23.9.5: TypeList으로부터 유형 제거하기

TypeList으로부터 유형을 제거하는 것도 가능하다. 역시, 여러 가능성이 있다. 각 가능성마다 서로 다른 알고리즘이 탄생한다. 당연히 TypeList로부터 유형을 지우는 다른 방법들이 있다. 결국 어떤 방법을 구현할 것인가는 환경에 따라 달라진다. 모든 알고리즘을 구현할 수는 없겠지만 템플릿 메타 프로그래밍은 매우 강력하다. 이제 위에 언급된 알고리즘들을 다음 목에서 개발해 보자.

23.9.5.1: 처음 나타나는 유형을 삭제하기

처음 나타나는 EraseTypeTypeList으로부터 제거하기 위해 재귀 알고리즘을 다시 사용한다. 템플릿 메타 프로그램은 총칭 Erase 구조체와 여러 특정화를 사용한다. 특정화는 List 유형을 정의한다. 삭제 후에 결과 TypeList를 담고 있다. 다음은 그 알고리즘이다. 다음은 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";

23.9.5.2: 인덱스로 유형 삭제하기

인덱스로 유형을 TypeList으로부터 제거하기 위해 다시 재귀 템플릿 메타 프로그래밍을 사용한다. EraseIdxsize_t 인덱스 값 그리고 TypeList를 기대한다. idx번째 (0-기반) 유형이 제거된다. EraseIdxList 유형을 정의한다. 안에 결과 TypeList를 담는다. 다음은 그 알고리즘이다. 다음은 어떻게 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";

23.9.5.3: 유형 삭제하기

EraseType의 모든 유형을 TypeList으로부터 쉽게 삭제할 수 있다. 삭제 절차를 TypeList의 머리부터 TypeList의 꼬리까지 적용하면 된다.

다음은 그 알고리즘이다. Erase의 알고리즘과 순서를 약간 다르게 기술한다.

다음은 어떻게 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";

23.9.5.4: 중복 없애기

TypeList으로부터 중복을 모두 없애려면 TypeList의 첫 원소를 TypeList의 꼬리로부터 제거하고 그 절차를 TypeList의 꼬리에 재귀적으로 적용해야 한다. 아래에 보여주는 알고리즘은 단순히 TypeList를 기대한다.

다음은 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";

23.10: TypeList 사용하기

이전 절에 TypeList의 정의와 특징을 소개했다. 대부분의 C++ 프로그래머는 TypeList를 신나고 흥미로운 지적 모험으로 생각한다. 재귀적 프로그래밍의 분야에서 기술을 연마할 기회로 생각한다.

그러나 TypeList에는 단순한 지적 모험보다 더 많은 것이 있다. 이 장의 마지막 절에 다음 주제를 다룬다.

역시 이 절에서 연구하는 재료의 많은 부분을 알렉산드레스쿠(Alexandrescu (2001))의 책에서 영감을 얻었다.

23.10.1: Wrap 클래스 템플릿과 Multi 클래스 템플릿

템플릿 메타 프로그래밍의 개념을 보여주기 위하여 이제 Multi 템플릿 클래스를 개발해 보자. MultiPolicy 템플릿 템플릿 매개변수로부터 새 클래스를 생성한다. 이 클래스에 데이터 저장 정책과 마침내 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)...)
        {}
    };

불행하게도 지금까지 기술한 디자인은 몇 가지 결함이 있다.

바탕 클래스 유형의 중복 문제를 돌아가는 길이 있다. 바탕 클래스로부터 직접 상속받는 대신에 이 바탕 클래스들을 먼저 유일한 유형 정의 클래스에 포장해 넣는다. 그러면 이 유일한 클래스들을 사용하여 상속 원리에 따라 바탕 클래스에 접근할 수 있다.

이 유일한 유형-정의 포장 클래스들은 단지 `실제' 바탕 클래스로부터 파생된 클래스일 뿐이므로, 바탕 클래스의 기능을 상속받는다 (그래서 제공한다). 유일한 유형 정의 포장 클래스는 이전에 정의한 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 값에 대하여 초기화 값을 기대한다. 그래서 MultiVectorint 그리고 string에 대하여 정의하면 생성자는 그에 부합하는 초기화 값을 받을 수 있다. 예를 들어,

    Multi<Vector, int, string> mvis({1, 2, 3}, {"one", "two", "three"});

23.10.2: MultiBase 클래스 템플릿

MultiBase 클래스 템플릿은 Multi 클래스의 바탕 클래스이다. 멀티 클래스는 건네어진 추가 유형을 사용하여 이 Policy 유형들을 생성한다. 이어서 바탕 클래스는 결국 Policy 유형의 리스트로부터 파생되는 클래스를 정의한다.

MultiBase 자체는 Policy의 개념이 없다. MultiBase에게 Policy는 그의 유형을 사용하여 클래스를 정의하는 그냥 간단한 템플릿 팩으로 구성된 것처럼 보일 뿐이다. PolicyTypes 템플릿 팩 말고도 MultiBasesize_t nr 비-유형 매개변수도 정의한다. 이 매개변수는 유일한 UWrap 유형을 생성한다. 다음은 MultiBase의 총칭 클래스 선언이다.

    template <size_t nr, typename ...PolicyTypes>
    struct MultiBase;

특정화가 가능한 모든 MultiBase의 요청을 두 가지 처리한다. 그 중 하나는 재귀 템플릿이다. 이 템플릿은 MultiBase의 템플릿 매개변수 팩의 첫 번째 유형을 처리하고 재귀적으로 자신을 이용해 나머지 유형을 처리한다. 두 번째 특정화는 템플릿 매개변수 팩이 고갈되면 요청되고 아무 일도 하지 않는다. 다음은 두 번째 특정화의 정의이다.

    template <size_t nr>
    struct MultiBase<nr>
    {};

재귀적으로 정의된 특정화는 흥미롭다. 다음의 일을 수행한다.

MultiBase 클래스 계통도의 조감은 그림 28에 보여준다.

Figure 28 is shown here.
그림 28: MultiBase 클래스 계통도

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)...)
        {}
    };

23.10.3: Support 템플릿

Multi 클래스 템플릿은 PlainTypesTypeList로 정의한다. 이 리스트는 매개변수 팩의 모든 유형을 담고 있다. UWrap 유형으로부터 파생된 각각의 MultiBase마다 Type 유형과 Base 유형을 정의한다. Type 유형은 UWrap 유형을 정의하는 데 사용되었던 정책 유형을 가리키고 Base 유형은 내포된 MultiBase 클래스의 유형을 나타낸다.

이 세가지 유형 정의 덕분에 Multi 객체를 생성한 유형에 접근할 수 있으며 또한 그런 유형의 값에도 접근할 수 있다.

typeAt은 순수한 메타 클래스 템플릿이다 (실행시간 실행 코드가 전혀 없다). typeAtsize_t idx 템플릿 인자를 기대한다. 이 인자는 Multi 유형과 더불어 Multi 유형의 객체 안에 있는 정책 유형의 인덱스를 지정한다. TypeMultiMultiBase<idx, ...> 바탕 클래스가 정의하는 Type으로 정의한다. 예를 들어:

    typeAt<0, Multi<Vector, int, double>>::Type // Type은 vector<int>이다.

클래스 템플릿 typeAt는 모든 일을 처리하는 내포된 클래스 템플릿 PolType를 (정의하고) 사용한다. PolType의 총칭 정의는 두 개의 템플릿 매개변수를 지정한다. index는 요청된 유형의 인덱스를 지정하고 typename은 MultiBase 유형 인자로 초기화된다. PolType의 재귀적 정의는 재귀적으로 인덱스 비-유형 매개변수를 줄여가면서 MultiBase의 상속 트리에서 다음 바탕 클래스를 재귀 호출에 건넨다. 마침내 PolTypeType 유형을 요청된 정책 유형으로 정의한다. 재귀 호출에 의해 정의되는 유형처럼 재귀 정의에 의해 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를 정의한다. AttypeAt과 비슷하게 구현되었지만 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> 유형처럼) 동일한 정책 유형 사이를 구별하기 위해서이다. UWrapnr 템플릿 인자로 유일하게 구별되므로 그리고 이것은 get에 건넨 인자가 숫자이므로 모호성을 손쉽게 방지할 수 있다.

23.10.4: Multi 사용하기

이제 Multi와 그의 지원 템플릿들을 개발하였으므로 어떻게 Multi를 사용할 수 있는가?

경고 한 마디를 덧붙인다. 개발된 클래스의 크기를 줄이기 위해 최소한으로 설계되었다. 예를 들어 get 함수 템플릿은 Multi const 객체와 함께 사용할 수 없으며 Multi 유형에 대하여 기본 생성자나 이동 생성자도 전혀 사용할 수 없다. Multi는 템플릿 메타 프로그래밍의 가능성을 보여주기 위하여 설계되었다. 그런 목적에 기여하기를 바라는 마음으로 Multi를 구현했다. 그렇다면 어떻게 사용하는가?

이 절은 예제에 주해를 붙였다. 주해는 모여서 일련의 서술문을 정의하게 되고 이를 main 함수의 몸체에 배치하면 그 결과로 작동하는 프로그램이 탄생할 수도 있다.

23.11: 표현식 템플릿

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회의 인덱스 표현식을 평가하면 될 것이다.

표현식 템플릿으로 정확하게 이런 종류의 최적화를 달성할 수 있다. 다음 항에서 그 설계와 구현을 살펴 보겠다.

23.11.1: 표현식 템플릿 설계하기

보셨다시피 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가 만들어진다. 그의 생성자 인자는 각각 onetwo에 대하여 방금 생성된 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의 인덱스 연산자는 그냥 lhsrhs 데이터 멤버에 상응하는 인덱스 표현식이 돌려주는 값들을 더할 뿐이다. 데이터 멤버가 벡터를 참조하면 상응하는 벡터 원소가 사용되어, 그것을 다른 데이터 멤버의 값에 더한다. 데이터 멤버 자체가 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+ 구현보다 더 효율적으로 덧셈을 평가한다. 표현식 템플릿을 사용하면 얻는 또다른 혜택은 괄호를 둘러 표현식을 사용할 때 따로 더 임시 벡터 객체를 만들지 않는다는 것이다.

23.11.2: 표현식 템플릿 구현하기

이 항은 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;
LHSRHS는 표현식 템플릿으로 처리되는 데이터 유형이거나 아니면 두 개의 다른 유형 이름을 요구하는 BinExpr이다. Operation은 표현식 템플릿이 수행하는 연산이다. 템플릿 템플릿 매개변수를 사용하면 BinExpr를 사용하여 단순한 덧셈 말고도 원하는 연산은 무엇이든 수행할 수 있다. 표준 산술 연산자에 대하여 std::plus와 같이 미리 정의된 함수 템플릿을 사용할 수 있다. 다른 연산자에 대해서는 따로 함수 템플릿을 정의할 수 있다.

BinExpr의 생성자는 lhsrhs에 대한 상수 참조를 초기화한다. 인-클래스 구현은 다음과 같다.

    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_lhsd_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 유형을 구체화한 것이다.

그래서 ObjTypevalue_type을 살펴볼 시간이다. 다음 절에 다루어 보겠다.

23.11.3: BasicType 유형속성 클래스와 클래스 순서

BinExpr 표현식 템플릿은 두 가지 유형을 미리 알아야 객체들을 구체화할 수 있다. 먼저 ObjType를 알아야 한다. 이것은 표현식 템플릿이 처리하는 객체의 유형이다. ObjType 객체에는 값이 담겨 있다. 그 다음 이 값들의 유형을 ObjType::value_type로 결정할 수 있어야 한다. 예를 들어 IntVect에 대하여 value_typeint이다.

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;
    };

BinExprObjType::value_type가 정의되어 있는 유형이어야 하므로 value_type이 자동으로 선택된다.

BinExprBasicType를 참조하고 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 멤버 함수
    };