제 21 장: 함수 템플릿과 변수 템플릿

C++ 프로그래머는 총칭 유형과 (추론된) 상수 값에 기반하여 일반적인 (또는 추상적인) 함수나 클래스를 완전하게 정의하고 사용할 수 있다. 추상 컨테이너에 관한 장(제 12장)과 STL에 관한 장(제 18장)에서 이미 템플릿 메커니즘을 배웠다.

템플릿 메커니즘을 이용하면 최종적으로 사용될 실제 유형에 신경쓰지 않고 클래스와 알고리즘을 지정할 수 있다. 컴파일러는 템플릿이 사용될 때마다 특정한 데이터 유형에 맞게 재단하여 코드를 생성한다. 이 코드는 템플릿 정의로부터 컴파일 시간에 생성된다. 템플릿으로부터 실제 코드를 생성하는 것을 구체화라고 부른다.

이 장은 템플릿의 구문적 특이성을 다룬다. 템플릿 유형 매개변수템플릿 비-유형 매개변수 그리고 함수 템플릿을 소개하고 (이 장과 제 24장에) 몇 가지 템플릿 예제를 제공한다. 템플릿 클래스는 제 22장에 다룬다.

C++가 제공하는 템플릿으로는 추상 컨테이너(제 12장)와 문자열(제 5장) 그리고 스트림(제 6장)과 총칭 알고리즘(제 19장)이 있다. 그래서 템플릿은 오늘날 C++에 핵심적인 역할을 한다. 그러므로 괴이한 특징으로 보면 안된다.

템플릿을 총칭 알고리즘처럼 대해야 한다. C++ 프로그래머라면 반드시 갖추어야 할 도구이다. 적극적으로 템플릿을 사용할 기회를 엿보아야 한다. 처음에는 템플릿이 좀 복잡해 보이고 거북할 수 있다. 그렇지만 시간이 지날수록 그의 강력한 힘과 혜택에 점점 더 고마움을 느끼게 될 것이다. 결국 템플릿을 사용해야 하는 이유를 알 수 있을 것이다. 어느새 여러분은 더 이상 평범한 함수와 클래스에 초점을 두지 않고 템플릿을 만들기 위해 열중하고 있는 자신을 보게 될 것이다.

이 장은 함수 템플릿을 소개하면서 시작한다. 필수 구문에 중점을 둔다. 이 장은 템플릿을 위한 토대를 마련한다. 이 토대 위에 다른 장들이 건설된다.

21.1: 함수 템플릿 정의하기

함수 템플릿의 정의는 함수의 정의와 아주 비슷하다. 함수 템플릿은 함수 머리부와 함수 몸체 그리고 반환 유형과 중복정의 정의 등등을 가진다. 그렇지만 정규 함수와 다르다. 함수 템플릿은 언제나 형식 유형을 사용한다. 형식 유형은 기존의 거의 모든 (클래스나 원시) 유형을 대신하는 유형이다. 간단한 예제를 하나 살펴보자. 다음 add 함수는 두 개의 Type 인자를 기대하고 그의 합을 돌려준다.
    Type add(Type const &lvalue, Type const &rvalue)
    {
        return lvalue + rvalue;
    }
위의 함수 정의가 얼마나 유사하게 그의 기술을 따르고 있는지 눈여겨보라. 두 개의 인자를 받고 그의 합을 돌려준다. 이제 이 함수를 int 값에 대하여 정의하면 어떤 일이 일어나는지 연구해 보자. 다음과 같이 작성한다면:
    int add(int const &lvalue, int const &rvalue)
    {
        return lvalue + rvalue;
    }
지금까지는 그런대로 좋다. 그렇지만 배정도 수를 두 개 더해야 한다면 이 함수를 중복정의해야 한다.
    double add(double const &lvalue, double const &rvalue)
    {
        return lvalue + rvalue;
    }
중복정의 버전의 갯수는 어디까지 정의해야 할지 끝이 없다. string이나 size_t 등등에 대하여 .... 중복정의 버전은 일반적으로 모든 유형에 대하여 operator+와 복사 생성자를 지원할 필요가 있다. 기본적으로 같은 함수인데도 불구하고 중복정의 버전이 모두 필요하다. 왜냐하면 강력하게 유형이 정의되는 C++의 본성 때문이다. 이 때문에 템플릿 메커니즘에 의존하지 않고서는 진정한 총칭 함수를 구성할 수 없다.

다행스럽게도 템플릿 함수의 중요한 부분을 이미 보았다. 최초 add 함수는 실제로 그런 함수의 구현이다. 물론 아직 완벽하게 템플릿으로 정의된 것은 아니다. 처음 add 함수를 컴파일러에게 주면 다음과 같은 에러 메시지를 보여줄 것이다.

    error: `Type' was not declared in this scope
    error: parse error before `const'

    에러: `Type'이 이 영역에 선언되어 있지 않습니다.
    에러: `const' 앞에서 해석할 수 없습니다.
당연하다. Type를 정의하지 않았기 때문이다. add 함수를 템플릿 정의로 완전히 바꾸면 에러가 사라진다. 이렇게 하기 위해 함수의 구현을 살펴보고 Type이 실제로 공식적인 유형 이름인지 결정한다. 그것을 대안 구현에 비교하면 Typeint로 바꾸어 첫 번째 구현을 얻을 수 있는지 아니면 Typedouble로 바꾸어 두 번째 구현을 얻을 수 있는지 확실하게 알 수 있다.

템플릿을 완벽하게 정의하면 Type 유형 이름의 이런 형식적인 본성을 만족시킬 수 있다. template 키워드를 사용하여 한 줄을 원래의 정의 앞에 둔다. 그러면 다음의 함수 템플릿 정의를 얻는다.

    template <typename Type>
    Type add(Type const &lvalue, Type const &rvalue)
    {
        return lvalue + rvalue;
    }

이 정의에서 다음과 같은 사실을 알 수 있다.

보통의 영역 규칙과 식별자 가시 규칙이 템플릿에 적용된다. 템플릿 정의의 영역 안에서 형식 유형 이름은 영역이 더 넓고 이름이 동일한 식별자를 덮어쓴다.

21.1.1: 템플릿 매개변수에 관한 연구

첫 번째 함수 템플릿을 설계해 보았다.
    template <typename Type>
    Type add(Type const &lvalue, Type const &rvalue)
    {
        return lvalue + rvalue;
    }

다시 add 템플릿의 매개변수를 살펴보자. Type이 아니라 Type const &를 지정하면 과도한 복사가 방지된다. 동시에 원시 유형의 값을 인자로 함수에 건넬 수 있다. 그래서 add(3, 4)를 호출하면 int{4}Type const &rvalue에 할당된다. 일반적으로 함수 매개변수는 Type const &로 지정해야 불필요한 복사를 방지할 수 있다. 컴파일러는 이 경우 `참조에 대한 참조'를 처리할 정도로 똑똑하다. 이것은 보통 C++ 언어가 지원하지 않는 것이다. 다음 main 함수를 연구해 보자 (여기와 다음의 간단한 예제에서 템플릿과 필요한 헤더들 그리고 이름공간 선언이 이미 제공되어 있다고 가정한다):

    int main()
    {
        size_t const &var = size_t{4};
        cout << add(var, var) << '\n';
    }
여기에서 var는 상수 size_t에 대한 참조이다. add 템플릿에 인자로 건네어지고 거기에서 Type const & 유형의 lvaluervaluesize_t const & 값으로 초기화한다. 컴파일러는 Typesize_t로 번역한다. 다른 방법으로서 Type const &가 아니라 Type &을 사용하여 매개변수들을 지정할 수도 있다. 이렇게 (비-상수로) 지정하면 단점이 있다. 임시 값들을 함수에 더 이상 건넬 수 없다. 그러므로 다음은 컴파일에 실패한다.
    int main()
    {
        cout << add(string{"a"}, string{"b"}) << '\n';
    }
여기에서 string &을 초기화하는 데 string const &는 사용할 수 없다. addType && 매개변수가 정의되어 있으면 위의 프로그램은 잘 컴파일되었을 것이다. 게다가 다음 예제는 올바르게 컴파일된다. 컴파일러가 Type이 분명히 string const라고 결정하기 때문이다.
    int main()
    {
        string const &s = string{"a"};
        cout << add(s, s) << '\n';
    }
이 예제로부터 무엇을 추론할 수 있을까? 다음 함수 템플릿을 두 번째 예로 연구해 보자:
    template <typename Type, size_t Size>
    Type sum(Type const (&array)[Size])
    {
        Type tp{};  // 주의: 기본 생성자는 존재해야 한다.

        for (size_t idx = 0; idx < Size; idx++)
            tp += array[idx];

        return tp;
    }
이 템플릿 정의는 다음의 새로운 개념과 특징을 이끌어 낸다.

클래스 정의처럼 템플릿 정의도 using을 포함하면 안된다. 지시어인가 아니면 선언인가. 지시어가 프로그래머의 의도를 거스르는 상황에 템플릿을 사용할 수 있다. 템플릿의 저자와 프로그래머가 서로 다른 using 지시어를 사용하면 모호성이나 기타 충돌이 일어날 수 있다 (예를 들어 cout 변수가 std 이름공간에 정의되거나 아니면 프로그래머 만의 이름공간에 정의되는 것). 대신에 템플릿 정의 안에서는 필요한 모든 이름공간 지정을 비롯하여 완전하게 자격을 갖춘 이름만 사용해야 한다.

21.1.2: Auto 그리고 decltype

3.3.6항에서 auto 키워드를 소개했다. decltype 키워드는 auto 키워드와 관련하여 조금 다른 행위를 보여준다. 이 항은 decltype를 집중 연구한다. 따로 더 지정할 필요가 없는 auto 키워드와 다르게 decltype 키워드 다음에는 언제나 반괄호 사이에 표현식이 따라온다 (예를 들어 decltype(variable)).

예를 들어 함수가 매개변수에 std::string const &text를 정의하고 있다고 가정해 보자. 함수 안에서 다음과 같이 두 개의 정의를 마주할 가능성이 있다.

    auto scratch1{text};
    decltype(text) scratch2 = text;
auto라면 컴파일러는 평범한 유형을 도출한다. 그래서 scratch1string이고, 복사 생성을 사용하여 `text'로 초기화한다.

이제 decltype을 연구해 보자. decltypetext의 유형을 결정한다. string const &가 그것으로서 scratch2의 유형으로 사용된다. string const &scratch2가 그 유형으로서, 문자열 text가 참조하는 것을 참조한다. 이것이 decltype의 표준 행위이다. 변수의 이름이 주어지면 그 변수의 유형으로 교체된다.

