함수 템플릿

함수 템플릿 (Function template)

함수 템플릿은 함수들의 계열(family)을 정의해요. 타입만 다르고 동작은 같은 함수를 일일이 쓰는 대신, 하나의 템플릿으로 여러 타입에 대해 똑같은 함수를 만들어내는 문법입니다.

출처: cppreference

본문

문법

    template < parameter-list > function-declaration (1)
    template < parameter-list > requires constraint function-declaration (2) (since C++20)
    function-declaration-with-placeholders (3) (since C++20)
    export template < parameter-list > function-declaration (4) (removed in C++11)

설명

| parameter-list | - | 비어 있지 않은, 템플릿 매개변수의 쉼표로 구분된 목록. 각각은 비타입 매개변수, 타입 매개변수, 템플릿 매개변수, 또는 그것들 중 하나의 매개변수 팩(C++11부터)이에요. 어떤 템플릿이든 매개변수는 제약될 수 있어요(C++20부터). | | function-declaration | - | 함수 선언. 선언된 함수 이름은 템플릿 이름이 돼요. | | constraint | - | 이 함수 템플릿이 받아들이는 템플릿 매개변수를 제한하는 제약 표현식 | | function-declaration-with-placeholders | - | 매개변수 중 적어도 하나의 타입이 auto 또는 Concept auto 자리 표시자를 사용하는 함수 선언. 각 자리 표시자마다 하나의 발명된 템플릿 매개변수가 템플릿 매개변수 목록에 생겨요(아래 축약 함수 템플릿 참고). | | export | - | 템플릿을 내보내진(exported) 것으로 선언하는 선택적 수정자였어요(클래스 템플릿에 쓰면 모든 멤버도 내보내진 것으로 선언). 내보내진 템플릿을 인스턴스화하는 파일은 그 정의를 포함할 필요가 없었고, 선언만으로 충분했죠. export의 구현은 드물었고 서로 세부 사항에서 일치하지 않았습니다. (C++11 이전) |

축약 함수 템플릿 (Abbreviated function template)

함수 선언이나 함수 템플릿 선언의 매개변수 목록에 자리 표시자 타입(auto 또는 Concept auto)이 나타나면, 그 선언은 함수 템플릿을 선언하며 각 자리 표시자마다 발명된 템플릿 매개변수 하나가 템플릿 매개변수 목록에 추가돼요:

void f1(auto);           // same as template<class T> void f1(T)
void f2(C1 auto);        // same as template<C1 T> void f2(T), if C1 is a concept
void f3(C2 auto...);     // same as template<C2... Ts> void f3(Ts...), if C2 is a concept
void f4(const C3 auto*, C4 auto&); // same as template<C3 T, C4 U> void f4(const T*, U&);

template<class T, C U>
void g(T x, U y, C auto z);  // same as template<class T, C U, C W> void g(T x, U y, W z);

축약 함수 템플릿은 다른 모든 함수 템플릿처럼 특수화될 수 있어요:

template<> void f4<int>(const int*, const double&); // specialization of f4<int, const double>

(C++20부터)

함수 템플릿 시그니처 (Function template signature)

모든 함수 템플릿은 시그니처를 가져요. template-head의 시그니처는 템플릿 매개변수 목록인데, 템플릿 매개변수 이름과 기본 인자, 그리고 requires-clause(있으면, C++20부터)를 뺀 것이에요.

함수 템플릿의 시그니처는 이름, 매개변수 타입 목록, 반환 타입, 후행 requires-clause(있으면, C++20부터), template-head의 시그니처를 포함해요. 다음 경우를 제외하고, 그 시그니처는 둘러싼 네임스페이스도 포함합니다:

  • 함수 템플릿이 클래스 멤버면, 그 시그니처는 둘러싼 네임스페이스 대신 함수가 멤버인 클래스를 포함해요. 또한 후행 requires-clause(있으면, C++20부터), ref 한정자(있으면), cv 한정자(있으면, C++11부터)도 포함합니다.
  • 함수 템플릿이 둘러싼 템플릿 매개변수를 포함하는 제약을 가진 friend면, 그 시그니처는 둘러싼 네임스페이스 대신 둘러싼 클래스를 포함해요. (C++20부터)

함수 템플릿 인스턴스화 (Function template instantiation)

함수 템플릿 그 자체는 타입도 함수도 아니에요. 템플릿 정의만 담긴 소스 파일에서는 코드가 생성되지 않습니다. 어떤 코드가 나타나려면 템플릿이 인스턴스화되어야 해요. 즉 컴파일러가 실제 함수(클래스 템플릿이라면 클래스)를 생성할 수 있도록 템플릿 인자가 결정되어야 합니다.

