템플릿 인자 추론
템플릿 인자 추론 (Template argument deduction)
함수 템플릿을 호출할 때 모든 템플릿 인자를 일일이 쓰지 않아도 되는 이유가, 컴파일러가 함수 인자에서 템플릿 인자를 추론해 주기 때문이에요. 이 페이지에서는 템플릿 인자 추론이 어떤 절차로 일어나는지, 그리고 어떤 맥락에서 추론이 일어나고 어떤 경우에 추론이 실패하는지 정리할게요.
출처: cppreference
본문
함수 템플릿을 인스턴스화하려면 모든 템플릿 인자를 알아야 하지만, 모든 템플릿 인자를 지정할 필요는 없어요. 가능하면 컴파일러가 함수 인자에서 빠진 템플릿 인자를 추론해요. 이는 함수 호출을 시도할 때, 함수 템플릿의 주소를 취할 때, 그리고 그 외 몇몇 맥락에서 일어나요:
template<typename To, typename From>
To convert(From f);
void g(double d)
{
int i = convert<int>(d); // convert<int, double>(double) 호출
char c = convert<char>(d); // convert<char, double>(double) 호출
int(*ptr)(float) = convert; // convert<int, float>(float)를 인스턴스화하고
// 그 주소를 ptr에 저장
}
이 메커니즘 덕분에 템플릿 연산자도 쓸 수 있어요. 연산자는 함수 호출 표현식으로 다시 쓰는 방법 외에는 템플릿 인자를 지정할 문법이 없거든요:
#include <iostream>
int main()
{
std::cout << "Hello, world" << std::endl;
// operator<<는 ADL로 std::operator<<로 조회되고,
// 두 번 모두 operator<<<char, std::char_traits<char>>로 추론됨
// std::endl은 &std::endl<char, std::char_traits<char>>로 추론됨
}
템플릿 인자 추론은 함수 템플릿 이름 조회(인자 의존 조회(ADL) 포함 가능) 이후, 그리고 템플릿 인자 치환(SFINAE 포함 가능)과 오버로드 결정 이전에 일어나요.
클래스 템플릿의 이름이 생성 중인 객체의 타입으로 쓰일 때도 템플릿 인자 추론이 수행돼요: cpp std::pair p(2, 4.5); std::tuple t(4, 3, 2.5); std::copy_n(vi1, 3, std::back_insert_iterator(vi2)); std::for_each(vi.begin(), vi.end(), Foo([&](int i) {...})); auto lck = std::lock_guard(foo.mtx); std::lock_guard lck2(foo.mtx, ul); 클래스 템플릿에 대한 템플릿 인자 추론은 선언과 명시적 캐스트 표현식에서 일어나요. 자세한 내용은 클래스 템플릿 인자 추론을 봐요. |
(C++17부터) |
|---|
함수 호출에서의 추론 (Deduction from a function call)
템플릿 인자 추론은 템플릿 인자(타입 템플릿 매개변수 Ti의 타입, 템플릿 템플릿 매개변수 TTi의 템플릿, 비-타입 템플릿 매개변수 Ii의 값)를 결정하려 시도해요. 이것들을 각 매개변수 P에 치환하면, 아래 나열된 조정 이후 인자 A의 타입과 같은 타입인 추론된A가 만들어져요.
매개변수가 여러 개면 각 P/A 쌍이 별도로 추론되고, 추론된 템플릿 인자들이 그다음 합쳐져요. 어떤 P/A 쌍에 대해 추론이 실패하거나 모호하거나, 다른 쌍이 서로 다른 추론 결과를 내거나, 어떤 템플릿 인자도 추론되거나 명시적으로 지정되지 않은 채 남아 있으면 컴파일이 실패해요.
P에서 참조와 cv-한정을 제거하면 std::initializer_list<P'>가 되고 A가 중괄호 초기화 목록이면, 이니셜라이저 목록의 모든 요소에 대해 P'를 매개변수로, 목록 요소 A'를 인자로 하는 추론이 수행돼요: cpp template<class T> void f(std::initializer_list<T>); f({1, 2, 3}); // P = std::initializer_list<T>, A = {1, 2, 3} // P'1 = T, A'1 = 1: 추론 T = int // P'2 = T, A'2 = 2: 추론 T = int // P'3 = T, A'3 = 3: 추론 T = int // OK: 추론 T = int f({1, "abc"}); // P = std::initializer_list<T>, A = {1, "abc"} // P'1 = T, A'1 = 1: 추론 T = int // P'2 = T, A'2 = "abc": 추론 T = const char* // 오류: 추론 실패, T가 모호함 P에서 참조와 cv-한정을 제거하면 P'[N]이 되고 A가 비-빈 중괄호 초기화 목록이면, 위처럼 추론이 수행돼요. 단 N이 비-타입 템플릿 매개변수면 이니셜라이저 목록의 길이에서 추론돼요: cpp template<class T, int N> void h(T const(&)[N]); h({1, 2, 3}); // 추론 T = int, 추론 N = 3 template<class T> void j(T const(&)[3]); j({42}); // 추론 T = int, 배열 경계는 매개변수가 아니므로 고려 안 됨 struct Aggr { int i; int j; }; template<int N> void k(Aggr const(&)[N]); k({1, 2, 3}); // 오류: 추론 실패, int에서 Aggr로 변환 없음 k({{1}, {2}, {3}}); // OK: 추론 N = 3 template<int M, int N> void m(int const(&)[M][N]); m({{1, 2}, {3, 4}}); // 추론 M = 2, 추론 N = 2 template<class T, int N> void n(T const(&)[N], T); n({{1}, {2}, {3}}, Aggr()); // 추론 T = Aggr, 추론 N = 3 매개변수 팩이 마지막 P로 나타나면, 타입 P는 호출의 각 남은 인자의 타입 A와 매칭돼요. 각 매칭은 팩 확장에서 다음 위치의 템플릿 인자를 추론해요: cpp template<class... Types> void f(Types&...); void h(int x, float& y) { const int z = x; f(x, y, z); // P = Types&..., A1 = x: Types...의 첫 멤버 = int 추론 // P = Types&..., A2 = y: Types...의 두 번째 멤버 = float 추론 // P = Types&..., A3 = z: Types...의 세 번째 멤버 = const int 추론 // f<int, float, const int> 호출 } |
(C++11부터) |
|---|
P가 함수 타입, 함수 포인터 타입, 멤버 함수 포인터 타입이고 A가 함수 템플릿을 담지 않은 오버로드 함수 집합이면, 각 오버로드로 템플릿 인자 추론이 시도돼요. 하나만 성공하면 그 추론이 사용돼요. 없거나 둘 이상 성공하면 그 템플릿 매개변수는 비-추론 맥락(아래 참고)이 돼요:
template<class T> int f(T(*p)(T));
int g(int);
int g(char);
f(g); // P = T(*)(T), A = 오버로드 집합
// P = T(*)(T), A1 = int(int): 추론 T = int
// P = T(*)(T), A2 = int(char): T 추론 실패
// 하나의 오버로드만 동작하므로 추론 성공
추론이 시작되기 전에 P와 A에 다음 조정이 가해져요:
P가 참조 타입이 아니면,
a) A가 배열 타입이면, A는 배열-to-포인터 변환으로 얻은 포인터 타입으로 대체돼요;
b) 그렇지 않고 A가 함수 타입이면, A는 함수-to-포인터 변환으로 얻은 포인터 타입으로 대체돼요;
c) 그렇지 않고 A가 cv-한정 타입이면, 최상위 cv-한정은 추론에서 무시돼요:
template<class T> void f(T);
int a[3];
f(a); // P = T, A = int[3], int*로 조정: 추론 T = int*
void b(int);
f(b); // P = T, A = void(int), void(*)(int)로 조정: 추론 T = void(*)(int)
const int c = 13;
f(c); // P = T, A = const int, int로 조정: 추론 T = int
-
P가 cv-한정 타입이면, 최상위 cv-한정은 추론에서 무시돼요. -
P가 참조 타입이면, 참조된 타입이 추론에 사용돼요. -
P가 cv-비한정 템플릿 매개변수에 대한 rvalue 참조(forwarding references)이고 대응 함수 호출 인자가 lvalue면, 추론에A대신A에 대한 lvalue 참조 타입이 사용돼요(참고: 이것이 std::forward 동작의 기초예요. 클래스 템플릿 인자 추론에서는 클래스 템플릿의 템플릿 매개변수가 forwarding reference가 될 수 없어요(C++17부터)):
template<class T> int f(T&&); // P는 cv-비한정 T에 대한 rvalue 참조 (forwarding reference)
template<class T> int g(const T&&); // P는 cv-한정 T에 대한 rvalue 참조 (특별하지 않음)
int main()
{
int i;
int n1 = f(i); // 인자가 lvalue: f<int&>(int&) 호출 (특수한 경우)
int n2 = f(0); // 인자가 lvalue 아님: f<int>(int&&) 호출
// int n3 = g(i); // 오류: g<int>(const int&&)로 추론되는데,
// rvalue 참조를 lvalue에 못 묶음
}
이 변환들 후에, 추론은 아래(타입에서의 추론 절 참고)처럼 진행되고, 추론된 A(즉 위 조정 후의 P에 추론된 템플릿 매개변수를 치환한 것)가 변환된A(즉 위 조정 후의 A)와 동일하게 만드는 템플릿 인자를 찾으려 시도해요.
P와 A에서의 평소 추론이 실패하면, 다음 대안을 추가로 고려해요:
P가 참조 타입이면, 추론된A(즉 참조가 가리키는 타입)는 변환된A보다 더 많이 cv-한정될 수 있어요:
template<typename T> void f(const T& t);
bool a = false;
f(a); // P = const T&, const T로 조정, A = bool:
// 추론 T = bool, 추론 A = const bool
// 추론 A는 A보다 더 많이 cv-한정됨
- 변환된
A는 한정 변환이나 함수 포인터 변환(C++17부터)으로 추론된A로 변환될 수 있는 다른 포인터/멤버 포인터 타입일 수 있어요:
template<typename T> void f(const T*);
int* p;
f(p); // P = const T*, A = int*:
// 추론 T = int, 추론 A = const int*
// 한정 변환이 적용됨 (int*에서 const int*로)
P가 클래스이고P가 simple-template-id(템플릿 인자 목록이 있는 템플릿 이름) 또는 템플릿 인자 목록이 있는 splice 지정자(C++26부터) 형태면, 변환된A는 추론된A의 파생 클래스일 수 있어요. 마찬가지로P가 simple-template-id 형태의 클래스에 대한 포인터이면, 변환된A는 추론된A가 가리키는 파생 클래스에 대한 포인터일 수 있어요:
template<class T> struct B {};
template<class T> struct D : public B<T> {};
template<class T> void f(B<T>&) {}
void f()
{
D<int> d;
f(d); // P = B<T>&, B<T>로 조정 (simple-template-id), A = D<int>:
// 추론 T = int, 추론 A = B<int>
// A는 추론 A에서 파생됨
}
비-추론 맥락 (Non-deduced contexts)
다음 경우에서 P를 구성하는 데 쓰인 타입, 템플릿, 비-타입 값은 템플릿 인자 추론에 참여하지 않아요. 대신 다른 곳에서 추론됐거나 명시적으로 지정된 템플릿 인자를 사용해요. 템플릿 매개변수가 비-추론 맥락에서만 쓰이고 명시적으로 지정되지 않으면 템플릿 인자 추론이 실패해요.
- 한정 식별자로 지정된 타입의 중첩 이름 지정자(범위 결정 연산자
::왼쪽의 모든 것):
// identity 템플릿. 특정 인자를 추론에서 제외하는 데 자주 쓰임
// (C++20부터 std::type_identity로 사용 가능)
template<typename T> struct identity { typedef T type; };
template<typename T> void bad(std::vector<T> x, T value = 1);
template<typename T> void good(std::vector<T> x, typename identity<T>::type value = 1);
std::vector<std::complex<double>> x;
bad(x, 1.2); // P1 = std::vector<T>, A1 = std::vector<std::complex<double>>
// P1/A1: 추론 T = std::complex<double>
// P2 = T, A2 = double
// P2/A2: 추론 T = double
// 오류: 추론 실패, T가 모호함
good(x, 1.2); // P1 = std::vector<T>, A1 = std::vector<std::complex<double>>
// P1/A1: 추론 T = std::complex<double>
// P2 = identity<T>::type, A2 = double
// P2/A2: P2에서 T가 :: 왼쪽에 있으므로 P1/A1이 추론한 T를 사용
// OK: T = std::complex<double>
2) 팩 인덱싱 지정자 또는 팩 인덱싱 표현식: cpp template<typename... Ts> void f(Ts...[0], std::tuple<Ts...>); f(3, std::tuple(5, 'A')); // P2 = std::tuple<Ts...>, A2 = std::tuple<int, char> // P2/A2: Ts...의 첫 멤버 = int 추론 // P2/A2: Ts...의 두 번째 멤버 = char 추론 // P1 = Ts...[0], A1 = int: Ts...[0]은 비-추론 맥락 |
(C++26부터) |
|---|---|
3) decltype-지정자의 표현식: cpp template<typename T> void f(decltype(*std::declval<T>()) arg); int n; f<int*>(n); // P = decltype(*declval<T>()), A = int: T는 비-추론 맥락 |
(C++11부터) |
4) splice 지정자의 표현식: cpp template<std::meta::info R> void f(typename[:R:] arg); int n; f<^^int>(n); // P = typename[:R:], A = ^^int: R은 비-추론 맥락 |
(C++26부터) |
- 하위 표현식이 템플릿 매개변수를 참조하는 비-타입 템플릿 인자 또는 배열 경계:
template<std::size_t N> void f(std::array<int, 2 * N> a);
std::array<int, 10> a;
f(a); // P = std::array<int, 2 * N>, A = std::array<int, 10>:
// 2 * N은 비-추론 맥락, N을 추론할 수 없음
// 참고: f(std::array<int, N> a)면 N을 추론할 수 있을 것
- 인자 추론이 이루어지는 호출에서 사용되고 있는 기본 인자를 가진 함수 매개변수의 매개변수 타입에 사용된 템플릿 매개변수:
template<typename T, typename F>
void f(const std::vector<T>& v, const F& comp = std::less<T>());
std::vector<std::string> v(3);
f(v); // P1 = const std::vector<T>&, A1 = std::vector<std::string> lvalue
// P1/A1 추론 T = std::string
// P2 = const F&, A2 = std::less<std::string> rvalue
// P2는 함수 매개변수 comp의 매개변수 타입(const F&)에 쓰인
// 템플릿 매개변수 F에 대한 비-추론 맥락이고,
// 그 매개변수는 f(v) 호출에서 사용되는 기본 인자를 가짐
A가 하나보다 많은 함수가P와 매칭되거나 어떤 함수도 매칭되지 않거나, 오버로드 집합이 하나 이상의 함수 템플릿을 포함하는 함수/오버로드 집합인 매개변수P:
template<typename T> void out(const T& value)
{
std::cout << value;
}
out("123"); // P = const T&, A = const char[4] lvalue: 추론 T = char[4]
out(std::endl); // P = const T&, A = 함수 템플릿: T는 비-추론 맥락
A가 중괄호 초기화 목록인데P가 std::initializer_list, 그에 대한 참조((cv-한정 가능)), 배열에 대한 참조가 아닌 매개변수P:
template<class T> void g1(std::vector<T>);
template<class T> void g2(std::vector<T>, T x);
g1({1, 2, 3}); // P = std::vector<T>, A = {1, 2, 3}: T는 비-추론 맥락
// 오류: T가 명시되거나 다른 P/A에서 추론되지 않음
g2({1, 2, 3}, 10); // P1 = std::vector<T>, A1 = {1, 2, 3}: T는 비-추론 맥락
// P2 = T, A2 = int: 추론 T = int
9) 매개변수 목록의 끝에 나타나지 않는 매개변수 팩인 매개변수 P: cpp template<class... Ts, class T> void f1(T n, Ts... args); template<class... Ts, class T> void f2(Ts... args, T n); f1(1, 2, 3, 4); // P1 = T, A1 = 1: 추론 T = int // P2 = Ts..., A2 = 2, A3 = 3, A4 = 4: 추론 Ts = [int, int, int] f2(1, 2, 3, 4); // P1 = Ts...: Ts는 비-추론 맥락 10) 매개변수 P 안에 나타나는 템플릿 매개변수 목록으로, 템플릿 매개변수 목록의 맨 끝에 있지 않은 팩 확장을 포함하는 것: cpp template<int...> struct T {}; template<int... Ts1, int N, int... Ts2> void good(const T<N, Ts1...>& arg1, const T<N, Ts2...>&); template<int... Ts1, int N, int... Ts2> void bad(const T<Ts1..., N>& arg1, const T<Ts2..., N>&); T<1, 2> t1; T<1, -1, 0> t2; good(t1, t2); // P1 = const T<N, Ts1...>&, A1 = T<1, 2>: // 추론 N = 1, 추론 Ts1 = [2] // P2 = const T<N, Ts2...>&, A2 = T<1, -1, 0>: // 추론 N = 1, 추론 Ts2 = [-1, 0] bad(t1, t2); // P1 = const T<Ts1..., N>&, A1 = T<1, 2>: // <Ts1..., N>은 비-추론 맥락 // P2 = const T<Ts2..., N>&, A2 = T<1, -1, 0>: // <Ts2..., N>은 비-추론 맥락 |
(C++11부터) |
|---|
- 배열 타입(배열에 대한 참조나 배열 포인터는 아님)의
P에 대해, 주요 배열 경계:
template<int i> void f1(int a[10][i]);
template<int i> void f2(int a[i][20]); // P = int[i][20], 배열 타입
template<int i> void f3(int(&a)[i][20]); // P = int(&)[i][20], 배열에 대한 참조
void g()
{
int a[10][20];
f1(a); // OK: 추론 i = 20
f1<20>(a); // OK
f2(a); // 오류: i는 비-추론 맥락
f2<10>(a); // OK
f3(a); // OK: 추론 i = 10
f3<10>(a); // OK
}
어떤 경우든 타입 이름의 일부가 비-추론이면 전체 타입 이름이 비-추론 맥락이에요. 다만 복합 타입은 추론된 타입 이름과 비-추론 타입 이름을 모두 포함할 수 있어요. 예를 들어 A<T>::B<T2>에서 T는 규칙 #1(중첩 이름 지정자) 때문에 비-추론이고, T2는 같은 타입 이름의 일부라서 비-추론이에요. 하지만 void(*f)(typename A<T>::B, A<T>)에서는 A<T>::B의 T는(같은 규칙 때문에) 비-추론인 반면, A<T>의 T는 추론돼요.
타입에서의 추론 (Deduction from a type)
하나 이상의 타입 템플릿 매개변수 Ti, 템플릿 템플릿 매개변수 TTi, 비-타입 템플릿 매개변수 Ii에 의존하는 함수 매개변수 P와 대응 인자 A가 주어졌을 때, P가 다음 형태 중 하나이면 추론이 일어나요:
cv(선택)TT*T&T&&(C++11부터)T(선택)[I(선택)]T(선택)(U(선택))(C++17 이전)T(선택)(U(선택))noexcept(I(선택))(C++17부터)T(선택)U(선택)::*TT(선택)<T>TT(선택)<I>TT(선택)<TU>TT(선택)<>
위 형태에서,
T(선택) 또는U(선택)는, 이 규칙들을 재귀적으로 충족하거나,P나A에서 비-추론 맥락이거나,P와A에서 같은 비-의존 타입인 타입 또는 매개변수 타입 목록을 나타내요.TT(선택) 또는TU(선택)는 클래스 템플릿 또는 템플릿 템플릿 매개변수를 나타내요.I(선택)는,I이거나,P나A에서 값-의존적이거나,P와A에서 같은 상수 값을 가진 표현식을 나타내요.noexcept(I(선택))는 (가능한 암묵적)noexcept지정자의 피연산자가 위I(선택)의 규칙을 충족하는 예외 지정을 나타내요(C++17부터).
P가 템플릿 매개변수 목록 <T>나 <I>를 포함하는 형태 중 하나면, 그 템플릿 인자 목록의 각 요소 Pi는 해당 A의 대응 템플릿 인자 Ai와 매칭돼요. 마지막 Pi가 팩 확장이면, 그 패턴은 A의 템플릿 인자 목록의 각 남은 인자와 비교돼요. 그 외엔 추론되지 않는 꼬리 매개변수 팩은 빈 매개변수 팩으로 추론돼요.
P가 함수 매개변수 목록 (T)를 포함하는 형태 중 하나면, 그 목록의 각 매개변수 Pi는 A의 함수 매개변수 목록의 대응 인자 Ai와 비교돼요. 마지막 Pi가 팩 확장이면, 그 선언자는 A의 매개변수 타입 목록의 각 남은 Ai와 비교돼요.
형태는 중첩되고 재귀적으로 처리될 수 있어요:
X<int>(*)(char[6])는T*의 예인데, 여기서T는X<int>(char[6])이에요.X<int>(char[6])는T(선택)(U(선택))의 예인데, 여기서T는X<int>이고U는char[6]이에요(C++17 이전).X<int>(char[6])는T(선택)(U(선택))noexcept(I(선택))의 예인데, 여기서T는X<int>,U는char[6], 그리고 암묵적noexcept지정자의I는false예요(C++17부터).X<int>는TT(선택)<T>의 예인데, 여기서TT는X이고T는int이에요.char[6]는T(선택)[I(선택)]의 예인데, 여기서T는char이고I는std::size_t(6)이에요.
타입 템플릿 인자는 비-타입 템플릿 인자의 타입에서 추론할 수 없어요: cpp template<typename T, T i> void f(double a[10][i]); double v[10][20]; f(v); // P = double[10][i], A = double[10][20]: // i는 20과 같다고 추론할 수 있음 // 하지만 T는 i의 타입에서 추론할 수 없음 |
(C++17 이전) |
|---|---|
의존 타입으로 선언된 비-타입 템플릿 매개변수 P에 대응하는 인자의 값이 표현식에서 추론될 때, P의 타입에 있는 템플릿 매개변수는 그 값의 타입에서 추론돼요. cpp template<long n> struct A {}; template<class T> struct C; template<class T, T n> struct C<A<n>> { using Q = T; }; typedef long R; typedef C<A<2>>::Q R; // OK: T는 타입 A<2>의 템플릿 인자 값에서 long으로 추론됨 template<auto X> class bar {}; template<class T, T n> void f(bar<n> x); f(bar<3>{}); // OK: T는 int로(그리고 n은 3으로) // 타입 bar<3>의 템플릿 인자 값에서 추론됨 타입 T[N]에서 N의 타입은 std::size_t예요. cpp template<class T, T i> void f(int(&a)[i]); int v[10]; f(v); // OK: T는 std::size_t 함수 타입의 noexcept(B) 지정자에서 B의 타입은 bool이에요. cpp template<bool> struct A {}; template<auto> struct B; template<auto X, void(*F)() noexcept(X)> struct B<F> { A<X> ax; }; void f_nothrow() noexcept; B<f_nothrow> bn; // OK: X는 true로, X의 타입은 bool로 추론됨 |
(C++17부터) |
함수 템플릿의 비-타입 템플릿 매개변수가 (역시 템플릿인) 함수 매개변수의 템플릿 매개변수 목록에 사용되고 대응 템플릿 인자가 추론되면, 추론된 템플릿 인자의 타입은 (그것을 둘러싼 템플릿 매개변수 목록에 지정된 대로, 참조 보존 의미) 비-타입 템플릿 매개변수의 타입과 정확히 일치해야 해요. 단 cv-한정은 버려지고, 템플릿 인자가 배열 경계에서 추론되는 경우는 예외인데 그 경우엔 어떤 정수 타입도 허용돼요(bool조차도, 항상 true가 되겠지만):
template<int i> class A {};
template<short s> void f(A<s>); // 비-타입 템플릿 매개변수의 타입은 short
void k1()
{
A<1> a; // a의 비-타입 템플릿 매개변수 타입은 int
f(a); // P = A<(short)s>, A = A<(int)1>
// 오류: 추론된 비-타입 템플릿 인자가 대응 템플릿 인자와
// 같은 타입이 아님
f<1>(a); // OK: 템플릿 인자가 추론되지 않음,
// 이 호출은 f<(short)1>(A<(short)1>)
}
template<int&> struct X;
template<int& R> void k2(X<R>&);
int n;
void g(X<n>& x)
{
k2(x); // P = X<R>, A = X<n>
// 매개변수 타입은 int&
// X의 템플릿 선언에서 인자 타입은 int&
// OK (CWG 2091): R이 n을 가리키도록 추론
}
타입 템플릿 매개변수는 함수 기본 인자의 타입에서 추론할 수 없어요:
template<typename T> void f(T = 5, T = 7);
void g()
{
f(1); // OK: f<int>(1, 7) 호출
f(); // 오류: T를 추론할 수 없음
f<int>(); // OK: f<int>(5, 7) 호출
}
템플릿 템플릿 매개변수의 추론은 함수 호출에 사용된 템플릿 특수화에 사용된 타입을 사용할 수 있어요:
template<template<typename> class X> struct A {}; // A는 TT 매개변수를 가진 템플릿
template<template<typename> class TT> void f(A<TT>) {}
template<class T> struct B {};
A<B> ab;
f(ab); // P = A<TT>, A = A<B>: 추론 TT = B, f(A<B>) 호출
다른 맥락 (Other contexts)
함수 호출과 연산자 표현식 말고도, 템플릿 인자 추론은 다음 상황에서 사용돼요:
#### auto 타입 추론 템플릿 인자 추론은 auto 지정자의 의미를 변수 이니셜라이저에서 추론할 때 선언에 사용돼요. 매개변수 P는 다음과 같이 얻어요: T(auto를 포함한 변수의 선언된 타입)에서 auto가 나올 때마다 가상의 타입 템플릿 매개변수 U로, 초기화가 복사 목록 초기화면 std::initializer_list<U>로 대체된다. 인자 A는 이니셜라이저 표현식이에요. 위 규칙에 따라 P와 A에서 U를 추론한 뒤, 추론된 U를 P에 치환해 실제 변수 타입을 얻어요: cpp const auto& x = 1 + 2; // P = const U&, A = 1 + 2: // template<class U> void f(const U& u)로 f(1 + 2)를 호출할 때와 같은 규칙 // 추론 U = int, x의 타입은 const int& auto l = { 13 }; // P = std::initializer_list<U>, A = {13}: // 추론 U = int, l의 타입은 std::initializer_list<int> 직접 목록 초기화(복사 목록 초기화는 아님)에서, 중괄호 초기화 목록에서 auto의 의미를 추론할 때 그 중괄호 초기화 목록은 단일 요소만 담아야 하고, auto의 타입은 그 요소의 타입이 돼요: cpp auto x1 = { 3 }; // x1은 std::initializer_list<int> auto x2 { 1, 2 }; // 오류: 단일 요소가 아님 auto x3 { 3 }; // x3은 int // (N3922 이전에는 x2와 x3 모두 std::initializer_list<int>였음) |
(C++11부터) |
|---|---|
#### auto 반환 함수 템플릿 인자 추론은 함수 선언에서, 함수의 반환 타입에 있는 auto 지정자의 의미를 return 문에서 추론할 때 사용돼요. auto 반환 함수에서 매개변수 P는 다음과 같이 얻어요: T(auto를 포함한 함수의 선언된 반환 타입)에서 auto가 나올 때마다 가상의 타입 템플릿 매개변수 U로 대체된다. 인자 A는 return 문의 표현식이고, return 문에 피연산자가 없으면 A는 void()예요. 위 규칙에 따라 P와 A에서 U를 추론한 뒤, 추론된 U를 T에 치환해 실제 반환 타입을 얻어요: cpp auto f() { return 42; } // P = auto, A = 42: // 추론 U = int, f의 반환 타입은 int 그런 함수에 return 문이 여러 개면 각 return 문에 대해 추론이 수행돼요. 모든 결과 타입은 같아야 하고 실제 반환 타입이 돼요. 그런 함수에 return 문이 없으면 추론 시 A는 void()예요. 참고: 변수·함수 선언에서 decltype(auto) placeholder의 의미는 템플릿 인자 추론을 사용하지 않아요. |
(C++14부터) |
| --- | --- |
오버로드 결정 (Overload resolution)
후보 템플릿 함수에서 특수화를 생성할 때 오버로드 결정 도중 템플릿 인자 추론이 사용돼요. P와 A는 일반 함수 호출과 같아요:
std::string s;
std::getline(std::cin, s);
// "std::getline"은 함수 템플릿 4개를 일컫고,
// 그중 2개가 후보 함수(매개변수 개수가 맞음)
// 1번째 후보 템플릿:
// P1 = std::basic_istream<CharT, Traits>&, A1 = std::cin
// P2 = std::basic_string<CharT, Traits, Allocator>&, A2 = s
// 추론이 타입 템플릿 매개변수 CharT, Traits, Allocator를 결정
// 특수화 std::getline<char, std::char_traits<char>, std::allocator<char>>
// 2번째 후보 템플릿:
// P1 = std::basic_istream<CharT, Traits>&&, A1 = std::cin
// P2 = std::basic_string<CharT, Traits, Allocator>&, A2 = s
// 추론이 타입 템플릿 매개변수 CharT, Traits, Allocator를 결정
// 특수화 std::getline<char, std::char_traits<char>, std::allocator<char>>
// 오버로드 결정이 lvalue std::cin에서의 참조 바인딩을 순위 매겨
// 두 후보 특수화 중 첫 번째를 선택
추론이 실패하거나, 추론이 성공해도 그 특수화가 유효하지 않으면(예: 매개변수가 클래스·열거 타입이 아닌 오버로드된 연산자), 그 특수화는 SFINAE와 유사하게 오버로드 집합에 포함되지 않아요.
오버로드 집합의 주소 (Address of an overload set)
함수 템플릿을 포함하는 오버로드 집합의 주소를 취할 때 템플릿 인자 추론이 사용돼요.
함수 템플릿의 함수 타입이 P예요. 대상 타입이 A의 타입이에요:
std::cout << std::endl;
// std::endl은 함수 템플릿을 일컬음
// endl의 타입 P =
// std::basic_ostream<CharT, Traits>& (std::basic_ostream<CharT, Traits>&)
// operator<< 매개변수 A =
// std::basic_ostream<char, std::char_traits<char>>& (*)(
// std::basic_ostream<char, std::char_traits<char>>&
// )
// (operator<<의 다른 오버로드는 viable하지 않음)
// 추론이 타입 템플릿 매개변수 CharT와 Traits를 결정
이 경우 추론에 추가 규칙이 적용돼요: 함수 매개변수 Pi와 Ai를 비교할 때, 어떤 Pi가 cv-비한정 템플릿 매개변수에 대한 rvalue 참조("forwarding reference")이고 대응 Ai가 lvalue 참조면, Pi는 템플릿 매개변수 타입으로 조정돼요(T&&가 T가 됨).
함수 템플릿의 반환 타입이 placeholder(auto나 decltype(auto))면, 그 반환 타입은 비-추론 맥락이고 인스턴스화에서 결정돼요. |
(C++14부터) |
|---|
부분 순서 (Partial ordering)
오버로드된 함수 템플릿의 부분 순서 동안 템플릿 인자 추론이 사용돼요.
변환 함수 템플릿 (Conversion function template)
사용자 정의 변환 함수 템플릿 인자를 선택할 때 템플릿 인자 추론이 사용돼요.
A는 변환 결과로 요구되는 타입이에요. P는 변환 함수 템플릿의 반환 타입이에요. P가 참조 타입이면, 이 절의 다음 부분에서는 참조된 타입이 P 대신 사용돼요.
A가 참조 타입이 아니면:
a) P가 배열 타입이면, 배열-to-포인터 변환으로 얻은 포인터 타입이 P 대신 사용돼요;
b) P가 함수 타입이면, 함수-to-포인터 변환으로 얻은 함수 포인터 타입이 P 대신 사용돼요;
c) P가 cv-한정이면, 최상위 cv-한정이 무시돼요.
A가 cv-한정이면 최상위 cv-한정이 무시돼요. A가 참조 타입이면 참조된 타입이 추론에 사용돼요.
P와 A에서의 (위에서 설명한) 평소 추론이 실패하면, 다음 대안을 추가로 고려해요:
a) A가 참조 타입이면, A는 추론된 A보다 더 많이 cv-한정될 수 있어요;
b) A가 포인터/멤버 포인터 타입이면, 추론된 A는 한정 변환으로 A로 변환될 수 있는 임의의 포인터일 수 있어요:
struct C
{
template<class T> operator T***();
};
C c;
const int* const* const* p1 = c;
// P = T***, A = const int* const* const*
// template<class T> void f(T*** p)를 타입
// const int* const* const*의 인자로 호출한 것 같은
// 일반 함수 호출 추론은 실패
// 변환 함수에 대한 추가 추론은 T = int를 결정
// (추론된 A는 int***, const int* const* const*로 변환 가능)
c) A가 함수 포인터 타입이면, 추론된 A는 함수 포인터 변환으로 A로 변환될 수 있는 noexcept 함수에 대한 포인터일 수 있어요; d) A가 멤버 함수 포인터이면, 추론된 A는 함수 포인터 변환으로 A로 변환될 수 있는 noexcept 멤버 함수에 대한 포인터일 수 있어요. |
(C++17부터) |
|---|
변환 함수 템플릿에 대한 다른 규칙은 멤버 템플릿을 봐요.
명시적 인스턴스화 (Explicit instantiation)
명시적 인스턴스화, 명시적 특수화, 그리고 declarator-id가 함수 템플릿의 특수화를 가리키는 friend 선언(예: friend ostream& operator<<<>(...))에서 템플릿 인자 추론이 사용돼요. 모든 템플릿 인자가 명시적으로 지정되거나 기본값으로 결정되지 않으면, 어느 템플릿의 특수화를 가리키는지 결정하는 데 템플릿 인자 추론이 사용돼요.
P는 잠재적 매칭으로 고려되는 함수 템플릿의 타입이고, A는 선언의 함수 타입이에요. 매칭이 없거나 (부분 순서 후에) 둘 이상이면, 함수 선언은 ill-formed예요:
template<class X> void f(X a); // 1번째 템플릿 f
template<class X> void f(X* a); // 2번째 템플릿 f
template<> void f<>(int* a) {} // f의 명시적 특수화
// P1 = void(X), A1 = void(int*): 추론 X = int*, f<int*>(int*)
// P2 = void(X*), A2 = void(int*): 추론 X = int, f<int>(int*)
// 그다음 f<int*>(int*)와 f<int>(int*)는 부분 순서에 회부되고,
// f<int>(int*)가 더 특수한 템플릿으로 선택
이 경우 추론에 추가 규칙이 적용돼요: 함수 매개변수 Pi와 Ai를 비교할 때, 어떤 Pi가 cv-비한정 템플릿 매개변수에 대한 rvalue 참조("forwarding reference")이고 대응 Ai가 lvalue 참조면, Pi는 템플릿 매개변수 타입으로 조정돼요(T&&가 T가 됨).
해제 함수 템플릿 (Deallocation function template)
주어진 placement 형태의 operator new와 매칭되는 해제 함수 템플릿 특수화를 결정할 때 템플릿 인자 추론이 사용돼요.
P는 잠재적 매칭으로 고려되는 함수 템플릿의 타입이고, A는 고려 중인 placement operator new와 매칭될 해제 함수의 함수 타입이에요. 매칭이 없거나 (오버로드 결정 후에) 둘 이상이면, placement 해제 함수는 호출되지 않아요(메모리 누수가 발생할 수 있음):
struct X
{
X() { throw std::runtime_error(""); }
static void* operator new(std::size_t sz, bool b)
{
return ::operator new(sz);
}
static void* operator new(std::size_t sz, double f)
{
return ::operator new(sz);
}
template<typename T>
static void operator delete(void* ptr, T arg)
{
::operator delete(ptr);
}
};
int main()
{
try
{
X* p1 = new(true) X;
// X()가 던질 때 operator delete가 조회됨
// P1 = void(void*, T), A1 = void(void*, bool): 추론 T = bool
// P2 = void(void*, T), A2 = void(void*, double): 추론 T = double
// 오버로드 결정이 operator delete<bool>을 선택
}
catch (const std::exception&) {}
try
{
X* p1 = new(13.2) X;
// 같은 조회, operator delete<double> 선택
}
catch (const std::exception&) {}
}
별명 템플릿 (Alias templates)
별명 템플릿은 클래스 템플릿 인자 추론을 제외하고는 추론되지 않아요(C++20부터):
template<class T> struct Alloc {};
template<class T> using Vec = vector<T, Alloc<T>>;
Vec<int> v;
template<template<class, class> class TT> void g(TT<int, Alloc<int>>);
g(v); // OK: 추론 TT = vector
template<template<class> class TT> void f(TT<int>);
f(v); // 오류: Vec은 별명 템플릿이므로 TT를 "Vec"으로 추론할 수 없음
암묵적 변환 (Implicit conversions)
타입 추론은 (위에 나열한 타입 조정 외의) 암묵적 변환을 고려하지 않아요. 그것은 나중에 일어나는 오버로드 결정의 몫이에요. 다만, 템플릿 인자 추론에 참여하는 모든 매개변수에 대해 추론이 성공하고, 추론되지 않은 모든 템플릿 인자가 명시적으로 지정되거나 기본값이면, 남은 함수 매개변수들은 대응 함수 인자와 비교돼요. 명시적으로 지정된 템플릿 인자를 치환하기 전에 비-의존 타입을 가진 각 남은 매개변수 P에 대해, 대응 인자 A를 P로 암묵적으로 변환할 수 없으면 추론이 실패해요.
템플릿 인자 추론에 템플릿 매개변수가 참여하지 않는 의존 타입의 매개변수, 그리고 명시적으로 지정된 템플릿 인자의 치환으로 비-의존이 된 매개변수는 오버로드 결정 동안 검사돼요:
template<class T>
struct Z { typedef typename T::x xx; };
template<class T> typename Z<T>::xx f(void*, T); // #1
template<class T> void f(int, T); // #2
struct A {} a;
int main()
{
f(1, a); // #1에 대해, 추론은 T = struct A를 결정하지만 남은 인자 1은
// 매개변수 void*로 암묵 변환될 수 없어 추론 실패
// 반환 타입의 인스턴스화는 요청되지 않음
// #2에 대해, 추론은 T = struct A를 결정하고 남은 인자 1은
// 매개변수 int로 암묵 변환될 수 있어 추론 성공
// 함수 호출은 #2에 대한 호출로 컴파일됨 (추론 실패는 SFINAE)
}
결함 보고 (Defect reports)
다음 동작 변경 결함 보고는 과거에 발표된 C++ 표준에 소급 적용됐어요.
| DR | 적용 표준 | 발표 당시 동작 | 수정된 동작 |
|---|---|---|---|
| CWG 70 | C++98 | 배열 경계가 추론될지 지정되지 않았음 | 비-추론으로 지정 |
| CWG 300 | C++98 | type(*)(T)/T(*)()/T(*)(T) 형태의 함수 매개변수에 대해 추론이 일어났는데, 함수 포인터는 이 형태와 매칭되지만 함수 참조는 그렇지 않았음 |
이 형태들을 type(T)/T()/T(T)로 바꿔 참조도 포함 |
| CWG 322 | C++98 | 참조 타입 매개변수가 추론에 참조된 타입을 사용하도록 조정되지 않았음 | 조정 추가 |
| CWG 976 | C++98 | 변환 연산자 템플릿 추론에서 const T& 반환 타입이 T 결과 타입과 절대 매칭될 수 없었음 |
그런 매칭을 허용하도록 규칙 조정 |
| CWG 1387 | C++11 | decltype-지정자의 표현식이 비-추론 맥락이 아니었음 | 비-추론 맥락이 됨 |
| CWG 1391 | C++98 | 추론에 참여하지 않는 인자의 암묵 변환 효과가 지정되지 않았음 | 위에 설명대로 지정 |
| CWG 1591 | C++11 | 중괄호 초기화 목록에서 배열 경계와 요소 타입을 추론할 수 없었음 | 추론 허용 |
| CWG 2052 | C++98 | 비-클래스 비-열거 인자로 연산자를 추론하는 게 하드 오류였음 | 다른 오버로드가 있으면 소프트 오류 |
| CWG 2091 | C++98 | 인자와의 타입 불일치로 참조 비-타입 매개변수를 추론하지 못했음 | 타입 불일치 회피 |
| N3922 | C++11 | auto의 직접 목록 초기화가 std::initializer_list를 추론했음 |
한 개 초과 요소면 ill-formed, 단일 요소면 요소 타입 추론 |
| CWG 2355 | C++17 | 함수 타입의 noexcept 지정자 안의 값이 추론 불가능했음 |
추론 가능하게 됨 |
더 알아보기
- 함수 템플릿
- 클래스 템플릿 인자 추론(C++17부터)
- auto 지정자