다른 방법으로서 decltype을 사용할 때 표현식을 지정할 수 있다. 물론 변수 자체가 표현식이지만, decltype의 문맥에서 `expression'은 단순히 평범한 변수 지정을 넘어서 더 복잡한 표현식을 정의한다. 변수의 이름을 반괄호 사이에 넣어 (variable)와 같이 단순하게 표현할 수도 있다.

표현식이 사용될 때 컴파일러는 표현식의 유형에 참조를 추가할 수 있는지 알아본다. 그렇다면, decltype(expression)은 그 lvalue 참조의 유형으로 교체된다 (그래서 expression-type &을 얻는다). 그렇지 않다면 decltype(expression)은 그 표현식의 평범한 유형으로 교체된다.

다음은 몇 가지 예이다.

    int *ptr;
    decltype(ptr) ref = ptr;
        // decltype의 인자는 평범한 변수이다. 그래서
        // ptr의 유형이 사용된다. int *ref = ptr 
        // decltype(ptr)은 int *로 교체된다.
        // (초기화-되지 않은/사용되지 않은 변수라는 경고 두 개).

    int *ptr;
    decltype( (ptr) ) ref = ptr;
        // decltype 함수의 인자는 표현식이다. 그래서
        // int *&ref = ptr이 사용된다.
        // decltype( (ptr))은 int *&로 교체된다.

    int value;
    decltype(value + value) var = value + value;
        // decltype의 인자는 표현식이다. 그래서 컴파일러는
       // decltype(...)을 int & (int &var = value + value)로 교체하려고 한다.
        // value + value는 임시적이고, var의 유형은 int &일 수 없기 때문이다.
        // 그래서 decltype(...)은 int로 교체된다.
        // (즉, value + value의 유형으로 교체된다) 

    string lines[20];
    decltype(lines[0]) ref = lines[6];
        // decltype의 인자는 표현식이다. 그래서
        // string &ref = lines[6] 표현식은 OK이다.
        // decltype(...)은 string &으로 교체된다.

    string &&strRef = string{};
    decltype(strRef) ref = std::move(strRef);
        // decltype의 인자는 평범한 변수이다. 그래서 그 변수의 유형이
        // 사용된다. string &&ref = std::move(strRef) 표현식은 OK이다.
        // decltype(...)은 strRef의 유형 즉, string &&으로 교체된다.

    decltype((strRef)) ref2 = strRef
        // decltype의 인자는 표현식이다. 그래서
        // string && &ref = strRef 표현식이 사용된다.
        // 자동으로 string &ref = strRef의 표현식이 되며 OK이다.
        // decltype은 string &으로 교체된다.

이것 말고도 decltype(auto)로 지정할 수 있다. 이 경우 decltype의 규칙이 auto에 적용된다. 그래서 객체의 기본 유형을 결정하는 데 auto가 사용된다. 그 다음, 표현식이 그저 변수에 불과하다면 그 표현식의 유형이 사용된다. 그렇지 않고 참조를 그 표현식에 추가할 수 있으면 decltype(auto)은 그 표현식의 유형에 대한 참조로 교체된다. 다음은 몇 가지 예이다.

    int *ptr;
    decltype(auto) ptr2 = ptr     
        // auto는 int *를 생산하며, ptr은 평범한 변수이다.
        // 그래서 decltype(auto)는 int *로 교체된다.

    int value;
    decltype(auto) ret = value + value;
        // auto는 int를 생산하며, value + value는 표현식이다.
        // 그래서 int &를 시도한다.
        // 그렇지만 value + value는 참조에 할당할 수 없다.
        // 그래서 표현식의 유형이 사용된다.
        // decltype(auto)는 int로 교체된다.

    string lines[20];
    decltype(auto) line = lines[0];
        // auto는 string을 생산하며, lines[0]은 표현식이다.
        // 그래서 string &을 시도한다.
        // string &line = lines[0]은 OK이다.
        // 그래서 decltype(auto)은 string &로 교체된다.

    decltype(auto) ref = string{} 
        // auto는 string를 생산하며, string{}은 표현식이다.
        // 그래서 string &를 시도한다.
        // 그렇지만 string &ref = string{}은 초기화가 올바르지 않다.
        // 그래서 string 자체가 사용된다.
        // decltype(auto)는 string으로 교체된다.

실전에서는 반환 유형을 정의하는 함수 템플릿에서 decltype(auto) 형태를 자주 만난다. 다음 구조체 정의를 한 번 살펴보자 (함수 템플릿을 사용하지는 않지만, decltype(auto)의 작동 방식을 보여준다):

    struct Data
    {
        vector<string> d_vs;
        string *d_val = new string[10];
    
        Data()
        :
            d_vs(1)
        {}
    
        auto autoFun() const
        {
            return d_val[0];
        }

        decltype(auto) declArr() const       
        {
            return d_val[0];
        }

        decltype(auto) declVect() const
        {
            return d_vs[0];
        }
    };

declArr의 반환 유형에는 const가 없는데 declVect의 반환 유형에는 있는지 궁금하다면 d_vsd_val을 살펴보자. 두 변수 모두 함수 안에서는 불변이다. 그러나 d_valconst *이기 때문에 가변 string 객체를 가리킨다. 그래서 declArrstring const &돌려줄 필요가 없지만 declVectstring const &돌려주어야 한다.

21.1.3: 늦게-지정되는 반환 유형

C++는 전통적으로 함수 템플릿에 반환 유형을 지정하기를 요구한다. 또는 반환 유형을 템플릿 유형 매개변수로 지정하기를 요구한다. 다음 함수를 연구해 보자:
    int add(int lhs, int rhs)
    {
        return lhs + rhs;
    }
위의 함수는 함수 템플릿으로 변환할 수 있다.
    template <typename Lhs, typename Rhs>
    Lhs add(Lhs lhs, Rhs rhs)
    {
        return lhs + rhs;
    }
불행하게도 함수 템플릿을 다음과 같이 호출하면
    add(3, 3.4)
의도했던 반환 유형은 double이다. int가 아니다. 이 문제는 템플릿 유형 매개변수를 추가하면 해결할 수 있다. 반환 유형을 명시적으로 지정해야 한다.
    add<double>(3, 3.4);
decltype을 사용하여 반환 유형을 정의하는 것은 작동하지 않는다 (3.3.5항). decltype이 사용될 때까지 lhsrhs가 컴파일러에게 알려지지 않기 때문이다. 그래서 추가 템플릿 유형 매개변수를 제거하려는 다음 시도는 컴파일에 실패한다.
    template <typename Lhs, typename Rhs>
    decltype(lhs + rhs) add(Lhs lhs, Rhs rhs)
    {
        return lhs + rhs;
    }

decltype을 기반으로 함수의 반환 유형을 정의하려면 조금 복잡해질 수 있다. 늦게-지정되는 반환 유형 구문을 사용하면 이 복잡성을 줄일 수 있다. 이 구문과 함께 decltype을 사용하여 함수의 반환 유형을 정의할 수 있다. 주로 함수 템플릿과 함께 사용된다. 그러나 보통의 (비-템플릿) 함수에도 사용할 수 있다.

    template <typename Lhs, typename Rhs>
    auto add(Lhs lhs, Rhs rhs) -> decltype(lhs + rhs)
    {
        return lhs + rhs;
    }
이 함수를 cout << add(3, 3.4)와 같은 서술문에 사용하면 결과 값은 6이 아니라 6.4가 될 것이다. 이것이 바로 의도한 결과이다. 늦게-지정되는 반환 유형이 어떻게 함수의 반환 유형 정의의 복잡성을 줄여 주는지 다음 예제를 연구해 보자:
    template <typename T, typename U>
    decltype((*(T*)0)+(*(U*)0)) add(T t, U u);
이해하기가 좀 어렵지 않은가? (*(T*)0)과 같은 표현식은 0을 정의한다. C 유형 변환을 유형 T를 가리키는 포인터처럼 사용한 다음에 그 포인터를 역참조한다. 그리하여 유형 T의 값을 산출한다 (값 자체가 변수로 존재하지 않음에도 불구하고 말이다). 마찬가지로 두 번째 용어도 decltype 표현식에 사용된다. 결과 유형은 그 다음에 add 함수의 반환 유형으로 사용된다. 늦게-지정되는 반환 유형을 사용하면 다음과 같은 코드를 얻는다.
    template <typename T, typename U>
    auto add(T t, U u) -> decltype(t+u);
대부분의 사람들이 읽기에 쉽다고 생각할 것이다.

decltype으로 지정된 표현식이라고 해서 반드시 lhsrhs를 사용할 필요는 없다. 다음 함수 정의는 lhs 대신에 lhs.length가 사용된다.

    template <typename Class, typename Rhs>
    auto  add(Class lhs, Rhs rhs) -> decltype(lhs.length() + rhs)
    {
        return lhs.length() + rhs;
    }
decltype이 컴파일되는 순간에 보이는 변수는 무엇이든 decltype 표현식 안에 사용할 수 있다. 그렇지만 현재는 멤버를 포인터로 통하여 멤버를 선택할 수 없다. 다음 코드는 멤버 함수의 주소를 add의 첫 인자로 지정하고 그의 반환 값을 사용하여 함수 템플릿의 반환 유형을 결정하는 것이 목적이다.
    std::string global{"hello world"};

    template <typename MEMBER, typename RHS>
    auto  add(MEMBER mem, RHS rhs) -> decltype((global.*mem)() + rhs)
    {
        return (global.*mem)() + rhs;
    }

    int main()
    {
        std::cout << add(&std::string::length, 3.4) << '\n';
        // 출력: 14.4
    }

위의 함수는 잘 컴파일되지만 현재 상태로는 사용할 수 없다. 다음 unimplemented: mangling dotstar_expr과 같은 컴파일 에러를 일으키기 때문이다 (cout << add(&string::length, 3.4)와 같은 서술문에 의해 야기된다).

21.2: 참조로 인자 건네기 (참조 포장자)

이 절에 논의하는 참조 포장자를 사용하기 전에 <functional> 헤더를 포함해야 한다.

함수 템플릿에 값이 아니라 참조가 건네어진다는 것을 컴파일러가 추론할 수 없는 상황이 존재한다. 다음 예제에서 outer 함수 템플릿은 int x를 첫 인자로 받는다. 컴파일러는 Typeint라고 추론한다.

    template <typename Type>
    void outer(Type t)
    {
        t.x();
    }
    void useInt()
    {
        int arg;
        outer(arg);
    }

물론 컴파일은 실패한다. 그러나 멋지게도 컴파일러는 추론한 유형을 보고한다.

    In function 'void outer(Type) [with Type = int]': ...

불행하게도 다음 예제에서 call 템플릿을 사용하면 같은 에러가 일어난다. call은 템플릿으로서 함수 하나와 그 함수에 건넬 값을 기대한다. 함수는 인자를 하나 기대하는데 인자 그 자체가 변경된다. 그 함수는 sqrtArg이다. 인자로 double 유형을 참조로 기대한다. 이 인자는 std::sqrt를 호출하여 변경된다.

    void sqrtArg(double &arg)
    {
        arg = sqrt(arg);
    }
    template<typename Fun, typename Arg>
    void call(Fun fun, Arg arg)
    {
        fun(arg);
        cout << "In call: arg = " << arg << '\n';
    }

double value = 3이라고 간주하면 call(sqrtArg, value)value를 변경하지 않는다. 컴파일러가 Argdouble이라고 추론하고 그러므로 value를 값으로 건네기 때문이다.

value 자체를 변경되도록 하려면 컴파일러에게 value를 참조로 건네야 한다고 알려야 한다. call의 템플릿 인자를 Arg &로 정의하는 것은 받아들이기 힘들다는 사실에 주목하라. 어떤 상황에서는 실제 인자를 변경하지 않는 것이 적절할 수도 있기 때문이다.

ref(arg)cref(arg) 참조 포장자를 사용할 수 있다. 이 함수들은 인자를 하나 받고 그것을 (상수) 참조 유형의 인자로 돌려준다. 실제로 value를 변경하기 위해 다음 main 함수에 보여주는 바와 같이 ref(value)를 사용하여 call에 건넬 수 있다.

    int main()
    {
        double value = 3;
        call(sqrtArg, value);
        cout << "Passed value, returns: " << value << '\n';

        call(sqrtArg, ref(value));
        cout << "Passed ref(value), returns: " << value << '\n';
    }
    /*
        출력:
            In call: arg = 1.73205
            Passed value, returns: 3
            In call: arg = 1.73205
            Passed ref(value), returns: 1.73205
    */

21.3: 지역 유형과 이름없는 유형을 템플릿 인자로 사용하기

유형은 이름이 있다. 그러나 익명의 유형도 정의할 수 있다.
    enum
    {
        V1,
        V2,
        V3
    };
여기에서 enum 이름없는 유형, 다시 말해 익명 유형을 정의한다.

함수 템플릿을 정의할 때 컴파일러는 템플릿 유형 매개변수의 유형을 인자로부터 추론한다.

    template <typename T>
    void fun(T &&t);

    fun(3);     // T는 int 유형이다.
    fun('c');   // T는 char 유형이다.
그렇지만 다음도 사용할 수 있다.
    fun(V1);    // T는 위의 열거 유형의 값이다.
fun 안에서 T 변수를 정의할 수 있다. 익명의 유형이라도 상관이 없다.
    template <typename T>
    void fun(T &&t)
    {
        T var(t);
    }

지역적으로 정의된 유형의 값이나 객체도 함수 템플릿에 인자로 건넬 수 있다. 예를 들어,

    void definer()
    {
        struct Local
        {
            double  dVar;
            int     iVar;
        };
        Local local;            // 지역 유형 사용

        fun(local);             // 좋다. T는 'Local' 유형이다.
    }

21.4: 템플릿 매개변수 추론

이 절은 컴파일러가 무엇을 가지고 템플릿 유형 매개변수의 실제 유형을 추론해 내는지 그 과정에 집중한다. 유형은 함수 템플릿을 호출할 때 추론된다. 이 과정을 템플릿 매개변수 추론이라고 부른다. 이미 보았듯이 컴파일러는 하나의 형식 템플릿 유형 매개변수를 다양한 실제 유형으로 교체할 수 있다. 그렇지만 무엇이든 생각대로 변환되는 것은 아니다. 특히 함수에 여러 템플릿 유형 매개변수가 같다면 컴파일러는 실제로 어떤 인자 유형을 받는지 결정하기가 대단히 어렵다.

컴파일러는 템플릿 유형 매개변수에 대하여 실제 유형을 추론할 때 실제로 사용된 인자의 유형만 고려한다. 이 과정에 지역 변수나 함수의 반환 값은 전혀 고려 대상이 아니다. 이것은 이해할 만하다. 함수가 호출될 때 컴파일러는 함수 템플릿의 인자 유형에 관해서만 알기 때문이다. 호출 시점에 함수의 지역 변수의 유형은 확실히 볼 수 없다. 함수의 반환 값은 실제로 사용이 안될 수도 있고 아니면 추론된 템플릿 유형 매개변수의 하위 (또는 상위) 유형의 변수에 할당될 수도 있다. 그래서 다음 예제에서 컴파일러는 fun() 함수를 호출할 수 없다. Type 템플릿 유형 매개변수에 대하여 실제 유형을 추론할 수 없기 때문이다.

    template <typename Type>
    Type fun()              // 절대 `fun()' 형태로 호출 불가능
    {
        return Type{};
    }