명시적 인스턴스화 (Explicit instantiation)

    template return-type name < argument-list > ( parameter-list ) ; (1)
    template return-type name ( parameter-list ) ; (2)
    extern template return-type name < argument-list > ( parameter-list ) ; (3) (since C++11)
    extern template return-type name ( parameter-list ) ; (4) (since C++11)
  • (1) 명시적 인스턴스화 정의(모든 비기본 템플릿 매개변수가 명시적으로 지정되면 템플릿 인자 추론 없음)
  • (2) 모든 매개변수에 대해 템플릿 인자 추론을 사용한 명시적 인스턴스화 정의
  • (3) 명시적 인스턴스화 선언(모든 비기본 템플릿 매개변수가 명시적으로 지정되면 템플릿 인자 추론 없음)
  • (4) 모든 매개변수에 대해 템플릿 인자 추론을 사용한 명시적 인스턴스화 선언

명시적 인스턴스화 정의는 그것이 가리키는 함수나 멤버 함수의 인스턴스화를 강제해요. 템플릿 정의 이후 어디에든 프로그램에 나타날 수 있고, 주어진 argument-list에 대해 프로그램에 단 한 번만 나타나는 게 허용돼요(진단 불요).

명시적 인스턴스화 선언(extern template)은 암시적 인스턴스화를 막아요. 그렇지 않으면 암시적 인스턴스화를 일으켰을 코드는 프로그램의 다른 곳에 제공된 명시적 인스턴스화 정의를 사용해야 합니다. (C++11부터)

함수 템플릿 특수화나 멤버 함수 템플릿 특수화의 명시적 인스턴스화에서, 함수 매개변수에서 추론될 수 있으면 후행 템플릿 인자를 지정하지 않아도 돼요:

template<typename T>
void f(T s)
{
    std::cout << s << '\n';
}

template void f<double>(double); // instantiates f<double>(double)
template void f<>(char);         // instantiates f<char>(char), template argument deduced
template void f(int);            // instantiates f<int>(int), template argument deduced

함수 템플릿이나 클래스 템플릿의 멤버 함수의 명시적 인스턴스화는 inline이나 constexpr을 쓸 수 없어요. 명시적 인스턴스화 선언이 암시적으로 선언된 특수 멤버 함수를 가리키면 프로그램은 ill-formed입니다.

생성자의 명시적 인스턴스화는 템플릿 매개변수 목록(문법 (1))을 쓸 수 없는데, 그것들은 추론될 수 있기 때문(문법 (2))에 필요하지 않아요.

전망 소멸자(prospective destructor)의 명시적 인스턴스화는 클래스의 선택된 소멸자를 가리켜야 해요. (C++20부터)

명시적 인스턴스화 선언은 인라인 함수, auto 선언, 참조, 클래스 템플릿 특수화의 암시적 인스턴스화를 억제하지 않아요. (그래서 명시적 인스턴스화 선언의 대상인 인라인 함수가 odr-사용되면, 인라이닝을 위해 암시적으로 인스턴스화되지만 이 번역 단위에서는 아웃오브라인 복사본이 생성되지 않아요.)

기본 인자가 있는 함수 템플릿의 명시적 인스턴스화 정의는 그 인자를 사용하는 것이 아니며, 그것들을 초기화하려 하지 않아요:

char* p = 0;

template<class T>
T g(T x = &p) { return x; }

template int g<int>(int); // OK even though &p isn't an int.

암시적 인스턴스화 (Implicit instantiation)

코드가 함수 정의가 존재해야 하는 문맥에서 함수를 참조하거나, 정의의 존재가 프로그램의 의미에 영향을 주면(C++11부터), 그리고 이 특정 함수가 명시적으로 인스턴스화되지 않았다면 암시적 인스턴스화가 일어나요. 문맥에서 추론될 수 있으면 템플릿 인자 목록을 제공하지 않아도 됩니다.

#include <iostream>

template<typename T>
void f(T s)
{
    std::cout << s << '\n';
}

int main()
{
    f<double>(1); // instantiates and calls f<double>(double)
    f<>('a');     // instantiates and calls f<char>(char)
    f(7);         // instantiates and calls f<int>(int)
    void (*pf)(std::string) = f; // instantiates f<string>(string)
    pf("∇");                     // calls f<string>(string)
}

함수의 정의 존재는, 상수 표현식의 평가가 불필요하거나 상수 표현식 평가가 그 정의를 사용하지 않더라도, 함수가 어떤 표현식의 상수 평가에 필요하면 프로그램의 의미에 영향을 주는 것으로 간주돼요:

template<typename T> constexpr int f() { return T::value; }

template<bool B, typename T> void g(decltype(B ? f<T>() : 0));
template<bool B, typename T> void g(...);

