Namespaces
Variants

Template argument deduction

From cppreference.net
C++ language
General topics
Flow control
Conditional execution statements
Iteration statements (loops)
Jump statements
Functions
Function declaration
Lambda function expression
inline specifier
Dynamic exception specifications ( until C++17* )
noexcept specifier (C++11)
Exceptions
Namespaces
Types
Specifiers
constexpr (C++11)
consteval (C++20)
constinit (C++20)
Storage duration specifiers
Initialization
Expressions
Alternative representations
Literals
Boolean - Integer - Floating-point
Character - String - nullptr (C++11)
User-defined (C++11)
Utilities
Attributes (C++11)
Types
typedef declaration
Type alias declaration (C++11)
Casts
Memory allocation
Classes
Class-specific function properties
Special member functions
Templates
Miscellaneous

为了实例化 函数模板 ,必须知晓所有模板参数,但并非所有模板参数都需要显式指定。在可能的情况下,编译器会从函数参数中推导缺失的模板参数。这种情况发生在尝试进行函数调用时、获取函数模板地址时以及某些 其他场景 中:

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

模板实参推导发生在函数模板 名称查找 (可能涉及 实参依赖查找 )之后,并在 模板实参替换 (可能涉及 SFINAE )与 重载决议 之前进行。

当类模板名称被用作构造对象的类型时,同样会执行模板实参推导:

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 起)

目录

函数调用中的模板实参推导

模板实参推导试图确定模板实参(用于类型模板参数 T i 的类型、用于模板模板参数 TT i 的模板,以及用于常量模板参数 I i 的值),这些实参可被代入每个参数 P 以生成推导出的类型 A ,该类型与经过下述调整后的实参 A 类型一致。

若存在多个参数,每个 P / A 对会分别进行推导,随后将推导出的模板参数进行合并。若任一 P / A 对的推导失败或存在歧义,或不同推导对得出不同的模板参数结果,或存在既未推导成功也未显式指定的模板参数,则编译失败。

如果从 P 中移除引用和cv限定符得到 std:: initializer_list < P '> ,且 A 花括号初始化列表 ,则对初始化列表的每个元素执行推导,以 P' 作为参数,列表元素 A' 作为实参:

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 是常量模板参数,此时从初始化列表的长度推导出 N

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 进行匹配。每次匹配推导出包展开中下一个位置的模板参数:

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 进行以下调整:

1) 如果 P 不是引用类型,
a) A 是数组类型, A 被替换为通过数组到指针转换获得的指针类型;
b) 否则,若 A 为函数类型,则将 A 替换为通过函数到指针转换获得的指针类型;
c) 否则,若 A 是 cv 限定类型,则忽略顶层 cv 限定符进行推导:
template<class T>
void f(T);
int a[3];
f(a); // P = T, A = int[3], adjusted to int*: deduced T = int*
void b(int);
f(b); // P = T, A = void(int), adjusted to void(*)(int): deduced T = void(*)(int)
const int c = 13;
f(c); // P = T, A = const int, adjusted to int: deduced T = int
2) 如果 P 是 cv 限定类型,则忽略顶层 cv 限定符以进行推导。
3) 如果 P 是引用类型,则使用被引用类型进行推导。
4) P 是未被 cv 限定的模板参数的右值引用(即所谓的 转发引用 ),且对应的函数调用实参是左值,则使用 A 的左值引用类型替代 A 进行推导(注意:这是 std::forward 行为的基础。注意:在 类模板实参推导 中,类模板的模板参数永远不会是转发引用 (C++17 起) ):
template<class T>
int f(T&&);       // P 是未被 cv 限定的 T 的右值引用(转发引用)
template<class T>
int g(const T&&); // P 是 cv 限定的 T 的右值引用(非特殊情形)
int main()
{
    int i;
    int n1 = f(i); // 实参为左值:调用 f<int&>(int&)(特殊情形)
    int n2 = f(0); // 实参非左值:调用 f<int>(int&&)
//  int n3 = g(i); // 错误:推导为 g<int>(const int&&),
                   // 无法将右值引用绑定到左值
}