컴파일러는 `fun()'에 대한 호출을 처리하지는 못하지만 유형을 명시적으로 지정하여 fun() 함수를 호출하는 것은 가능하다. 예를 들어 fun<int>()fun을 호출해 int로 초기화한다. 물론 이것은 컴파일러의 인자 추론같지 않다.

일반적으로 함수에 동일한 템플릿 유형의 매개변수가 여럿 있을 때 실제 유형은 반드시 정확하게 같아야 한다. 그래서 다음과 같은 함수라면

    void binarg(double x, double y);
intdouble을 사용하여 호출할 수 있는데 int 인자로 호출하면 조용하게 double로 승격된다. 비슷하지만 함수 템플릿은 intdouble 인자를 사용하여 호출할 수 없다. 컴파일러 자체에서 Typedouble이어야 한다고 판단하고 intdouble로 승격하지 않기 때문이다.
    template <typename Type>
    void binarg(Type const &p1, Type const &p2)
    {}

    int main()
    {
        binarg(4, 4.5); // ?? 컴파일 불가: 실제 유형이 다르기 때문이다.
    }

그러면 컴파일러는 템플릿 유형 매개변수의 실제 유형을 추론할 때 어떻게 변형하는가? 매개변수 유형 변형에 대하여 딱 세 가지 그리고 함수 템플릿 비-유형 매개변수에 대하여 변형이 하나 있다. 이런 변형을 사용하여 실제 유형을 추론할 수 없으면 그 함수 템플릿은 고려 대상에서 제외된다. 컴파일러가 수행하는 변형 방법은 다음과 같다.

다양한 템플릿 매개변수 유형을 추론하여 변형하는 목적은 함수 인자를 함수 매개변수에 맞추어 보는 것이 아니다. 그게 아니라 템플릿 유형 매개변수의 실제 유형을 결정하는 것이 목적이다.

21.4.1: Lvalue 변형

lvalue 변형은 세 가지가 있다.

21.4.2: 자격(Qualification) 변형

자격 변형constvolatile 자격을 포인터에 추가한다. 이 변형은 함수 템플릿의 유형 매개변수가 명시적으로 const (또는 volatile)을 지정하지만 그 함수의 인자가 const 또는 volatile 개체가 아닐 경우에 적용된다. 이 경우 constvolatile을 컴파일러가 제공한다. 이어서 컴파일러는 템플릿 유형 매개변수를 추론한다. 예를 들어,
    template<typename Type>
    Type negate(Type const &value)
    {
        return -value;
    }
    int main()
    {
        int x = 5;
        x = negate(x);
    }
여기에서 함수 템플릿의 Type const &value 매개변수를 볼 수 있다. const Type에 대한 참조이다. 그렇지만 인자는 const int가 아니라 변경될 수 있는 int이다. 자격 변형을 적용하면 컴파일러는 constx의 유형에 추가한다. 그리고 int const x에 일치시킨다. 다음으로 이것을 다시 Type const &value에 일치시켜 컴파일러가 Typeint라고 추론할 수 있도록 만든다.

21.4.3: 바탕 클래스로 변형

클래스 템플릿의 생성22장의 주제이지만 이미 이전에 널리 클래스 템플릿을 사용해 보았다. 예를 들어 추상 컨테이너는 (제 12장) 클래스 템플릿으로 정의된다. 클래스 템플릿은 보통의 클래스처럼 클래스 계통도의 생성에 참여한다.

22.11절클래스 템플릿을 또다른 클래스 템플릿으로부터 어떻게 파생시킬 수 있는지 보여주었다.

클래스 템플릿 파생은 여전히 다룰 것이 있기 때문에 다음 연구는 좀 이른 감이 있다. 물론 독자는 가뿐하게 건너 뛰어 22.11절로 갔다가 다시 돌아와도 좋다.

이 절에서는 연구를 위해 Vector 클래스 템플릿을 std::vector로부터 파생시켰다고 가정한다. 게다가 벡터를 정렬하기 위해 obj 함수객체를 사용하여 다음 함수 템플릿을 생성했다고 간주한다.

    template <typename Type, typename Object>
    void sortVector(std::vector<Type> vect, Object const &obj)
    {
        sort(vect.begin(), vect.end(), obj);
    }
대소문자를 구분하지 않고 std::vector<string> 객체를 정렬하기 위해 class Caseless를 다음과 같이 생성할 수 있다.
    class CaseLess
    {
        public:
            bool operator()(std::string const &before,
                            std::string const &after) const
            {
                return strcasecmp(before.c_str(), after.c_str()) < 0;
            }
    };
이제 sortVector()를 사용하여 다양한 벡터를 정렬할 수 있다.
    int main()
    {
        std::vector<string> vs;
        std::vector<int> vi;

        sortVector(vs, CaseLess());
        sortVector(vi, less<int>());
    }
클래스 템플릿을 구체화된 바탕 클래스로 변형하면 sortVector 함수 템플릿은 이제 Vector 객체를 정렬하는 데에도 사용이 가능해진다. 예를 들어,
    int main()
    {
        Vector<string> vs;      // `std::vector' 대신에 `Vector'를 사용한다.
        Vector<int> vi;

        sortVector(vs, CaseLess());
        sortVector(vi, less<int>());
    }
이 예제는 인자로 VectorsortVector에 건넨다. 클래스 템플릿으로부터 파생된 바탕 클래스로 변형을 적용하여 컴파일러는 Vectorstd::vector로 간주한다. 덕분에 컴파일러는 템플릿 유형 매개변수를 추론할 수 있다. Vector vsstd::stringVector viint로 추론한다.

21.4.4: 템플릿 매개변수 추론 알고리즘

컴파일러는 다음 알고리즘을 사용하여 템플릿 유형 매개변수의 실제 유형을 추론한다.

21.4.5: 템플릿의 유형 축약

함수 템플릿에서 템플릿 인자의 유형과 템플릿 매개변수의 유형을 조합하면 흥미로운 축약을 보여준다. 예를 들어 템플릿 유형 매개변수를 rvalue 참조로 지정했지만 lvalue 참조 인자 유형이 제공되면 무슨 일이 일어나는가?

그런 경우에 컴파일러는 유형을 축약한다. 이중 동일 참조 유형은 간단하게 축약된다. 예를 들어 템플릿 매개변수 유형이 Type &&으로 지정되어 있고 실제 매개변수는 int &&라면 Typeint &&가 아니라 int로 추론된다.