template<bool B, typename T> void h(decltype(int{B ? f<T>() : 0}));
template<bool B, typename T> void h(...);

void x()
{
    g<false, int>(0); // OK: B ? f<T>() : 0 is not potentially constant evaluated
    h<false, int>(0); // error: instantiates f<int> even though B evaluates to false
                      // and list-initialization of int from int cannot be narrowing
}

(C++11부터)

참고: <>를 아예 생략하면 오버로드 해석이 템플릿과 비템플릿 오버로드 모두를 조사할 수 있어요.

템플릿 인자 추론 (Template argument deduction)

함수 템플릿을 인스턴스화하려면 모든 템플릿 인자를 알아야 하지만, 모든 템플릿 인자를 지정해야 하는 건 아니에요. 가능하면 컴파일러는 함수 인자에서 빠진 템플릿 인자를 추론해요. 이는 함수 호출이 시도될 때와 함수 템플릿의 주소를 구할 때 발생합니다.

template<typename To, typename From>
To convert(From f);

void g(double d)
{
    int i = convert<int>(d);    // calls convert<int,double>(double)
    char c = convert<char>(d);  // calls convert<char,double>(double)
    int(*ptr)(float) = convert; // instantiates convert<int, float>(float)
}

이 메커니즘 덕분에 템플릿 연산자를 쓸 수 있어요. 연산자에 대한 템플릿 인자를 함수 호출 표현식으로 다시 쓰지 않고 지정할 문법이 없거든요:

#include <iostream>

int main()
{
    std::cout << "Hello, world" << std::endl;
    // operator<< is looked up via ADL as std::operator<<,
    // then deduced to operator<<<char, std::char_traits<char>> both times
    // std::endl is deduced to &std::endl<char, std::char_traits<char>>
}

템플릿 인자 추론은 함수 템플릿 이름 조회(인자 종속 조회를 포함할 수 있음) 이후, 오버로드 해석 이전에 일어나요. 자세한 내용은 템플릿 인자 추론 문서를 참고하세요.

명시적 템플릿 인자 (Explicit template arguments)

함수 템플릿의 템플릿 인자는 다음에서 얻을 수 있어요:

  • 템플릿 인자 추론
  • 기본 템플릿 인자
  • 명시적 지정 — 다음 문맥에서 가능:
    • 함수 호출 표현식에서
    • 함수의 주소를 구할 때
    • 함수에 대한 참조를 초기화할 때
    • 멤버 함수 포인터를 만들 때
    • 명시적 특수화에서
    • 명시적 인스턴스화에서
    • friend 선언에서

오버로드된 연산자, 변환 함수, 생성자에는 함수 이름을 사용하지 않고 호출되기 때문에 템플릿 인자를 명시적으로 지정할 방법이 없어요.

지정된 템플릿 인자는 템플릿 매개변수와 종류가 일치해야 해요(즉 타입은 타입에, 비타입은 비타입에, 템플릿은 템플릿에). 매개변수보다 인자가 많을 수 없어요(하나의 매개변수가 매개변수 팩이면, 각 비팩 매개변수에 인자가 있어야 하지만요, C++11부터).

지정된 비타입 인자는 대응하는 비타입 템플릿 매개변수의 타입과 일치하거나 그것들로 변환 가능해야 해요.

템플릿 인자 추론에 참여하지 않는 함수 매개변수(예: 대응 템플릿 인자가 명시적으로 지정된 경우)는 평소 오버로드 해석처럼 대응 함수 매개변수의 타입으로의 암시적 변환이 적용돼요.

명시적으로 지정된 템플릿 매개변수 팩은 추가 인자가 있으면 템플릿 인자 추론으로 확장될 수 있어요:

template<class... Types>
void f(Types... values);

void g()
{
    f<int*, float*>(0, 0, 0); // Types = {int*, float*, int}
}

(C++11부터)

템플릿 인자 치환 (Template argument substitution)

모든 템플릿 인자가 지정·추론·기본 템플릿 인자에서 얻어지면, 함수 매개변수 목록에서 템플릿 매개변수의 사용은 모두 대응 템플릿 인자로 치환돼요.

치환 실패(즉 추론되거나 제공된 템플릿 인자로 템플릿 매개변수를 치환하는 데 실패)는 그 함수 템플릿을 오버로드 집합에서 제거해요. 이 덕분에 템플릿 메타프로그래밍으로 오버로드 집합을 조작하는 여러 방법이 가능해지는데, 자세한 내용은 SFINAE를 참고하세요.

치환 후, 배열·함수 타입의 모든 함수 매개변수는 포인터로 조정되고 모든 최상위 cv-한정자는 함수 매개변수에서 제거돼요(일반 함수 선언처럼).

최상위 cv-한정자의 제거는 함수 안에서 나타나는 매개변수의 타입에는 영향을 주지 않아요:

