模板实参推导

来自cppreference.com
< cpp‎ | language

为了实例化一个函数模板,需要知晓但不需要指定每个模板实参。编译器会尽可能从函数实参推导缺失的模板实参。这在尝试调用函数、取函数模板地址时,和某些其他语境中发生:

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

从函数调用推导

模板实参推导试图确定模板实参(类型模板形参 Ti 的类型,模板模板形参 TTi 的模板,和非类型模板形参 Ii 的值),它们在经过以下列出的调整之后可以替换到每个函数形参 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 是非类型模板形参,那么它会从初始化器列表的长度被推导:

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 出现,那么对调用的每个剩余实参类型 A 与类型 P 匹配。每个匹配为包展开中的下个位置推导模板实参:

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
      // 只有一个重载有效,推导成功

推导开始前,对 PA 进行下列调整:

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],调整为 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
2) 如果 P 是有 cv 限定的类型,那么推导时会忽略顶层 cv 限定符。
3) 如果 P 是引用类型,那么用 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)的模板实参。

如果来自 PA 的通常推导失败,那么额外考虑下列替用者:

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*:
      // 推导 T = int 以及 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:推导出 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:使用 P1/A1 推导出的 T,因为 T 在 P2 中的 :: 的左侧
              // OK:T = std::complex<double>
2) decltype 说明符的表达式:
template<typename T>
void f(decltype(*std::declval<T>()) arg);
 
int n;
f<int*>(n); // P = decltype(*declval<T>()),A = int:T 处于不推导语境
(C++11 起)
3) 非类型模板实参或数组边界,其中子表达式引用一个模板形参:
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
4) 用于函数形参的形参类型中拥有默认实参的模板形参,且该函数形参在正在进行实参推导的调用中:
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> 左值
      // P1/A1 推导出 T = std::string
      // P2 = const F&,A2 = std::less<std::string> 右值
      // P2 在用于函数形参 comp 的形参类型(const F&)的 F(模板形参)的不推导语境,
      // 该函数形参拥有调用 f(v) 中正在使用的默认实参
5) 形参 P,其实参 A 是一个函数,没有函数或有多个函数与 P 匹配的重载集,或包含一个或多个函数模板的重载集:
template<typename T>
void out(const T& value) { std::cout << value; }
 
out("123");     // P = const T&,A = const char[4] 左值:推导出 T = char[4]
out(std::endl); // P = const T&,A = 函数模板:T 处于不推导语境
6) 形参 P,其实参 A花括号初始化器列表,但 Pstd::initializer_list、到它的引用(可以有 cv 限定),或者 (C++17 起)到数组的引用:
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
7) 作为形参包且没有在形参列表尾部出现的形参 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 在不推导语境
8) 在形参 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 起)
9) 数组(不考虑到数组的引用或数组的指针)类型 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> 中的 T 会被推导时 A<T>::B 中的 T 也(因为相同规则)不会被推导。

从类型推导

给定依赖一或多个类型模板形参 Ti、模板模板形参 TTi 或非类型模板形参 Ii 的模板形参 P 及其对应实参 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(可选) 表示递归地满足这些规则的、在 PA 的非推导语境中的,或是与 PA 中的同一非待决类型的类型或 形参类型列表
  • TT(可选)TU(可选) 表示类模板或模板模板形参。
  • I(可选) 表示作为 I、在 PA 中为值待决,或在 PA 中有相同常量值的表达式。
  • 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* 的样例,其中 TX<int>(char[6])
  • X<int>(char[6])T(可选) (U(可选)) 的样例,其中 TX<int>Uchar[6]
(C++17 前)
  • X<int>(char[6])T(可选) (U(可选)) noexcept(I(可选)) 的样例,其中 TX<int>Uchar[6] ,而隐式 noexcept 说明符中的 Ifalse
(C++17 起)
  • X<int>TT(可选)<T> 的样例,其中 TTXTint,而
  • char[6]T(可选) [I(可选)] 的样例,其中 TcharIstd::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
      // 但不能从 i 的类型推导 T