상당히 직관적이다. 그러나 실제 유형이 int &라면 어떻게 될까? int & &&param와 같은 것은 전혀 있을 수 없으므로 컴파일러는 rvalue 참조를 제거함으로써 이중 참조를 축약한다. lvalue 참조는 유지한다. 다음 규칙이 적용된다.

  1. lvalue 참조 인자를 받는 템플릿 유형 매개변수에 (예를 들어 Type &에) 대한 lvalue 참조로 정의된 함수 템플릿 매개변수는 결과적으로 하나의 lvalue 참조가 된다.
  2. 종류에 상관없이 참조 인자를 받는 템플릿 유형 매개변수에 (예를 들어 Type &&에) 대한 rvalue 참조로 정의된 함수 템플릿 매개변수는 인자의 참조 유형을 사용한다.

예를 들어,

축약이 일어나는 구체적인 예제를 살펴보자. 다음 함수 템플릿을 연구해 보자. 함수 매개변수가 어떤 템플릿 유형 매개변수에 대한 rvalue 참조로 정의되어 있다.

    template <typename Type>
    void function(Type &&param)
    {
        callee(static_cast<Type &&>(param));
    }
이 상황에서 functionTP & 유형의 (lvalue) 인자를 가지고 호출될 때 템플릿 유형 매개변수 TypeTp &로 추론된다. 그러므로 Type &&paramTp &param으로 구체화되고 TypeTp가 된다. 그리고 rvalue 참조는 lvalue 참조로 교체된다.

마찬가지로 static_cast를 사용하여 callee가 호출될 때도 똑같이 축약이 일어난다. 그래서 Type &&paramTp &param에 작동한다. 그러므로 (축약을 사용하면) 정적 형변환도 역시 Tp &param 유형을 사용한다. param이 어쩌다가 유형이 Tp &&이면 정적 유형변환은 Tp &&param 유형을 사용한다.

이 특징 덕분에 유형을 바꿀 필요 없이 함수 인자를 내포 함수에 건넬 수 있다. lvalue는 그대로 lvalue이고 rvalue는 그대로 rvalue이다. 그러므로 이 특징은 완벽한 전달(perfect forwarding)이라고도 알려져 있다. 이에 관해서는 22.5.2항에 더 자세하게 연구한다. 완벽한 전달 덕분에 템플릿 저자는 함수 템플릿의 중복정의 버전을 여러 벌 정의하지 않아도 된다.

21.5: 함수 템플릿 선언하기

지금까지 함수 템플릿을 정의하기만 했다. 함수 템플릿 정의를 여러 소스 파일에 포함하면 다양한 영향이 있다. 심각한 것은 아니지만 알아야 할 가치가 있다. 그래서 어떤 문맥에서는 템플릿 정의를 요구하지 않는다. 프로그래머는 수 많은 소스 파일에 템플릿의 정의를 반복적으로 포함하는 대신에 템플릿을 선언하기로 선택할 수도 있다.

템플릿을 선언하면 컴파일러는 템플릿의 정의를 반복해서 처리할 필요가 없다. 템플릿 선언만으로는 구체화되지 않는다. 실제로 요구되는 구체화는 다른 곳에서 사용할 수 있어야 한다 (물론 일반적으로 이 사실은 선언에도 마찬가지로 유효하다). 보통 라이브러리에 저장되어 있는, 평범한 함수에서 마주치는 상황과 다르게 현재로는 템플릿을 라이브러리에 저장하는 것은 불가능하다 (물론 컴파일러가 미리 컴파일된 헤더 파일을 생성할 수는 있다). 결론적으로 템플릿 선언을 사용하면 프로그래머의 어깨 위에 큰 부담을 지우는 셈이다. 프로그래머는 요구된 실체가 존재하는지 확인할 의무가 있다. 아래에 이를 쉽게 확인할 수 있는 방법을 소개한다.

함수 템플릿을 선언하려면 그냥 함수의 몸체를 쌍반점으로 교체하면 된다. 이것은 평범한 함수를 선언하는 방식과 정확하게 똑 같다는 것을 주목하라. 그래서 이전에 정의된 add 함수 템플릿은 다음과 같이 간단하게 선언할 수 있다.

    template <typename Type>
    Type add(Type const &lvalue, Type const &rvalue);
이미 템플릿 선언을 만나 본 바 있다. ios 클래스와 그의 파생 클래스로부터 원소들의 실체화를 요구하지 않는 소스 파일에는 iosfwd 헤더를 포함해도 된다. 다음 선언을 컴파일하기 위해 stringistream 헤더를 포함할 필요는 없다.
    std::string getCsvLine(std::istream &in, char const *delim);
다음 한 줄이면 충분하다.
    #include <iosfwd>
iosfwd 헤더를 처리하려면 시간이 약간 더 걸린다. stringistream 헤더를 처리해야 하기 때문이다.

21.5.1: 구체화 선언

함수 템플릿을 정의하면 컴파일 속도와 링크 속도가 높아진다는 것을 어떻게 확신할 수 있을까? 프로그램이 최종적으로 링크될 때 요구한 함수 템플릿의 실체가 존재한다는 것을 어떻게 확신할 수 있을까?

이를 위하여 이른바 명시적인 구체화 선언이라는 템플릿 선언의 변형을 사용할 수 있다. 명시적인 구체화 선언은 다음 요소들로 구성된다.

이것은 선언이지만 컴파일러는 함수 템플릿의 특별한 변형을 구체화해 달라는 요청으로 이해한다.

명시적으로 구체화를 선언하면 프로그램이 요구하는 템플릿 함수의 실체를 모두 파일 하나에 모을 수 있다. 이 파일은 보통의 소스 파일로서 템플릿 정의 헤더를 포함해야 하고 이어서 필요한 명시적인 구체화 선언을 지정해야 한다. 소스 파일이기 때문에 다른 소스에 포함되지 않는다. 그래서 필요한 헤더를 포함했다면 이름공간 using 지시어와 선언을 안전하게 사용할 수 있다.

다음은 이전의 add 함수 템플릿에 대하여 요구한 구체화를 보여주는 예이다. 각각 doubleint 그리고 std::string 유형에 대하여 구체화를 한다.

    #include "add.h"
    #include <string>
    using namespace std;

    template int add<int>(int const &lvalue, int const &rvalue);
    template double add<double>(double const &lvalue, double const &rvalue);
    template string add<string>(string const &lvalue, string const &rvalue);
혹시 프로그램이 요구한 구체화를 언급하는 것을 깜박 잊어 버리더라도 쉽게 보완할 수 있다. 빠진 구체화 선언을 위 리스트에 추가하기만 하면 된다. 파일을 다시 컴파일하고 링크하면 완성이다.

21.6: 함수 템플릿을 구체화하기

컴파일러가 정의를 읽기만 하면 코드가 되는 평범한 함수와 다르게 템플릿은 정의를 읽어도 구체화되지 않는다. 템플릿은 그저 때가 되면 컴파일러에게 특정한 코드를 만드는 법을 알려 주는 요리법일 뿐이다. 실제로 요리 책의 요리법과 많이 닮았다. 빵을 굽는 법을 다 읽었다고 해서 실제로 빵을 잘 구울 수 있는 것은 아니다.

그래서 함수 템플릿은 실제로 언제 구체화되는가? 컴파일러가 템플릿을 구체화하기로 결정하는 상황이 두 가지 있다.

컴파일러가 템플릿을 구체화하도록 촉발시키는 서술문의 위치를 그 템플릿의 구체화 시점이라고 부른다. 구체화 시점은 함수 템플릿의 코드에 심각한 영향을 미친다. 이 영향에 관해서는 다음 21.13절에 논의한다.

컴파일러가 템플릿 유형 매개변수를 언제나 명확하게 추론할 수 있는 것은 아니다. 컴파일러가 모호하다고 보고하면 그것은 프로그래머가 해결해야 한다. 다음 코드를 연구해 보자:

    #include <iostream>
    #include "add.h"

    size_t fun(int (*f)(int *p, size_t n));
    double fun(double (*f)(double *p, size_t n));

    int main()
    {
        std::cout << fun(add);
    }

이 작은 프로그램을 컴파일하면 컴파일러는 모호성을 해결할 수 없다고 불평한다. 후보 함수가 두 개이기 때문이다. fun의 중복정의 버전 각각에 대하여 add 함수를 구체화할 수 있기 때문이다.

    error: call of overloaded 'fun(<unknown type>)' is ambiguous
    note: candidates are: int fun(size_t (*)(int*, size_t))
    note:                 double fun(double (*)(double*, size_t))

    에러: 중복정의 'fun(<unknown type>)' 함수를 호출하기가 애매합니다.
    고지: 후보 함수: int fun(size_t (*)(int*, size_t))
    고지:            double fun(double (*)(double*, size_t))
물론 이런 상황은 피해야 한다. 함수 템플릿은 모호성이 없을 경우에만 구체화될 수 있다. 모호성은 컴파일러의 함수 선택 메커니즘에 여러 함수가 출현할 때 일어난다 (21.14절). 그 모호성을 해결하는 것은 프로그래머의 책임이다. 무딘 static_cast를 사용하여 해결할 수도 있다 (가능한 모든 대안 중에서 하나를 선택한다):
    #include <iostream>
    #include "add.h"

    int fun(int (*f)(int const &lvalue, int const &rvalue));
    double fun(double (*f)(double const &lvalue, double const &rvalue));

    int main()
    {
        std::cout << fun(
                        static_cast<int (*)(int const &, int const &)>(add)
                    );
    }

그러나 가능하면 유형을 강제로 변환하는 것은 피하는 것이 좋다. 그 방법은 다음 21.7절에 설명한다.

21.6.1: 구체화: `코드 비만' 없음

앞서 21.5절에 언급했듯이 링커는 최종 프로그램으로부터 템플릿의 동일한 실체를 제거한다. 실제 템플릿 유형 매개변수의 집합마다 따로따로 하나의 실체만 남긴다. 링커는 다음과 같이 행위한다. 이 작은 실험으로부터 얻은 결론은 실제로 링커는 동일한 템플릿 객체를 최종 프로그램으로부터 제거한다는 것이다. 게다가 단순히 템플릿을 선언한다고 해서 템플릿 객체가 되지는 않는다는 결론을 내릴 수 있다.

21.7: 명시적인 템플릿 유형 사용하기

이전 절에서 컴파일러가 템플릿을 구체화할 때 모호성을 맞이할 가능성이 있었다. 한 예제에 서로 다른 유형의 인자를 기대하는 중복정의 버전의 함수(fun)가 있었는데, 함수 템플릿을 구체화할 때 두 인자가 동일하게 제공되었기 때문에 모호성이 생겼다. 이런 모호성을 해결하는 직관적인 방법은 static_cast를 사용하는 것이다. 그러나 강제 유형변환은 되도록이면 피하는 것이 좋다.

함수 템플릿에서 정적 유형 변환은 실제로 피할 수 있다. 명시적인 템플릿 유형 인자를 사용하면 된다. 템플릿을 구체화할 때 사용해야 할 실제 유형에 관하여 컴파일러에게 알려 줄 수 있다. 이를 사용하려면 함수의 이름 다음에 실제 템플릿 유형 인자 리스트가 오고 그 다음에 또 함수의 인자 리스트가 올 수 있다. 실제 템플릿 인자 리스트에 언급된 실제 유형은 컴파일러가 사용하여 템플릿을 구체화할 때 어떤 유형을 사용해야 할지 `추론한다'. 다음은 이전 절에서 가져온 예제이다. 이제는 명시적인 템플릿 유형 인자를 사용한다.

    #include <iostream>
    #include "add.h"

    int fun(int (*f)(int const &lvalue, int const &rvalue));
    double fun(double (*f)(double const &lvalue, double const &rvalue));

    int main()
    {
        std::cout << fun(add<int>) << '\n';
    }