template<class T>
void f(T t);

template<class X>
void g(const X x);

template<class Z>
void h(Z z, Z* zp);

// two different functions with the same type, but
// within the function, t has different cv qualifications
f<int>(1);       // function type is void(int), t is int
f<const int>(1); // function type is void(int), t is const int

// two different functions with the same type and the same x
// (pointers to these two functions are not equal,
//  and function-local statics would have different addresses)
g<int>(1);       // function type is void(int), x is const int
g<const int>(1); // function type is void(int), x is const int

// only top-level cv-qualifiers are dropped:
h<const int>(1, NULL); // function type is void(int, const int*)
                       // z is const int, zp is const int*

함수 템플릿 오버로딩 (Function template overloading)

함수 템플릿과 비템플릿 함수는 오버로드될 수 있어요. 비템플릿 함수는 항상 같은 타입의 템플릿 특수화와 구별돼요. 다른 함수 템플릿의 특수화들은 같은 타입이어도 항상 서로 구별됩니다. 같은 반환 타입과 같은 매개변수 목록을 가진 두 함수 템플릿은 구별되며, 명시적 템플릿 인자 목록으로 구분할 수 있어요.

타입 또는 비타입 템플릿 매개변수를 사용하는 표현식이 함수 매개변수 목록이나 반환 타입에 나타나면, 그 표현식은 오버로딩 목적상 함수 템플릿 시그니처의 일부로 남아요:

template<int I, int J>
A<I+J> f(A<I>, A<J>); // overload #1

template<int K, int L>
A<K+L> f(A<K>, A<L>); // same as #1

template<int I, int J>
A<I-J> f(A<I>, A<J>); // overload #2

템플릿 매개변수를 포함하는 두 표현식은, 이 표현식을 포함한 두 함수 정의가 ODR 아래에서 같다면 — 즉 두 표현식이 이름 조회로 같은 엔터티에 해석되는 같은 토큰 시퀀스를 포함한다면(템플릿 매개변수는 다르게 이름 붙일 수 있음) — 동등(equivalent)하다고 해요. 두 람다 표현식은 절대 동등하지 않습니다. (C++20부터)

template<int I, int J>
void f(A<I+J>); // template overload #1

template<int K, int L>
void f(A<K+L>); // equivalent to #1

두 의존 표현식이 동등한지 결정할 때는 관련된 의존 이름만 고려하고, 이름 조회의 결과는 고려하지 않아요. 같은 템플릿의 여러 선언이 이름 조회 결과에서 다르면, 첫 번째 그런 선언이 사용됩니다:

template<class T>
decltype(g(T())) h(); // decltype(g(T())) is a dependent type

int g(int);

template<class T>
decltype(g(T())) h()
{                  // redeclaration of h() uses earlier lookup
    return g(T()); // although the lookup here does find g(int)
}

int i = h<int>(); // template argument substitution fails; g(int)
                  // was not in scope at the first declaration of h()

두 함수 템플릿은 다음 경우 동등하다고 간주돼요:

  • 같은 스코프에서 선언됐음
  • 같은 이름을 가짐
  • 동등한 템플릿 매개변수 목록을 가짐. 즉 목록의 길이가 같고, 각 대응 매개변수 쌍에 대해 다음이 모두 참:
    • 두 매개변수가 같은 종류(둘 다 타입, 둘 다 비타입, 둘 다 템플릿)
    • 둘 다 매개변수 팩이거나 둘 다 아님(C++11부터)
    • 비타입이면 타입이 동등하고, 템플릿이면 템플릿 매개변수가 동등
    • 하나가 concept-name으로 선언됐으면 둘 다 그렇고, concept-name이 동등(C++20부터)
  • 반환 타입과 매개변수 목록에서 템플릿 매개변수를 포함하는 표현식이 동등
  • requires-clause의 표현식이(있으면) 동등 — 템플릿 매개변수 목록을 따르는 것과 함수 declarator를 따르는 것 모두 (C++20부터)

템플릿 매개변수를 포함하는 두 잠재적으로 평가되는(C++20부터) 표현식은 동등하지 않지만, 어떤 주어진 템플릿 인자 집합에 대해 두 표현식의 평가가 같은 값을 내면 기능적으로 동등(functionally equivalent)하다고 해요.

두 함수 템플릿은, 동등하지만 반환 타입과 매개변수 목록에서 템플릿 매개변수를 포함하는 하나 이상의 표현식이 기능적으로 동등하면 기능적으로 동등하다고 간주돼요.

추가로, 두 함수 템플릿은 제약이 다르게 지정됐지만 같은 집합의 템플릿 인자 목록을 받아들이고 만족하면 기능적으로 동등하지만 동등하지 않아요. (C++20부터)

