CppDS.com

C++ 98 11 14 17 20 手册

模板实参推导

来自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)
}

此机制使得使用模板运算符可行,因为除了将其重写为函数调用表达式之外,不存在为运算符指定模板实参的语法:

#include <iostream>
 
int main() 
{
    std::cout << "Hello, world" << std::endl;
    // operator<< 通过 ADL 查找为 std::operator<< ,
    // 然后推导出 operator<<<char, std::char_traits<char>> 两次
    // std::endl 被推导为 &std::end5.l<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++14 起)
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 匹配或者没有函数与 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 限定),或者到数组的引用:
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
(C++11 起)
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> 处于非推导语境
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>::B 中的 T 为非推导的(因为相同规则),不过 A<T> 中的 T 可推导。

从类型推导

给定依赖一或多个类型模板形参 Ti、模板模板形参 TTi 或非类型模板形参 Ii 的模板形参 P 及其对应实参 A,若 P 拥有下列形式之一则进行推导:

  • T;
  • cv-list T;
  • T*;
  • T&;
  • T&&;
  • T[整数常量];
  • 类模板名<T>;
  • 类型(T);
  • T();
  • T(T);
  • T 类型::*;
  • 类型 T::*;
  • T T::*;
  • T(类型::*)();
  • 类型(T::*)();
  • 类型(类型::*)(T);
  • 类型(T::*)(T);
  • T (类型::*)(T);
  • T (T::*)();
  • T (T::*)(T);
  • 类型[i];
  • 类模板名<I>;
  • TT<T>;
  • TT<I>;
  • TT<>;

其中

  • (T) 是其中至少有一个形参类型含有 T 的函数形参列表;
  • () 是其中没有含有 T 的形参的函数形参列表;
  • <T> 是至少一个实参含有 T 的模板实参列表;
  • <I> 是其中至少一个实参含有 I 的模板实参列表;
  • <> 其中没有含有 T 或 I 的实参的模板实参列表。

P 具有包含模板形参列表 <T><I> 的形式之一,则将该模板形参列表的每个元素 Pi 与其 A 的对应模板实参 Ai 进行匹配。若最后一个 Pi 是包展开,则将其模式与 A 的模板实参列表中的每个剩余实参进行比较。其他情况下不推导的尾随参数包,被推导为空形参包。

P 具有包含函数形参列表 (T) 的形式,则将来自该列表的每个形参 Pi 与来自 A 的函数形参列表的对应实参 A 进行比较。若最后一个 Pi 是包展开,则将其声明符和 A 的形参类型列表中的每个剩余的 Ai 进行比较。

这些形式可以嵌套并递归处理:X<int>(*)(char[6]) 是一个 类型(*)(T) 的例子,其中 类型类模板名<T> 而 T 为 类型[i]

不能从非类型模板实参推导模板类型实参:

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

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

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

若在形参列表中使用某个非类型模板形参,且推导了其对应的模板实参,则推导的模板实参类型(如同在其外围模板形参列表中指定,这表示引用被保留)必须与该非类型模板形参的类型严格匹配,但 cv 限定符被丢弃,且不包括从数组边界推导的模板实参——此情况下允许任何整型类型,即使是 bool 亦可(虽然它总是变为 true 值):

template<int i> class A { };
template<short s> void f(A<s>); // 非类型模板形参的类型是 short
 
void k1()
{
    A<1> 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 是模板,拥有形参 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:在变量的被声明类型 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>)

返回 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 绑定的引用排在高位
// 并选取两个候选特化的第一个

若推导失败,或若推导成功,但它产生的特化无效(例如形参既非类类型亦非枚举类型的重载运算符), (C++14 起)则重载集不包含该特化,这类似于 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 是转换函数模板的返回类型,除了

a) 若返回类型是引用类型,则 P 为被引用类型;
b) 若返回类型是数组类型且 A 非引用类型,则 P 为从数组到指针转换获得的指针类型;
c) 若返回类型为函数类型且 A 非引用类型,则 P 为从函数到指针转换获得的函数指针类型;
d)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++14 起)

缺陷报告

下列更改行为的缺陷报告追溯地应用于以前出版的 C++ 标准。

DR 应用于 出版时的行为 正确行为
CWG 1391 C++14 未指名推导所不涉及的实参隐式转换的效果 指定为如上文所述
CWG 1591 C++11 不能从 花括号初始化器列表 推导数组边界和元素类型 允许推导
CWG 2052 C++14 以非类实参推导运算符是硬错误(某些编译器中) 若有其他重载则为软错误
CWG 2091 C++98 推导引用非类型形参不可用,因为类型不能匹配实参 避免类型不匹配
N3922 C++11 auto 的直接列表初始化推导出 std::initializer_list 对 >1 个元素为非良构,对单个元素推出元素类型
关闭