经过这些转换后,推导过程如下所述(参见 从类型推导 章节),并尝试找到这样的模板参数,使得推导出的 A (即经过上述调整及推导出的模板参数替换后的 P )与 转换后 A (即经过上述调整后的 A )完全一致。

如果从 P A 的常规推导失败,将额外考虑以下替代方案:

1) 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 限定
2) 转换后的 A 可以是另一个指针或成员指针类型,能通过 限定转换 或函数指针转换 (C++17 起) 转换为推导出的 A
template<typename T>
void f(const T*);
int* p;
f(p); // P = const T*, A = int*:
      // deduced T = int, deduced A = const int*
      // 应用限定转换(从 int* 到 const int*)
3) 如果 P 是一个类且 P 具有 简单模板标识 的形式,则转换后的 A 可以是推导出的 A 的派生类。同样地,如果 P 是指向 简单模板标识 形式类的指针,则转换后的 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>&, 调整为 P = B<T> (简单模板标识), A = D<int>:
          // 推导出 T = int, 推导出 A = B<int>
          // A 是推导出的 A 的派生类
}

非推导语境

在以下情况下,用于构成 P 的类型、模板和常量不参与模板实参推导,而是 使用 已在其他地方推导出或显式指定的模板实参。如果模板形参仅用于非推导语境且未显式指定,则模板实参推导将失败。

1) 使用 限定标识符 指定的类型的 嵌套名称说明符 (作用域解析运算符 :: 左侧的所有内容):
// 恒等模板,常用于从推导中排除特定参数
// (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: deduced T = std::complex<double>
              // P2 = T, A2 = double
              // P2/A2: deduced T = double
              // error: deduction fails, T is ambiguous
good(x, 1.2); // P1 = std::vector<T>, A1 = std::vector<std::complex<double>>
              // P1/A1: deduced T = std::complex<double>
              // P2 = identity<T>::type, A2 = double
              // P2/A2: uses T deduced by P1/A1 because T is to the left of :: in P2
              // OK: T = std::complex<double>
2) 包索引说明符 包索引表达式
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: deduced first member of Ts... = int
// P2/A2: deduced second member of Ts... = char
// P1 = Ts...[0], A1 = int: Ts...[0] is in non-deduced context
(since C++26)
3) decltype 说明符的表达式:
template<typename T>
void f(decltype(*std::declval<T>()) arg);
int n;
f<int*>(n); // P = decltype(*declval<T>()), A = int: T is in non-deduced context
(since C++11)
4) 引用模板参数的子表达式出现在常量模板实参或数组边界中的情况:
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
5) 在函数参数的参数类型中使用的模板形参,且该函数参数具有在正在进行实参推导的调用中被使用的默认实参:
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 deduced T = std::string
      // P2 = const F&, A2 = std::less<std::string> rvalue
      // P2 is non-deduced context for F (template parameter) used in the
      // parameter type (const F&) of the function parameter comp,
      // that has a default argument that is being used in the call f(v)
6) 参数 P ,其 A 是一个函数或一组重载函数,使得多个函数与 P 匹配,或没有函数与 P 匹配,或该重载集合包含一个或多个函数模板:
template<typename T>
void out(const T& value) { std::cout << value; }
out("123");     // P = const T&, A = const char[4] lvalue: deduced T = char[4]
out(std::endl); // P = const T&, A = function template: T is in non-deduced context
7) 当参数 P A 是花括号初始化列表,但 P 不是 std::initializer_list 、对其(可能带 cv 限定)的引用或对数组的引用时}}:
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
8) 参数包 P 未出现在参数列表末尾的情况:
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 处于非推导语境
9) 出现在参数 P 内部的模板参数列表,且该列表包含不在最末位的包展开:
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 起)
10) 对于数组类型(但不包括数组引用或数组指针)的 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);     // 正确:推导出 i = 20
    f1<20>(a); // 正确
    f2(a);     // 错误:i 处于不可推导上下文
    f2<10>(a); // 正确
    f3(a);     // 正确:推导出 i = 10
    f3<10>(a); // 正确
}