프로그램이 기능적으로 동등하지만 동등하지 않은 함수 템플릿 선언을 포함하면 ill-formed이며 진단은 요구되지 않아요.

// equivalent
template<int I>
void f(A<I>, A<I+10>); // overload #1
template<int I>
void f(A<I>, A<I+10>); // redeclaration of overload #1

// not equivalent
template<int I>
void f(A<I>, A<I+10>); // overload #1
template<int I>
void f(A<I>, A<I+11>); // overload #2

// functionally-equivalent but not equivalent
// This program is ill-formed, no diagnostic required
template<int I>
void f(A<I>, A<I+10>);      // overload #1
template<int I>
void f(A<I>, A<I+1+2+3+4>); // functionally equivalent

같은 함수 템플릿 특수화가 둘 이상의 오버로드된 함수 템플릿과 일치하면(그런 일은 종종 템플릿 인자 추론에서 생겨요), 가장 좋은 일치를 고르기 위해 오버로드된 함수 템플릿의 부분 순서(partial ordering) 지정이 수행돼요.

구체적으로, 부분 순서 지정은 다음 상황에서 일어납니다:

  1. 함수 템플릿 특수화에 대한 호출의 오버로드 해석:
template<class X> void f(X a);
template<class X> void f(X* a);
int* p;
f(p);
  1. 함수 템플릿 특수화의 주소를 구할 때:
template<class X> void f(X a);
template<class X> void f(X* a);
void (*p)(int*) = &f;
  1. 배치 operator delete(함수 템플릿 특수화)가 배치 operator new와 일치하도록 선택될 때
  2. friend 함수 선언, 명시적 인스턴스화, 명시적 특수화가 함수 템플릿 특수화를 가리킬 때:
template<class X> void f(X a);   // first template f
template<class X> void f(X* a);  // second template f
template<> void f<>(int* a) {}   // explicit specialization
// template argument deduction comes up with two candidates:
// f<int*>(int*) and f<int>(int*)
// partial ordering selects f<int>(int*) as more specialized

비공식적으로 "A가 B보다 더 특수화됨"은 "A가 B보다 더 적은 타입을 받아들임"을 뜻해요. 공식적으로는, 두 함수 템플릿 중 어느 것이 더 특수화됐는지 결정하기 위해 부분 순서 과정이 먼저 두 템플릿 중 하나를 다음과 같이 변환해요:

  • 각 타입·비타입·템플릿 매개변수(매개변수 팩 포함, C++11부터)에 대해 고유한 허구 타입·값·템플릿을 생성해 그 템플릿의 함수 타입에 치환한다.
  • 비교하는 두 함수 템플릿 중 하나만 멤버 함수이고 그것이 어떤 클래스 A의 비정적 멤버라면, 그 매개변수 목록에 새 첫 매개변수가 삽입된다. 함수 템플릿의 cv 한정자를 cv, ref 한정자(C++11부터)를 ref라고 할 때, 새 매개변수 타입은 cv A&이다. 단 ref가 &&이거나, ref가 없고 다른 템플릿의 첫 매개변수가 rvalue 참조 타입이면 타입은 cv A&&다(C++11부터). 이는 멤버와 비멤버 함수로 모두 조회되는 연산자의 순서 지정을 돕는다:
struct A {};

template<class T>
struct B
{
    template<class R>
    int operator*(R&); // #1
};

template<class T, class R>
int operator*(T&, R&); // #2

int main()
{
    A a;
    B<A> b;
    b * a; // template argument deduction for int B<A>::operator*(R&) gives R=A
           //                             for int operator*(T&, R&), T=B<A>, R=A

    // For the purpose of partial ordering, the member template B<A>::operator*
    // is transformed into template<class R> int operator*(B<A>&, R&);
    
    // partial ordering between
    //     int operator*(   T&, R&)  T=B<A>, R=A
    // and int operator*(B<A>&, R&)  R=A
    // selects int operator*(B<A>&, A&) as more specialized
}

두 템플릿 중 하나를 위처럼 변환한 후, 변환된 템플릿을 인자 템플릿으로, 다른 템플릿의 원래 타입을 매개변수 템플릿으로 사용해 템플릿 인자 추론이 실행돼요. 그런 다음 두 번째 템플릿(변환 후)을 인자로, 첫 번째 템플릿을 원래 형태로 매개변수로 사용해 과정이 반복됩니다.

순서를 결정하는 데 쓰이는 타입은 문맥에 따라 달라져요:

  • 함수 호출 문맥에서, 함수 호출에 인자가 있는 함수 매개변수 타입들이 그것(기본 함수 인자, 매개변수 팩(C++11부터), 생략 부호 매개변수는 고려하지 않음 — 아래 예시 참고)
  • 사용자 정의 변환 함수 호출 문맥에서, 변환 함수 템플릿의 반환 타입
  • 다른 문맥에서, 함수 템플릿 타입