명시적인 템플릿 유형 인자는 어느 유형을 실제로 사용해야 할지 컴파일러가 탐지할 방법이 없는 상황에 사용할 수 있다. 예를 들어 21.4절에서 함수 템플릿 Type fun()을 정의했다. double 유형에 대하여 이 함수를 구체화하려면 fun<double>()으로 호출하면 된다.

21.8: 함수 템플릿 중복정의하기

add 템플릿을 다시 한 번 더 살펴보자. 이 템플릿은 두 개체의 합을 돌려주도록 설계되었다. 세 개의 개체의 합을 계산하고 싶다면 다음과 같이 작성할 수 있다.
    int main()
    {
        add(add(2, 3), 4);
    }
이것은 가끔씩 있는 상황에는 받아들일 만한 해결책이다. 그렇지만 개체 세 개를 자주 사용할 필요가 있다면 세 개의 인자를 기대하는 중복정의 버전의 add 함수가 유용한 함수가 될 것이다. 이 문제에 대한 간단한 해결책이 있다. 함수 템플릿을 중복정의할 수 있다.

함수 템플릿을 중복정의하려면 그냥 템플릿의 여러 정의를 헤더 파일에 배치하면 된다. add 함수에 대하여 이것은 다음과 같이 요약될 것이다.

    template <typename Type>
    Type add(Type const &lvalue, Type const &rvalue)
    {
        return lvalue + rvalue;
    }
    template <typename Type>
    Type add(Type const &lvalue, Type const &mvalue, Type const &rvalue)
    {
        return lvalue + mvalue + rvalue;
    }
중복정의 함수는 간단한 값의 관점에서 정의해야 할 필요가 없다. 다른 모든 중복정의 함수처럼, 유일한 집합의 함수 매개변수만 있으면 중복정의 함수 템플릿을 정의하기에 충분하다. 예를 들어 다음은 중복정의 버전이다. 벡터의 원소들의 합을 계산하는 데 사용할 수 있다.
    template <typename Type>
    Type add(std::vector<Type> const &vect)
    {
        return accumulate(vect.begin(), vect.end(), Type());
    }

함수 템플릿을 중복정의할 때 함수의 매개변수 리스트에 얽매일 필요가 없다. 템플릿 유형 매개변수 리스트 자체가 또 중복정의가 가능하다. add 템플릿의 마지막 정의로 vector를 첫 인자로 지정할 수 있다. 그러나 dequemap은 지정할 수 없다. 물론 그런 유형의 컨테이너에 대하여 중복정의 버전을 생성할 수 있다. 그러나 얼마나 더 멀리 가야 하는가?

더 좋은 접근법은 이런 컨테이너들의 공통적 특성을 살펴보는 것이다. 공통적 특성을 발견하면 거기에 근거하여 중복정의 함수 템플릿을 정의할 수 있다. 위에 언급한 컨테이너들의 공통적 특성 하나는 그들 모두 beginend 멤버를 지원하고 반복자를 돌려준다는 것이다.

이것을 이용하면 이런 멤버들을 지원해야 하는 컨테이너를 나타내는 템플릿 유형 매개변수를 정의할 수 있다. 그러나 순수하게 `컨테이너 유형'을 언급하고 있더라도 구체화된 데이터 유형이 무엇인지는 알 수 없다. 그래서 컨테이너의 데이터 유형을 나타내는 두 번째 템플릿 유형 매개변수가 필요하다. 그래야 템플릿 유형 매개변수 리스트를 중복정의할 수 있다. 다음은 결과로 나온 add 템플릿의 중복정의 버전이다.

    template <typename Container, typename Type>
    Type add(Container const &cont, Type const &init)
    {
        return std::accumulate(cont.begin(), cont.end(), init);
    }
init 매개변수를 매개변수 리스트에서 빼버려도 되는지 궁금하실 것이다. init이 종종 기본 초기화 값을 가지기 때문에 그 대답은 `된다'이다. 그러나 거기에는 복잡한 문제가 관련되어 있다. 다음과 같이 add 함수를 정의할 수 있다:
    template <typename Type, typename Container>
    Type add(Container const &cont)
    {
        return std::accumulate(cont.begin(), cont.end(), Type());
    }
그렇지만 템플릿 유형 매개변수의 순서가 바뀐 것에 주목하라. 컴파일러가 다음과 같은 호출에서 Type을 결정할 수 없기 때문에 꼭 필요하다.
    int x = add(vectorOfInts);
템플릿 유형 매개변수의 순서를 바꾸어서 Type을 맨 앞에 두면 첫 번째 템플릿 유형 매개변수에 명시적인 템플릿 유형 인자를 제공할 수 있다.
    int x = add<int>(vectorOfInts);
이 예제에서 vector<int> 인자를 제공했다. 컴파일러가 템플릿 유형 매개변수 Type을 결정하도록 하기 위해 왜 int를 명시적으로 지정해야 하는지 궁금할 것이다. 실제로는 그럴 필요가 없다. 세 번째 종류의 템플릿 매개변수가 존재한다. 템플릿 템플릿 매개변수가 그것으로서 컴파일러는 실제 컨테이너 인자로부터 직접 Type을 결정할 수 있다. 템플릿 템플릿 매개변수는 다음 23.4절에 논의한다.

21.8.1: 중복정의 함수 템플릿을 사용하는 예제

이 모든 중복정의 버전이 제자리에 자리를 잡으면 이제 컴파일러를 기동해 다음 함수를 컴파일할 수 있다.
    using namespace std;

    int main()
    {
        vector<int> v;

        add(3, 4);          // 1 (해설 참고)
        add(v);             // 2
        add(v, 0);          // 3
    }

두 개의 같은 그리고 두 개의 다른 템플릿 유형 매개변수에 대하여 add 함수 템플릿을 정의했으므로 두 개의 템플릿 유형 매개변수를 가진 add 함수 템플릿을 사용해 볼 가능성은 없어졌다.

21.8.2: 함수 템플릿을 중복정의할 때의 모호성에 관하여

또다른 add 함수 템플릿을 정의할 수는 있지만 이렇게 하면 모호성이 생긴다. 컴파일러가 유형이 서로 다른 함수 매개변수를 정의한 중복정의 버전 두 개 중에 어느 것을 사용해야 할지 결정할 수 없기 때문이다. 예를 들어 다음과 같이 정의하면:
    #include "add.h"

    template <typename T1, typename T2>
    T1 add(T1 const &lvalue, T2 const &rvalue)
    {
        return lvalue + rvalue;
    }
    int main()
    {
        add(3, 4.5);
    }
컴파일러는 다음과 같이 모호하다고 불평한다.
        error: call of overloaded `add(int, double)' is ambiguous
        error: candidates are: Type add(const Container&, const Type&)
                                    [with Container = int, Type = double]
        error:                 T1 add(const T1&, const T2&)
                                    [with T1 = int, T2 = double]
이제 세 개의 인자를 받는 중복정의 함수 템플릿을 떠올려 보자:
    template <typename Type>
    Type add(Type const &lvalue, Type const &mvalue, Type const &rvalue)
    {
        return lvalue + mvalue + rvalue;
    }
이 함수가 동등한 유형의 인자만 받는다는 것은 단점으로 간주할 수도 있다 ( int 세 개, double 세 개, 등등.). 이를 바로 잡기 위하여 중복정의 함수 템플릿을 또하나 정의한다. 이 번에는 유형에 상관없이 인자를 받는다. 이 함수 템플릿은 operator+가 함수의 실제 사용된 유형 사이에 정의되어 있지만 그 말고는 문제가 없어 보일 경우에만 사용할 수 있다. 다음은 유형을 가리지 않고 인자를 받는 중복정의 버전이다.
    template <typename Type1, typename Type2, typename Type3>
    Type1 add(Type1 const &lvalue, Type2 const &mvalue, Type3 const &rvalue)
    {
        return lvalue + mvalue + rvalue;
    }
이제 인자를 세 개 기대하는 위의 두 중복정의 함수 템플릿을 정의했으므로 다음과 같이 add를 호출해 보자:
    add(1, 2, 3);
여기에서 모호성을 기대할 수 있을까? 결국, 컴파일러는 Type == int이라고 추론하고서 앞의 함수를 선택할 수 있지만 Type1 == intType2 == int 그리고 Type3 == int라고 추론하고서 뒤의 함수를 선택할 수도 있다. 놀랍게도 컴파일러는 모호하다고 불평하지 않는다.

다음과 같은 이유 때문에 모호성이 보고되지 않는다. 특정화가 덜 된 또는 특정화가 더 된 템플릿 유형 매개변수를 사용하여 중복정의 템플릿 함수가 정의되어 있으면 (예를 들어 특정화가 덜 된 경우는 유형이 모두 다름 vs. 특정화가 더 된 경우는 유형이 모두 같음) 그러면 컴파일러는 가능하면 특정화가 더 된 함수를 선택한다.

제일 규칙: 구체화할 중복정의 함수 템플릿을 선택할 때 모호성을 방지하기 위하여 중복정의 함수 템플릿은 템플릿의 유형 인자를 유일하게 조합하여 지정하도록 허용해야 한다. 함수 템플릿의 유형 매개변수 리스트에서 템플릿 유형 매개변수의 순서는 중요하지 않다. 다음 함수 템플릿 중 하나를 구체화하려고 시도하면 모호성이 발생한다.

    template <typename T1, typename T2>
    void binarg(T1 const &first, T2 const &second)
    {}
    template <typename T1, typename T2>
    void binarg(T2 const &first, T1 const &second)
    {}
당연히 놀라울 것이 없다. 결국 템플릿 유형 매개변수는 그저 형식적인 이름에 불과하다. 그 이름만으로는 (T1이든 T2이든 또는 그 무엇이든) 구체적인 의미는 없는 것이다.

21.8.3: 중복정의 함수 템플릿을 선언하기

다른 모든 함수가 그런 것처럼 중복정의 함수를 선언할 수 있다. 평범하게 선언하거나 구체화 선언을 하면 된다. 명시적인 템플릿 인자 유형도 사용할 수 있다. 예를 들어,

21.9: 유형을 우회하기 위하여 템플릿 특정화하기

유형이 같은 두 개의 매개변수를 정의한 최초의 add 템플릿은 operator+와 복사 생성자를 지원하는 모든 유형에 대하여 잘 작동한다. 그렇지만 이 가정이 언제나 충족되는 것은 아니다. 예를 들어 char *에서 operator+이나 `복사 생성자'를 사용하는 것은 무의미하다. 컴파일러는 그 함수 템플릿을 구체화하려고 시도한다. 그러나 컴파일에 실패한다. 포인터에 대하여 operator+가 정의되어 있지 않기 때문이다.