在任何情况下,如果类型名称的任何部分属于非推导上下文,则整个类型名称都是非推导的。然而,复合类型可以同时包含推导和非推导的类型名称。例如,在 A < T > :: B < T2 > 中, T 由于规则 #1(嵌套名称说明符)是非推导的,而 T2 由于属于同一类型名称也是非推导的;但在 void ( * f ) ( typename A < T > :: B , A < T > ) 中, A < T > :: B 中的 T 是非推导的(由于相同规则),而 A < T > 中的 T 是推导的。

从类型推导

当函数参数 P 依赖于一个或多个类型模板参数 T i、模板模板参数 TT i 或常量模板参数 I i,且存在对应的实参 A 时,若 P 满足以下形式之一,则进行推导:

  • cv (可选) T
  • T*
  • 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> 的形式,则该模板实参列表中的每个元素 P i 会与其对应的模板实参 A i 进行匹配。如果最后一个 P i 是包展开,则其模式会与 A 的模板实参列表中的每个剩余实参进行比较。未被推导的尾部形参包会被推导为空形参包。

如果 P 包含函数参数列表 (T) 的某种形式,则将该列表中的每个参数 P i 与 A 的函数参数列表中对应的实参 A i 进行比较。如果最后一个 P i 是包展开,则其声明符将与 A 参数类型列表中的每个剩余 A i 进行比较。

表单可以嵌套并递归处理:

  • 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 )

类型模板参数无法从常量模板参数的类型推导:

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 类型中的模板参数从该值的类型推导。

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 从类型 bar<3> 中的模板参数值推导为 int(n 推导为 3)

类型 T[N] N 的类型是 std::size_t

template<class T, T i>
void f(int (&a)[i]);
int v[10];
f(v); // OK: T 是 std::size_t

函数类型的 noexcept ( B ) 说明符中 B 的类型是 bool

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); // 正确:模板参数未经过推导
             // 此处调用 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&
           // 正确(遵循 CWG 2091):推导出 R 引用 n
}

类型模板参数无法从函数默认参数的类型推导:

template<typename T>
void f(T = 5, T = 7);
void g()
{
    f(1);     // 正确:调用 f<int>(1, 7)
    f();      // 错误:无法推导 T
    f<int>(); // 正确:调用 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>)

其他上下文

除了函数调用和运算符表达式外,模板实参推导还在以下情况中使用:

auto 类型推导

模板实参推导用于变量 声明 中,当从变量的初始化器推导 auto 说明符 的含义时。

参数 P 按如下方式获得:在包含 auto 的变量声明类型 T 中,每个 auto 的出现被替换为一个虚构的类型模板参数 U ,或者如果初始化是复制列表初始化,则替换为 std::initializer_list<U> 。实参 A 是初始化器表达式。根据上述规则从 P A 推导出 U 后,将推导出的 U 代入 P 以获得实际的变量类型:

const auto& x = 1 + 2; // P = const U&, A = 1 + 2:
                       // 规则与调用 f(1 + 2) 相同,其中 f 是
                       // template<class U> void f(const U& u)
                       // 推导出的 U = int,x 的类型是 const int&
auto l = {13}; // P = std::initializer_list<U>, A = {13}:
               // 推导出的 U = int,l 的类型是 std::initializer_list<int>

在直接列表初始化(但不在复制列表初始化中),当从花括号初始化列表推导 auto 的含义时,花括号初始化列表必须仅包含一个元素,且 auto 的类型将是该元素的类型:

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 说明符的含义时,会使用模板实参推导。

对于auto返回类型函数,参数 P 按如下方式获取:在包含 auto 的函数声明返回类型 T 中,每个 auto 的出现都被替换为虚构的类型模板参数 U 。实参 A return 语句的表达式,如果return语句没有操作数,则 A void ( ) 。根据上述规则从 P A 推导出 U 后,将推导出的 U 代入 T 以获得实际返回类型:

auto f() { return 42; } // P = auto, A = 42:
                        // deduced U = int, the return type of f is int

如果此类函数有多个返回语句,则对每个返回语句执行推导。所有结果类型必须相同,并成为实际返回类型。

如果此类函数没有返回语句,则在推导时 A void ( )

注意:变量和函数声明中 decltype ( auto ) 占位符的含义不使用模板实参推导。

(C++14 起)

重载决议

模板实参推导在 重载决议 期间使用,当从候选模板函数生成特化时。 P A 与常规函数调用中的定义相同:

std::string s;
std::getline(std::cin, s);
// "std::getline" 命名了4个函数模板,
// 其中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>>
// 第二个候选模板:
// 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>>
// 重载决议根据左值 std::cin 的引用绑定进行排序
// 并选择两个候选特化中的第一个

如果推导失败,或者推导成功但产生的特化无效(例如,重载运算符的参数既非类类型也非枚举类型),则该特化不会包含在重载集中,这与 SFINAE 机制类似。

重载集合的地址

模板实参推导用于获取 重载集地址 时,该重载集包含函数模板。

函数模板的函数类型为 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<< 的其他重载版本不可行)
// 推导过程确定类型模板参数 CharT 和 Traits