매개변수 템플릿의 위 목록 각 타입이 추론돼요. 추론이 시작되기 전에 매개변수 템플릿의 각 매개변수 P와 인자 템플릿의 대응 인자 A가 다음과 같이 조정됩니다:

  • P와 A가 둘 다 참조 타입이었다면, 어느 쪽이 더 cv-한정됐는지 결정(그 외의 경우 부분 순서 목적에서는 cv-한정을 무시)
  • P가 참조 타입이면, 가리키는 타입으로 대체
  • A가 참조 타입이면, 가리키는 타입으로 대체
  • P가 cv-한정이면, P는 cv-한정되지 않은 버전으로 대체
  • A가 cv-한정이면, A는 cv-한정되지 않은 버전으로 대체

이 조정 후, P from A 추론은 타입에서의 템플릿 인자 추론에 따라 수행돼요.

P가 함수 매개변수 팩이면, 인자 템플릿의 각 나머지 매개변수 타입 A가 함수 매개변수 팩의 declarator-id 타입 P와 비교돼요. 각 비교는 함수 매개변수 팩이 확장하는 템플릿 매개변수 팩의 이후 위치에 대한 템플릿 인자를 추론해요. A가 함수 매개변수 팩에서 변환됐다면, 매개변수 템플릿의 각 나머지 매개변수 타입과 비교됩니다. (C++11부터)

변환된 template-1의 인자 A가 template-2의 대응 매개변수 P를 추론하는 데 쓰일 수 있지만 그 반대는 안 된다면, 이 P/A 쌍이 추론하는 타입(들)에 관해 A가 P보다 더 특수화된 거예요. 양방향으로 추론이 성공하고 원래 P와 A가 참조 타입이었다면, 추가 테스트가 이뤄져요:

  • A가 lvalue 참조이고 P가 rvalue 참조였다면, A가 P보다 더 특수화된 것으로 간주
  • A가 P보다 더 cv-한정됐다면, A가 P보다 더 특수화된 것으로 간주
  • 그 외의 경우, 이 P/A 쌍이 추론하는 타입(들)에 관해 어느 템플릿도 다른 것보다 더 특수화되지 않음

양방향으로 모든 P와 A를 고려한 후, 고려된 각 타입에 대해:

  • template-1이 모든 타입에 대해 template-2만큼 이상 특수화되고
  • template-1이 일부 타입에 대해 template-2보다 더 특수화되고
  • template-2가 어떤 타입에 대해서도 template-1보다 더 특수화되지 않거나, 어떤 타입에 대해서도 template-1만큼 이상 특수화되지 않으면

template-1이 template-2보다 더 특수화된 거예요. 템플릿 순서를 바꾼 뒤 위 조건이 참이면 template-2가 더 특수화된 거고, 그렇지 않으면 어느 쪽도 더 특수화되지 않아요.

동률(tie)일 때, 한 함수 템플릿에 후행 매개변수 팩이 있고 다른 쪽에는 없다면, 매개변수를 생략한 쪽이 빈 매개변수 팩을 가진 쪽보다 더 특수화된 것으로 간주돼요. (C++11부터)

모든 오버로드된 템플릿 쌍을 고려한 후, 다른 모든 것보다 명확히 더 특수화된 것이 하나 있으면 그 템플릿의 특수화가 선택되고, 그렇지 않으면 컴파일이 실패해요.

다음 예시에서 허구 인자를 U1, U2라고 부를게요:

template<class T>
void f(T);        // template #1
template<class T>
void f(T*);       // template #2
template<class T>
void f(const T*); // template #3

void m()
{
    const int* p;
    f(p); // overload resolution picks: #1: void f(T ) [T = const int *]
          //                            #2: void f(T*) [T = const int]
          //                            #3: void f(const T *) [T = int]

    // partial ordering:
    
    // #1 from transformed #2: void(T) from void(U1*): P=T A=U1*: deduction ok: T=U1*
    // #2 from transformed #1: void(T*) from void(U1): P=T* A=U1: deduction fails
    // #2 is more specialized than #1 with regards to T
    
    // #1 from transformed #3: void(T) from void(const U1*): P=T, A=const U1*: ok
    // #3 from transformed #1: void(const T*) from void(U1): P=const T*, A=U1: fails
    // #3 is more specialized than #1 with regards to T
    
    // #2 from transformed #3: void(T*) from void(const U1*): P=T* A=const U1*: ok
    // #3 from transformed #2: void(const T*) from void(U1*): P=const T* A=U1*: fails
    // #3 is more specialized than #2 with regards to T
    
    // result: #3 is selected
    // in other words, f(const T*) is more specialized than f(T) or f(T*)
}