이런 상황에 컴파일러는 템플릿 유형 매개변수를 해결할 수 있지만 표준 구현이 의미가 없으며 에러를 야기한다는 사실을 탐지할 수도 있다.

이 문제를 해결하기 위하여 템플릿의 명시적인 특정화를 정의할 수 있다. 템플릿의 명시적인 특정화는 특정한 실제 템플릿 유형 매개변수를 사용하여 총칭 정의가 이미 존재하는 함수 템플릿을 정의한다. 이전 절에서 보았듯이 컴파일러는 언제나 특정화가 덜 된 함수보다 더된 함수를 선호한다. 그래서 가능하면 템플릿의 명시적인 특정화가 선택된다.

템플릿의 명시적인 특정화는 템플릿 유형 매개변수(들)에 대하여 특정화를 제공한다. 그 특별한 유형은 일관성 있게 함수 템플릿의 코드에 있는 템플릿 유형 매개변수로 교체된다. 예를 들어 명시적으로 특정화된 유형이 char const *이라면 다음 템플릿 정의에서

    template <typename Type>
    Type add(Type const &lvalue, Type const &rvalue)
    {
        return lvalue + rvalue;
    }
Typechar const *으로 교체될 것이며 결과적으로 원형이 다음과 같은 함수가 탄생한다.
    char const *add(char const *const &lvalue, char const *const &rvalue);
이제 이 함수를 사용해 보자:
    int main(int argc, char **argv)
    {
        add(argv[0], argv[1]);
    }
그렇지만 컴파일러는 우리의 특정화를 무시하고 최초의 함수 템플릿을 구체화하려고 시도한다. 이것은 실패한다. 결과적으로 도데체 왜 컴파일러가 명시적인 특정화를 선택하지 않았는지 궁금해진다.

여기에서 무슨 일이 일어났는지 알아보기 위해 한 단계씩 컴파일러의 조치를 감상해 보자:

우리의 add 함수 템플릿이 char *를 템플릿 유형 인자로 다룰 수 있으려면 char *에 대하여 또다른 명시적인 특정화가 요구된다. 결과적으로 그 원형은 다음과 같다.
    char *add(char *const &lvalue, char *const &rvalue);

또다른 명시적인 특정화를 정의하는 대신에 포인터를 기대하는 중복정의 함수 템플릿을 설계할 수 있다. 다음 함수 템플릿 정의는 상수 Type 값을 가리키는 두 개의 포인터를 기대하고 비-상수 Type을 포인터로 돌려준다.

    template <typename Type>
    Type *add(Type const *t1, Type const *t2)
    {
        std::cout << "Pointers\n";
        return new Type;
    }
실제로 어떤 유형이 위의 함수 매개변수에 묶일까? 이 경우 Type const *만 묶이는데, 인자로 char const *이라는 유형을 건넬 수 있다. 여기에는 자격 변형에게 기회가 없다. const 또는 const &의 관점에서 (Type아닌) 매개변수 자체가 지정된다면 컴파일러는 자격 변형을 활용하여 const를 비-상수 인자에 추가할 수 있다. t1을 보면 Type const *로 정의되어 있는 것을 볼 수 있다. 여기에 그 매개변수를 참조하는 const라는 것은 전혀 없다 (이 경우라면 Type const *const t1 또는 Type const *const &t1이 되었을 것이다). 결론적으로 자격 변형은 여기에 적용할 수 없다.

위의 중복정의 함수 템플릿은 char const *만 인자로 받기 때문에 (reinterpret cast 없이는) char *를 인자로 받지 않을 것이다. 그래서 mainargv 인자는 우리의 중복정의 함수 템플릿에 건넬 수 없다.

21.9.1: 너무 많은 특정화를 피하기

그래서 이 번에는 Type * 인자를 기대하는 또다른 중복정의 함수 템플릿을 선언해야 하는가? 가능하기는 하지만 우리의 접근법이 유연하지 못하다는 사실이 결국 드러날 것이다. 평범한 함수와 클래스처럼 함수 템플릿도 개념적으로 확실한 한 가지 목적을 가져야 한다. 중복정의 함수 템플릿에 중복정의 함수 템플릿을 추가하려는 시도는 즉시 템플릿의 인터페이스를 조잡하게 만들어 버린다. 이런 접근법을 사용하지 마라. 더 좋은 접근법은 원래의 목적에 충실하도록 템플릿을 생성하는 것이다. 문서에 그의 목적을 명료하게 기술하고 가끔씩 특정한 사례를 허용하기 위한 목적으로만 템플릿을 생성하는 것이 좋다.

템플릿을 생성하는 상황에서 명시적인 특정화는 물론 합당할 수 있다. 우리의 add 함수 템플릿에 문자를 가리키는 const 그리고 비-const 포인터에 대하여 두 개의 특정화가 적절할 것이다. 다음은 그 생성 방법이다.

다음은 char *char const * 인자를 기대하는 함수 템플릿 add에 대한 명시적인 특정화 두 가지이다.

    template <> char *add<char *>(char *const &p1,
                                        char *const &p2)
    {
        std::string str(p1);
        str += p2;
        return strcpy(new char[str.length() + 1], str.c_str());
    }

    template <> char const *add<char const *>(char const *const &p1,
                                        char const *const &p2)
    {
        static std::string str;
        str = p1;
        str += p2;
        return str.c_str();
    }
템플릿의 명시적 특정화는 보통 다른 함수 템플릿의 구현이 들어있는 파일 안에 포함된다.

21.9.2: 특정화 선언하기

템플릿의 명시적 특정화는 보통의 방법대로 선언할 수 있다. 즉, 몸체를 쌍반점으로 바꾸면 된다.

템플릿의 명시적 특정화를 선언할 때 template 키워드 다음의 옆꺽쇠 한 쌍이 중요하다. 생략하면 템플릿의 구체화를 선언한다. 컴파일러는 조용하게 처리한다. 컴파일 시간이 약간 더 길어지는 희생은 감수한다.

템플릿의 명시적인 특정화를 선언할 때 (또는 구체화를 선언할 때) 컴파일러가 함수의 인자로부터 이런 유형을 추론할 수 있으면 템플릿 유형 매개변수를 명시적으로 지정하지 않아도 된다. char (const) * 특정화의 경우라면 다음과 같이 선언할 수도 있다.

    template <> char *add(char *const &p1, char *const &p2)
    template <> char const *add(char const *const &p1,
                                char const *const &p2);
게다가 template <>을 생략할 수 있다면 템플릿 문자(쌍반점)는 선언으로부터 제거될 것이다. 결과로 나온 선언은 이제 단순히 함수 선언에 불과하다. 이것은 에러가 아니다. 함수 템플릿과 평범한 (비-템플릿) 함수는 서로 중복정의할 수 있다. 평범한 함수는 함수 템플릿에 비해 유형 변환에 제한이 없다. 이 때문에 이따금씩 평범한 함수로 템플릿을 중복정의하기도 한다.

함수 템플릿의 명시적 특정화는 그냥 함수 템플릿의 또다른 중복정의 버전에 불과하다. 중복정의 버전은 완전히 다른 집합의 템플릿 매개변수를 정의할 수 있는 반면에, 특정화는 비-특정화된 변형과 똑 같은 템플릿 매개변수의 집합을 사용해야 한다. 컴파일러는 실제 템플릿 인자가 특정화로 정의된 유형에 부합하는 상황에 특정화를 사용한다 (인자 집합에 부합하여 매개변수가 가장 특정화된 집합이 사용된다는 규칙을 따른다). 다양한 매개변수 집합에 대하여 중복정의 버전의 함수를 (또는 함수 템플릿을) 사용해야 한다.

21.9.3: 삽입 연산자를 사용할 때의 복잡성

명시적인 특정화와 중복정의를 다루어 보았으므로 이제 클래스가 std::string 변환 연산자를 정의할 때 무슨 일이 일어나는지 연구해 보자 (11.3절).

변환 연산자는 rvalue로 사용될 것이라고 보장된다. 이것은 string 변환 연산자가 정의된 클래스의 객체를 string 객체에 할당할 수 있다는 뜻이다. 그러나 string 변환 연산자를 정의한 객체를 스트림에 삽입하려고 시도하면 컴파일러는 부적절한 유형을 ostream에 삽입하려 한다고 불평한다.

반면에 이 클래스가 int 변환 연산자를 정의하고 있으면 문제없이 삽입된다.

이렇게 구분하는 이유는 operator<<가 기본 유형을 (예를 들어 int 유형을) 삽입할 때는 평범한 (자유) 함수로 정의되어 있지만 string을 삽입할 때는 함수 템플릿으로 정의되어 있기 때문이다. 그러므로 string 변환 연산자를 정의한 클래스의 객체를 삽입하려고 할 때 컴파일러는 ostream 객체로 삽입하는 삽입 연산자의 모든 중복정의 버전을 방문한다.

기본 유형의 변환이 없으므로 기본 유형의 삽입 연산자는 사용할 수 없다. 템플릿 인자에 대한 변환은 변환 연산자를 찾아 보도록 컴파일러에게 허용하지 않기 때문에 string 변환 연산자를 정의한 우리의 클래스는 ostream에 삽입할 수 없다.

그런 클래스의 객체를 ostream 객체에 삽입한다고 하더라도 클래스는 (string 인자에서 클래스의 객체를 rvalue로 사용하는 데 요구되는 string 변환 연산자 말고도) 반드시 자신만의 중복정의 삽입 연산자를 정의해야 한다.

21.10: 정적 표명(assertions)

    static_assert(constant expression, error message)
이 서술문으로 템플릿 정의 안에 (프로그래머의 의도를) 표명할 수 있다. 다음은 두 가지 사용법이다.
    static_assert(BUFSIZE1 == BUFSIZE2,
                                "BUFSIZE1 and BUFSIZE2 must be equal");

    template <typename Type1, typename Type2>
    void rawswap(Type1 &type1, Type2 &type2)
    {
        static_assert(sizeof(Type1) == sizeof(Type2),
                        "rawswap: Type1 and Type2 must have equal sizes");
        // ...
    }