(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:从类型 A<2> 中的模板实参值推导出 T = long
 
template<auto X>
class bar{};
 
template<class T, T n>
void f(bar<n> x);
 
f(bar<3>{}); // OK:从类型 bar<3> 中的模板实参值推导出 T = 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); // 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>
           // struct X 的声明中形参类型是 int&
           // 实参类型是 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 是一个拥有模板模板形参的模板
 
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:在变量的被声明类型 T(包含 auto)中,auto 的每次出现都被替换成一个虚构的类型模板形参 U,或在它的初始化是复制列表初始化时替换成 std::initializer_list<U>。实参 A 是初始化器表达式。按上文所述的规则从 PA 推导出 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 的函数

当从 return 语句推导函数返回类型中的 auto 说明符的含义时,将模板实参推导用于函数的声明。

对于返回 auto 的函数,通过以下方式获得形参 P:在被声明函数的返回类型 T(包含 auto)中,auto 的每次出现都会被替换成一个虚构的类型模板实参 U。实参 Areturn 语句的表达式,而如果 return 语句没有操作数,那么 Avoid()。按上文所述的规则从 PA 推导出 U 后,将推导出的 U 替换到 T 中以获取实际的返回类型:

auto f() { return 42; } // P = auto,A = 42:
                        // 推导出 U = int,f 的返回类型是 int

如果这种函数拥有多个 return 语句,那么就会对每个 return 语句进行推导。所有结果类型必须相同,并成为该函数的实际返回类型。

如果这种函数没有 return 语句,那么推导时 Avoid()

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

(C++14 起)

重载决议

从候选模板函数生成特化时,在重载决议期间使用模板实参推导: 其 PA 和常规函数调用相同。

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>>
 
// 重载决议将从左值 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

这种情况下的推导适用一条额外的规则:当比较函数形参 Pi 和 Ai 时,如果有任何 Pi 是到无 cv 限定模板形参的右值引用(“转发引用”)且对应的 Ai 是左值引用,那么会将 Pi 调整为模板形参类型(T&& 成为 T)。

如果函数模板的返回类型是占位符(autodecltype(auto)),那么返回类型处于不推导语境,并由实例化决定。

(C++14 起)

偏序

重载的函数模板的偏序过程中使用模板实参推导。

转换函数模板

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

A 是要求作为转换结果的类型。P 是转换函数模板的返回类型。如果 P 是引用类型,那么在本节后续部分出现的 P 表示的是被引用的类型。

如果 A 不是引用类型,那么:

a) 如果返回类型是数组类型,那么 P 表示的是从数组到指针转换获得的指针类型;
b) 如果返回类型是函数类型,那么 P 表示的是从函数到指针转换获得的函数指针类型;
c) 如果 P 有 cv 限定,那么忽略顶层 cv 限定符。

如果 A 有 cv 限定,那么忽略顶层 cv 限定符。如果 A 是引用类型,那么推导使用被引用的类型。

如果从 PA 进行的常规推导(如上文所述)失败,那么考虑下列替代方式:

a) 如果 A 是引用类型,那么 A 可以比推导出的 A 有更多的 cv 限定;
b) 如果 A 是指针或成员指针类型,那么推导出的 A 可以是任意能以限定转换转换到 A 的指针:
struct A
{
    template<class T>
    operator T***();
};
A a;
 
const int* const* const* p1 = a; // 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
(C++17 起)

关于转换函数模板的其他规则,见成员模板

显式实例化

显式实例化显式特化 以及声明符标识恰好指代某个函数模板特化的友元声明(例如 friend ostream& operator<< <> (...))在还有模板实参没有被显式指定且没有默认值时会用模板实参推导来确定指代哪个模板特化。

P 是被认为是潜在匹配的函数模板的类型,而 A 是声明中的函数类型。如果(偏序后)没有匹配或多于一个匹配,那么函数声明为谬构:

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 限定模板形参的右值引用(“转发引用”)且对应的 Ai 是左值引用,那么将 Pi 调整为模板形参类型(T&& 成为 T)。

解分配函数模板

确定解分配函数模板特化是否与给定的 operator new 布置形式相匹配时使用模板实参推导。

P 是被认为是潜在匹配的函数模板的类型,而 A 是应当与考虑中的布置 operator new 相匹配的解分配函数的函数类型。如果(在重载决议后)没有匹配或多于一个匹配,那么不调用布置解分配函数(可能发生内存泄漏):

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&) {}
}

别名模版

别名模版始终不会进行推导:

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); // 错误: 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 形式为 类型(*)(T)/T(*)()/T(*)(T) 的函数形参会
参与推导,函数指针可以匹配这些形参,但函数引用不能
将这些形式改为 类型(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 说明符中的值曾为不可推导 使之可推导