template<class T>
void f(T, T*);   // #1
template<class T>
void f(T, int*); // #2

void m(int* p)
{
    f(0, p); // deduction for #1: void f(T, T*) [T = int]
             // deduction for #2: void f(T, int*) [T = int]
    
    // partial ordering:
    
    // #1 from #2: void(T,T*) from void(U1,int*): P1=T, A1=U1: T=U1
    //                                            P2=T*, A2=int*: T=int: fails
    
    // #2 from #1: void(T,int*) from void(U1,U2*): P1=T A1=U1: T=U1
    //                                             P2=int* A2=U2*: fails
    
    // neither is more specialized w.r.t T, the call is ambiguous
}

template<class T>
void g(T);  // template #1
template<class T>
void g(T&); // template #2

void m()
{
    float x;
    g(x); // deduction from #1: void g(T ) [T = float]
          // deduction from #2: void g(T&) [T = float]
    
    // partial ordering:
    
    // #1 from #2: void(T) from void(U1&): P=T, A=U1 (after adjustment), ok
    
    // #2 from #1: void(T&) from void(U1): P=T (after adjustment), A=U1: ok
    
    // neither is more specialized w.r.t T, the call is ambiguous
}

template<class T>
struct A { A(); };

template<class T>
void h(const T&); // #1
template<class T>
void h(A<T>&);    // #2

void m()
{
    A<int> z;
    h(z); // deduction from #1: void h(const T &) [T = A<int>]
          // deduction from #2: void h(A<T> &) [T = int]
    
    // partial ordering:
    
    // #1 from #2: void(const T&) from void(A<U1>&): P=T A=A<U1>: ok T=A<U1>
    
    // #2 from #1: void(A<T>&) from void(const U1&): P=A<T> A=const U1: fails
    
    // #2 is more specialized than #1 w.r.t T
    
    const A<int> z2;
    h(z2); // deduction from #1: void h(const T&) [T = A<int>]
           // deduction from #2: void h(A<T>&) [T = int], but substitution fails
    
    // only one overload to choose from, partial ordering not tried, #1 is called
}

호출 문맥은 명시적 호출 인자가 있는 매개변수만 고려하므로, 명시적 호출 인자가 없는 함수 매개변수 팩(C++11부터), 생략 부호 매개변수, 기본 인자가 있는 매개변수는 무시돼요:

template<class T>
void f(T);         // #1
template<class T>
void f(T*, int = 1); // #2

void m(int* ip)
{
    int* ip;
    f(ip); // calls #2 (T* is more specialized than T)
}

template<class T>
void g(T);       // #1
template<class T>
void g(T*, ...); // #2

void m(int* ip)
{
    g(ip); // calls #2 (T* is more specialized than T)
}

template<class T, class U>
struct A {};

template<class T, class U>
void f(U, A<U, T>* p = 0); // #1
template<class U>
void f(U, A<U, U>* p = 0); // #2

void h()
{
    f<int>(42, (A<int, int>*)0); // calls #2
    f<int>(42);                  // error: ambiguous
}

template<class T>
void g(T, T = T()); // #1
template<class T, class... U>
void g(T, U...);    // #2

void h()
{
    g(42); // error: ambiguous
}

template<class T, class... U>
void f(T, U...); // #1
template<class T>
void f(T);       // #2

void h(int i)
{
    f(&i); // calls #2 due to the tie-breaker between parameter pack and no parameter
           // (note: was ambiguous between DR692 and DR1395)
}

template<class T, class... U>
void g(T*, U...); // #1
template<class T>
void g(T);        // #2

void h(int i)
{
    g(&i); // OK: calls #1 (T* is more specialized than T)
}

template<class... T>
int f(T*...);    // #1
template<class T>
int f(const T&); // #2

f((int*)0); // OK: selects #2; non-variadic template is more specialized than
            // variadic template (was ambiguous before DR1395 because deduction
            // failed in both directions)

template<class... Args>
void f(Args... args);        // #1
template<class T1, class... Args>
void f(T1 a1, Args... args); // #2
template<class T1, class T2>
void f(T1 a1, T2 a2);        // #3

f();        // calls #1
f(1, 2, 3); // calls #2
f(1, 2);    // calls #3; non-variadic template #3 is more
            // specialized than the variadic templates #1 and #2

부분 순서 과정 안에서의 템플릿 인자 추론 중에는, 인자가 부분 순서에 고려된 어떤 타입에도 사용되지 않으면 템플릿 매개변수를 인자와 일치시킬 필요가 없어요:

template<class T>
T f(int); // #1
template<class T, class U>
T f(U);   // #2