在此情况下对推导应用了一条额外规则:当比较函数参数 P i 和 A i 时,如果任意 P i 是 cv 未限定模板参数的右值引用(即“转发引用”),且对应的 A i 是左值引用,则 P i 会被调整为模板参数类型(T&& 变为 T)。

若函数模板的返回类型是占位符( auto decltype ( auto ) ),则该返回类型是非推导语境,并由实例化决定。

(since C++14)

偏序规则

模板实参推导用于 重载函数模板的偏序处理 过程。

转换函数模板

模板实参推导用于选择 用户定义转换函数 的模板实参。

A 是转换结果所需的类型。 P 是转换函数模板的返回类型。如果 P 是引用类型,则在本节后续内容中使用被引用类型替代 P

如果 A 不是引用类型:

a) P 是数组类型,则通过数组到指针转换获得的指针类型将替代 P
b) P 为函数类型,则通过函数到指针转换获得的函数指针类型将替代 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 允许为指向 noexcept 函数的指针,可通过函数指针转换转换为 A
d) A 为指向成员函数的指针,推导出的 A 允许为指向 noexcept 成员函数的指针,可通过函数指针转换转换为 A
(since C++17)

参见 成员模板 以了解有关转换函数模板的其他规则。

显式实例化

模板实参推导用于 显式实例化 显式特化 以及那些声明符-id恰好指向函数模板特化的 友元声明 (例如 friend ostream & operator << <> ( ... ) )。当未显式指定或默认所有模板实参时,将使用模板实参推导来确定所引用的模板特化。

P 是被视为潜在匹配的函数模板类型,而 A 是来自声明的函数类型。如果没有匹配项或存在多个匹配项(经过偏序处理后),该函数声明属于非良构:

template<class X>
void f(X a);        // 第一个模板 f
template<class X>
void f(X* a);       // 第二个模板 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*) 作为更特化的模板

在此情况下对推导应用了一条额外规则:当比较函数参数 P i 和 A i 时,如果任意 P i 是 cv 未限定模板参数的右值引用(即“转发引用”),且对应的 A i 是左值引用,则 P i 会被调整为模板参数类型(T&& 变为 T)。

Deallocation function template

模板实参推导用于确定 解分配函数 模板特化是否匹配给定布置形式的 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&) {}
}

别名模板

别名模板 不会被推导 ,除了在 类模板实参推导 (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); // 正确:推导出 TT = vector
template<template<class> class TT>
void f(TT<int>);
f(v); // 错误:无法将 TT 推导为 "Vec",因为 Vec 是别名模板

隐式转换

类型推导不考虑隐式转换(除了上述列出的类型调整之外):这是稍后进行的 重载决议 的任务。然而,如果所有参与模板实参推导的参数都推导成功,且所有未推导的模板实参都被显式指定或设为默认值,那么剩余的函数参数会与对应的函数实参进行比较。对于每个在替换任何显式指定的模板实参之前类型为非依赖的剩余参数 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)
}

缺陷报告

以下行为变更缺陷报告被追溯应用于先前发布的C++标准。

缺陷报告 适用标准 发布时行为 正确行为
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 多元素时视为病式,
单元素时推导元素类型
CWG 2355 C++17 函数类型 noexcept 说明符中的值不可推导 改为可推导