첫 번째 예제는 또다른 전처리기 지시어를 피하는 법을 보여준다 (이 경우 #error 지시어).

두 번째 예제는 어떻게 static_assert를 사용하여 템플릿이 올바른 조건에서 작동하는지 확인하는 방법을 보여준다.

static_assert의 첫 번째 인자에 정의된 조건이 false이면 static_assert의 두 번째 인자에 정의된 문자열이 화면에 보여지고 컴파일은 멈춘다.

#error 전처리기 지시어처럼 static_assert컴파일 시간에 관련된다. 코드에 사용되더라도 실행 시간의 효율성에 영향을 전혀 주지 않는다.

21.11: 숫치 한계

<climits> 헤더 파일은 다양한 유형의 상수를 정의한다. 예를 들어 INT_MAXint에 저장할 수 있는 최대 값을 정의한다.

climits에 정의된 한계의 단점은 제한이 고정되어 있다는 것이다. 어떤 유형의 인자를 받는 함수 템플릿을 작성한다고 해 보자. 예를 들어,

    template<typename Type>
    Type operation(Type &&type);
이 함수는 Type에 대하여 type이 음의 값이면 가장 큰 음의 값을 돌려주어야 하고 type이 양의 값이면 가장 큰 양의 값을 돌려주어야 한다. 그렇지만 정수가 아니면 0을 돌려주어야 한다.

어떻게 처리할 것인가?

climits의 상수들은 사용할 유형을 이미 알고 있을 때만 사용할 수 있기 때문에 유일한 접근법은 다양한 정수 유형에 대하여 함수 템플릿 특정화를 사용하는 것이다. 예를 들어,

    template<>
    int operation<int>(int &&type)
    {
        return type < 0 ? INT_MIN : INT_MAX;
    }

numeric_limits가 제공하는 편의기능에 대안이 있다. 이 편의기능을 사용하려면 <limits> 헤더를 포함해야 한다.

numeric_limits 클래스 템플릿에는 숫치 유형에 관련하여 온갖 질문에 답하는 다양한 멤버가 들어있다. 이들을 소개하기 전에 operation 함수 템플릿을 단 하나의 함수 템플릿으로 어떻게 구현할 수 있을지 살펴보자:

    template<typename Type>
    Type operation(Type &&type)
    {
        return
            not numeric_limits<Type>::is_integer ? 0 :
            type < 0 ? numeric_limits<Type>::min() :
                       numeric_limits<Type>::max();
    }
이제 모든 원시 유형에 대하여 operation을 사용할 수 있다.

다음은 numeric_limits이 제공하는 편의기능을 개관한다. numeric_limits이 정의하는 멤버 함수들은 constexpr 값을 돌려주는 것에 주목하라. Type 유형에 대하여 numeric_limits에 정의된 `member' 멤버는 다음과 같이 사용할 수 있다.

    numeric_limits<Type>::member    // 데이터 멤버
    numeric_limits<Type>::member()  // 멤버 함수
모든 numeric_limits 멤버 함수는 constexpr 값을 돌려준다.

21.12: 함수객체에 대한 다형적 포장자

C++에서 (멤버) 함수를 가리키는 포인터는 rvalue가 매우 엄격하다. 유형에 부합하는 함수만 가리킬 수 있다. 이것은 템플릿을 정의할 때 문제가 된다. 함수 포인터의 유형이 템플릿의 매개변수에 달려 있기 때문이다.

이 문제를 해결하기 위하여 다형적 포장자(함수객체)를 사용할 수 있다. 다형적 포장자는 함수 포인터나 멤버 함수 또는 함수객체를 참조한다. 매개변수가 유형과 갯수에 부합하기만 하면 된다.

다형적 함수 포장자를 사용하기 전에 `<functional>' 헤더를 포함해야 한다.

다형적 포장 함수는 std::function 클래스 템플릿을 통하여 사용할 수 있다. 그의 템플릿 인자는 포장자를 두를 함수의 원형이다. 다음은 다형적 포장 함수를 정의하는 예이다. 두 개의 int 값을 기대하고 int 하나를 돌려주는 함수를 가리킨다.

    std::function<int (int, int)> ptr2fun;
여기에서 템플릿의 매개변수는 int (int, int)이다. 함수가 두 개의 int 인자를 기대하고 하나의 int를 돌려준다는 뜻이다. 다른 원형은 부합하는 다른 함수 포장자를 돌려준다.

그런 함수 포장자는 이제 자신이 포장한 어떤 함수도 가리키는 데 사용할 수 있다. 예를 들어 `plus<int> add'는 int operator()(int, int) 함수 호출 멤버를 정의하는 함수객체를 생성한다. 이것은 원형이 int (int, int)인 함수의 자격이 있기 때문에 우리의 ptr2funadd를 가리킬 수 있다.

    ptr2fun = add;

ptr2fun이 아직 함수를 가리키지 않는다면 (그저 정의되어 있을 뿐이라면) 그리고 그것을 통하여 함수를 호출하려고 시도했다면 `std::bad_function_call' 예외가 던져진다. 함수의 주소가 아직 할당되어 있지 않은 다형적 함수 포장자는 (마치 값이 0인 포인터처럼) 논리 표현식에서 false 값을 나타낸다.

    std::function<int(int)> ptr2int;

    if (not ptr2int)
        cout << "ptr2int is not yet pointing to a function\n";

다형적 함수 포장자는 또한 함수나 함수객체 또는 다른 다형적 함수 포장자를 참조하는 데 사용할 수 있다. 그 원형에 매개변수나 반환 값에 대하여 표준 변환이 존재하기만 하면 된다. 예를 들어,

    bool predicate(long long value);

    void demo()
    {
        std::function<int(int)> ptr2int;

        ptr2int = predicate;    // OK, 매개변수 변환 가능. 유형을 반환함

        struct Local
        {
            short operator()(char ch);
        };

        Local object;

        std::function<short(char)> ptr2char(object);

        ptr2int = object;       // OK, object는 함수객체로서
                                // 변환가능한 매개변수를 가진다.
                                // 유형을 반환함.
        ptr2int = ptr2char;     // OK, 이제 다형성과 함수객체 그리고 포장자를 사용함
    }

21.13: 템플릿 정의를 컴파일하고 구체화하기

다음의 add 함수의 템플릿 정의를 연구해 보자:
    template <typename Container, typename Type>
    Type add(Container const &container, Type init)
    {
        return std::accumulate(container.begin(), container.end(), init);
    }
여기에서 std::accumulate가 호출된다. containerbegin 멤버와 end 멤버를 사용한다.

container.begin()container.end()에 대한 호출은 템플릿 유형 매개변수에 의존한다고 말한다. 컴파일러는 container의 인터페이스를 보지 못했기 때문에 containerbegin 멤버와 end 멤버가 실제로 입력 반복자를 돌려주는지 확신할 수 없다.

반면에 std::accumulate 자체는 템플릿 매개변수의 유형에 완전히 독립적이다. 그의 인자는 템플릿 매개변수에 달려 있지만 함수 호출 자체는 거기에 의존하지 않는다. 템플릿 매개변수의 유형에 독립적인 템플릿 몸체의 서술문을 템플릿 매개변수의 유형에 의존하지 않는다라고 말한다.

템플릿 정의를 만나면 컴파일러는 템플릿 매개변수에 의존하지 않는 서술문의 구문적 정확성을 모두 검증한다. 즉, 모든 클래스 정의와 모든 유형 정의 그리고 모든 함수 선언 등등을 이미 다 보았어야 한다. 컴파일러가 필요한 선언과 정의를 다 보지 못했다면 템플릿의 정의를 거부할 것이다. 그러므로 위의 템플릿을 컴파일러에게 제출할 때 numeric 헤더를 먼저 포함했어야 한다. 이 헤더 파일에 std::accumulate 알고리즘이 선언되어 있기 때문이다.

템플릿 매개변수에 의존하는 서술문에 컴파일러는 그런 광범위한 구문적 점검을 수행할 수 없다. 컴파일러는 아직 지정되지 않은 유형인 Container에 대하여 begin 멤버의 존재를 검증할 방법이 없다. 이 경우 컴파일러는 필요한 멤버와 연산자 그리고 유형이 결국 사용가능하게 될 것이라고 간주하고서 형식적으로 점검한다.

템플릿이 초기화되는 소스의 위치를 구체화 시점이라고 부른다. 구체화 시점에 컴파일러는 템플릿 매개변수의 실제 유형을 추론한다. 그 시점에 템플릿 유형 매개변수에 의존하는 템플릿의 서술문의 구문이 올바른지 점검한다. 이것은 컴파일러가 필요한 선언을 구체화 시점에 이미 보았어야 한다는 것을 암시한다. 제일 규칙으로서 컴파일러가 템플릿을 구체화하는 시점에 필요한 모든 선언을 (보통은 헤더를) 읽었는지 확인해야 한다. 템플릿의 정의 자체에 대해서는 좀 더 느슨하게 요구할 수 있다. 정의를 읽을 때는 템플릿 유형 매개변수에 의존하지 않는 서술문에 요구되는 선언만 제공하면 된다.

21.14: 함수 선택 메커니즘

컴파일러가 함수 호출을 만날 때 중복정의 함수를 사용할 수 있으면 어느 함수를 호출할 지 결정해야 한다. 이전에 만나 보았던 원칙처럼 `가장 구체적인 함수가 선택된다'. 이것은 컴파일러의 함수 선택 메커니즘을 아주 직관적으로 기술한 것이다. 다음 절은 더 자세하게 이 메커니즘을 살펴본다.

컴파일러에게 다음 main 함수를 컴파일해 달라고 요구한다고 가정하자.

    int main()
    {
        process(3, 3);
    }
또 컴파일러가 main을 컴파일하려고 할 때 다음 함수 선언을 만났다고 가정하자.
    template <typename T>
    void process(T &t1, int i);                 // 1

    template <typename T1, typename T2>
    void process(T1 const &t1, T2 const &t2);   // 2

    template <typename T>
    void process(T const &t, double d);         // 3

    template <typename T>
    void process(T const &t, int i);            // 4

    template <>
    void process<int, int>(int i1, int i2);     // 5

    void process(int i1, int i2);               // 6

    void process(int i, double d);              // 7

    void process(double d, int i);              // 8

    void process(double d1, double d2);         // 9

    void process(std::string s, int i)          // 10

    int add(int, int);                          // 11
컴파일러는 main의 서술문을 이미 읽었으므로 이제 어느 함수를 실제로 호출해야 하는지 결정해야 한다. 다음과 같이 처리된다. 이 시점에서 컴파일러는 템플릿 유형 매개변수의 유형을 결정하려고 시도한다. 이 단계는 다음 절에서 연구한다.

21.14.1: 템플릿 유형 매개변수 결정하기

후보 함수들을 결정했고 그 중에서 가능한 함수들을 결정했으므로 이제 컴파일러는 템플릿 유형 매개변수의 실제 유형을 결정해야 한다.

실제 유형을 템플릿 유형 매개변수에 맞추어 볼때 세 가지 표준 템플릿 매개변수 변형 절차 중 무엇이든 사용할 수 있다 (21.4절). 이렇게 처리하다가 함수 1의 T &t1 매개변수에 있는 T에 대하여 어떤 유형도 결정할 수 없다고 결론을 내린다. 인자 3은 상수 int 값이기 때문이다. 그래서 함수 1은 생존 가능한 함수 리스트에서 제외된다. 이제 컴파일러는 잠재적으로 구체화가 가능한 함수 템플릿과 평범한 함수들을 마주하게 되었다.

    void process(T1 [= int] const &t1, T2 [= int] const &t2);   // 2
    void process(T [= int] const &t, double d);                 // 3
    void process(T [= int] const &t, int i);                    // 4
    void process<int, int>(int i1, int i2);                     // 5
    void process(int i1, int i2);                               // 6
    void process(int i, double d);                              // 7
    void process(double d, int i);                              // 8
    void process(double d1, double d2);                         // 9

컴파일러는 직접 일치 갯수 값을 각각의 생존가능한 함수에 연관짓는다. 직접적 일치 갯수는 (자동) 유형 변환 없이 함수 매개변수에 일치하는 인자의 갯수를 센다. 예를 들어 함수 2에 대하여 이 갯수는 2이고 함수 7에 대해서는 1이며 함수 9는 0이다. 함수들은 이제 직접 일치 갯수를 기준으로 (내림차순으로) 정렬된다.

                                                             부합
                                                             횟수
    void process(T1 [= int] const &t1, T2 [= int] const &t2);  2 // 2
    void process(T [= int] const &t, int i);                   2 // 4
    void process<int, int>(int i1, int i2);                    2 // 5
    void process(int i1, int i2);                              2 // 6
    void process(T [= int] const &t, double d);                1 // 3
    void process(int i, double d);                             1 // 7
    void process(double d, int i);                             1 // 8
    void process(double d1, double d2);                        0 // 9
최상위 값에 무승부가 없다면 상응하는 함수가 선택되고 함수 선택 과정은 완료된다.

상단에 여러 함수가 나타나면 컴파일러는 모호성이 없는지 검증한다. 유형 변환이 요구되(지 않)는 매개변수의 순서가 다르면 모호성에 봉착한다. 예를 들어 함수 3과 8을 연구해 보자. `직접 일치'에는 D를 사용하고 `변환'에는 C를 인자로 사용한다고 하자. 그러면 함수 3에는 인자가 D와 C가 되고 그리고 함수 8에는 C와 D가 된다. 2와 4 그리고 5와 6을 사용할 수 없다고 가정하면 컴파일러는 모호하다고 보고할 것이다. 함수 3과 8에 대하여 인자/매개변수의 부합 절차가 다르기 때문이다. 함수 7과 8을 비교하면 똑 같이 차이가 있다. 그러나 함수 3과 7을 비교하면 그런 차이가 없다.

이 시점에서 최상위 값에 무승부가 있고 컴파일러는 관련 함수들을 처리한다 (함수 2와 4 그리고 5와 6). 이 함수들의 비-템플릿 매개변수의 갯수를 세어 이 평범한 매개변수 갯수를 각각의 함수에 연관짓는다. 함수들은 이 갯수를 기준으로 내림차순으로 정렬된다. 그 결과는 다음과 같다.

                                                         평범한 매개변수의
                                                                갯수
    void process(int i1, int i2);                              2 // 6
    void process(T [= int] const &t, int i);                   1 // 4
    void process(T1 [= int] const &t1, T2 [= int] const &t2);  0 // 2
    void process<int, int>(int i1, int i2);                    0 // 5
이제 최상위 값에 무승부가 없다. 상응하는 함수가 (process(int, int), 함수 6) 선택되고 함수 선택 처리는 완료된다. 함수 6이 main의 함수 호출 서술문에 사용된다.

함수 6이 정의되어 있지 않았다면 함수 4가 사용되었을 것이다. 함수 4도 함수 6도 정의되어 있지 않았다면 함수 2와 5에 선택 처리가 계속되었을 것이다.

                                                         평범한 매개변수의
                                                                갯수
    void process(T1 [= int] const &t1, T2 [= int] const &t2);  0 // 2
    void process<int, int>(int i1, int i2);                    0 // 5

이 상황에서 다시 무승부를 만난다. 그리고 선택 절차는 계속된다. `함수 유형' 값을 평범한 매개변수 갯수가 가장 높은 각각의 함수에 연관짓는다. 그리고 이 함수들은 함수 값의 유형을 기준으로 내림차순으로 정렬된다. 값 2는 평범한 함수에 연관되고 값 1은 템플릿의 명시적인 특정화에 연관되며 그리고 값 0은 평범한 함수 템플릿에 연관된다.

최상위 값에 무승부가 없다면 상응하는 함수가 선택되고 함수 선택은 완료된다. 무승부가 있다면 컴파일러는 모호성을 보고하고 어느 함수를 호출할지 결정하지 못한다. 함수 2와 5만 존재한다고 간주하면 이 선택 과정은 다음과 같은 순서가 되었을 것이다.

                                                             함수
                                                             유형
    void process<int, int>(int i1, int i2);                    1 // 5
    void process(T1 [= int] const &t1, T2 [= int] const &t2);  0 // 2
템플릿을 명시적으로 특정화한 함수 5가 선택되었을 것이다.

Figure 25 is shown here.
그림 25: 함수 템플릿 선택 메커니즘

다음은 함수 템플릿 선택 메커니즘을 요약한 것이다 (그림 25 참고):

21.15: SFINAE: Substitution Failure Is Not An Error

교체 실패는 에러가 아니다. 다음 구조체 정의를 연구해 보자:
    struct Int
    {
        typedef int type;
    };
이 시점에서 typedef를 구조체 안에 내장하는 것이 이상하게 보일지 모르겠지만 제 23장에서 이것이 실제로 무척 유용한 상황을 만나보게 될 것이다. 템플릿이 요구하는 유형의 변수를 정의할 수 있다. 예를 들어 (다음 함수 매개변수 리스트에 typename을 사용한 것은 무시한다. 자세한 것은 22.2.1항을 참고하라):
    template <typename Type>
    void func(typename Type::type value)
    {
    }
func(10)을 호출하려면 Int를 명시적으로 지정해야 한다. 많은 구조체에서 type을 정의하고 있기 때문이다. 컴파일러는 도움이 필요하다. 올바른 호출은 func<Int>(10)이다. 이제 Int를 의도한다는 것이 명백하므로 컴파일러는 valueint라고 올바르게 추론한다.

그러나 템플릿은 중복정의가 될 수 있고 다음 정의를 보면:

    template <typename Type>
    void func(Type value)
    {}
이제 이 함수를 확실하게 사용하기 위해 func<int>(10)를 지정하면 역시 문제없이 컴파일된다.

그러나 이전 절에서 보았듯이 컴파일러는 어느 템플릿을 구체화할지 결정할 때 함수 원형의 매개변수 유형과 실제로 제공된 인자 유형을 맞추어 보고 가능한 함수 리스트를 만든다. 그렇게 하려면 컴파일러는 매개변수의 유형을 결정할 수 있어야 하는데 바로 여기에 문제가 있다.

Type = int를 평가할 때 컴파일러는 (첫 번째 템플릿 정의인) func(int::type)의 원형과 (두 번째 템플릿 정의인) func(int)의 원형을 만난다. 그러나 int::type은 없다. 그래서 에러가 자주 일어난다. 그렇지만 그 에러는 제공된 템플릿 유형 인자를 다양한 템플릿 정의로 교체했기 때문이다.

템플릿 정의에서 유형을 교체함으로써 야기되는 유형-문제는 에러로 간주되지 않는다. 그저 그 특별한 유형이 특별한 템플릿에 사용될 수 없다는 사실을 알려줄 뿐이다.

이 원칙은 교체 실패에러가 아니다라는 것으로 알려져 있다 (SFINAE). 그리고 컴파일러가 (여기에 보여주듯이) 간단한 중복정의 함수를 선택하는 데 뿐만 아니라 여러 가능한 특정화 템플릿 중에서 고를 때에도 사용한다 (제 22장23장).

21.16: `if constexpr'를 이용하여 조건적으로 함수 정의하기 (C++17)

C++17 표준은 if constexpr (cond) 구문이 있다. if 선택 서술문이 사용되는 모든 곳에 사용할 수 있지만, 특정한 사용법은 함수 템플릿에 있다. if constexpr으로 컴파일러는 컴파일 시간에 if constexpr (cond) 절을 평가하여 템플릿 함수를 부분적으로 구체화할 수 있다.

다음은 예제이다.

     1: void positive();
     2: void negative();
     3:
     4: template <int value>
     5: void fun()
     6: {
     7:     if constexpr (value > 0)
     8:         positive();
     9:     else if constexpr (value < 0)
    10:         negative();
    11: }
    12:
    13: int main()
    14: {
    15:     fun<4>();
    16: }

if constexpr 서술문 자체는 실행 코드가 되지 않음을 눈여겨보라. 컴파일러가 구체화할 부분을 선택하는 데 사용될 뿐이다. 이 경우 positive만 적절하게 구체화된다. 프로그램이 링크 단계에 들어 가기 전에 사용가능하기 때문이다.

21.17: 템플릿 선언 문법 요약

이 절은 템플릿을 선언하기 위한 기본적인 구문을 요약한다. 템플릿을 정의할 때 맨 뒤의 쌍반점은 함수 몸체로 교체하면 된다.

템플릿 선언이 모두 템플릿 정의로 변환되는 것은 아니다. 정의가 제공된다면 명시적으로 선언된 것이다.

21.18: C++14: 변수처럼 템플릿 사용하기(템플릿 변수)

함수 템플릿과 클래스 템플릿 외에도 (제 22장) C++14 표준에서는 변수 템플릿을 정의할 수 있다. 변수 템플릿은 (함수나 클래스) 템플릿을 정의할 때 편리하다. 변수 템플릿 안에 정의된 유형 매개변수에 유형을 지정한다.

변수 템플릿은 익숙한 template 헤더로 시작하여 다음에 변수 자체의 정의가 따라온다. 템플릿 헤더는 유형을 지정한다. 거기에 기본 유형을 지정해도 된다. 예를 들어,

    template<typename T = long double>
    constexpr T pi = T(3.1415926535897932385);

이 변수를 사용하려면 유형을 지정해야 한다. 그리고 초기화된 그 값을 지정된 유형으로 변환할 수 있으면 컴파일러가 조용하게 변환해 준다.

    cout << pi<> << ' ' << pi<int>;
두 번째 삽입에서 long double 초기화 값은 int로 변환되고 3을 화면에 보여준다.

특정화도 지원한다. 예를 들어 텍스트 `pi'를 보여주기 위해 char const * 유형에 대한 특정화는 다음과 같이 정의할 수 있다.

    template<>
    constexpr char const *pi<char const *> = "pi";
이 특정화로 cout << pi<char const *>을 하면 pi를 보여줄 수 있다.