void g()
{
    f<int>(1); // specialization of #1 is explicit: T f(int) [T = int]
               // specialization of #2 is deduced:  T f(U) [T = int, U = int]
    
    // partial ordering (only considering the argument type):
    
    // #1 from #2: T(int) from U1(U2): fails
    // #2 from #1: T(U) from U1(int): ok: U=int, T unused
    
    // calls #1
}

템플릿 매개변수 팩을 포함하는 함수 템플릿의 부분 순서 지정은 그 템플릿 매개변수 팩에 대한 추론된 인자의 개수와 무관해요:

template<class...>
struct Tuple {};

template<class... Types>
void g(Tuple<Types...>);      // #1
template<class T1, class... Types>
void g(Tuple<T1, Types...>);  // #2
template<class T1, class... Types>
void g(Tuple<T1, Types&...>); // #3

g(Tuple<>());            // calls #1
g(Tuple<int, float>());  // calls #2
g(Tuple<int, float&>()); // calls #3
g(Tuple<int>());         // calls #3

(C++11부터)

함수 템플릿에 대한 호출을 컴파일하려면 컴파일러가 비템플릿 오버로드, 템플릿 오버로드, 템플릿 오버로드의 특수화 사이를 결정해야 해요:

template<class T>
void f(T);      // #1: template overload
template<class T>
void f(T*);     // #2: template overload

void f(double); // #3: non-template overload
template<>
void f(int);    // #4: specialization of #1

f('a');        // calls #1
f(new int(1)); // calls #2
f(1.0);        // calls #3
f(1);          // calls #4

함수 오버로드 vs 함수 특수화

오버로드 해석에는 비템플릿과 기본 템플릿 오버로드만 참여한다는 점을 기억하세요. 특수화는 오버로드가 아니며 고려되지 않아요. 오버로드 해석이 가장 잘 맞는 기본 함수 템플릿을 선택한 후에야, 그 특수화가 더 좋은 일치인지 살펴봅니다:

template<class T>
void f(T);    // #1: overload for all types
template<>
void f(int*); // #2: specialization of #1 for pointers to int
template<class T>
void f(T*);   // #3: overload for all pointer types

f(new int(1)); // calls #3, even though specialization of #1 would be a perfect match

번역 단위의 헤더 파일 순서를 정할 때 이 규칙을 기억하는 게 중요해요. 함수 오버로드와 함수 특수화의 상호작용에 대한 더 많은 예시는 아래에서 볼 수 있습니다.

예제

먼저 인자 종속 조회를 사용하지 않는 몇 가지 시나리오를 볼게요. 이를 위해 (f)(t) 호출을 사용합니다. ADL 문서에 설명된 대로, 함수 이름을 괄호로 감싸면 인자 종속 조회를 억제해요.

g()에서 참조 지점(POR) 이전에 선언된 f()의 여러 오버로드:

#include <iostream>

struct A {};

template<class T>
void f(T)  { std::cout << "#1\n"; } // overload #1 before f() POR
template<class T>
void f(T*) { std::cout << "#2\n"; } // overload #2 before f() POR

template<class T>
void g(T* t)
{
    (f)(t); // f() POR
}

int main()
{
    A* p = nullptr;
    g(p); // POR of g() and f()
}

// Both #1 and #2 are added to the candidate list;
// #2 is selected because it is a better match.

출력:

#2

더 잘 맞는 템플릿 오버로드가 POR 이후에 선언된 경우. 이때는 ADL 덕분에 후보 목록에 추가돼요:

#include <iostream>

struct A {};

template<class T>
void f(T)    { std::cout << "#1\n"; } // #1

template<class T>
void g(T* t)
{
    f(t); // f() POR
}

template<class T>
void f(T*)   { std::cout << "#2\n"; } // #2
template<>
void f<>(A*) { std::cout << "#3\n"; } // #3

int main()
{
    A* p = nullptr;
    g(p); // POR of g() and f()
}

// #1 is added to the candidate list as a result of the ordinary lookup;
// #2 is defined after POR but it is added to the candidate list via ADL lookup.
// #2 is selected among the primary templates, being the better match.
// Since #3 is declared after #2, it is an explicit specialization of #2;
// therefore, selected as the function to call.

출력:

#3

인자가 C++ 기본 타입이면 ADL 관련 네임스페이스가 없어요. 따라서 그런 시나리오는 위의 비ADL 예시들과 동일합니다.

오버로드 해석의 상세 규칙은 오버로드 해석 문서를 참고하세요.

함수 템플릿 특수화

이 섹션은 불완전해요. (부분 특수화가 없다는 점, 함수 오버로드와의 상호작용 등 함수 고유의 특수화 규칙이 명시 특수화 문서에 이미 있어요.)

키워드

template, extern (C++11부터)

더 알아보기 (Learn more)