类模板实参推导(C++17 起)

来自cppreference.com
< cpp‎ | language

为了实例化类模板,必须知晓每个模板实参,但不必每个模板实参都被指定。在下列语境中,编译器会从初始化器的类型推导缺失的模板实参:

  • 任何指定变量及变量模板初始化的声明
std::pair p(2, 4.5);     // 推导出 std::pair<int, double> p(2, 4.5);
std::tuple t(4, 3, 2.5); // 同 auto t = std::make_tuple(4, 3, 2.5);
std::less l;             // 同 std::less<void> l;
template<class T> struct A { A(T,T); };
auto y = new A{1,2}; // 分配的类型是 A<int>
auto lck = std::lock_guard(mtx); // 推导出 std::lock_guard<std::mutex>
std::copy_n(vi1, 3, std::back_insert_iterator(vi2)); // 或 std::back_inserter(vi2)
std::for_each(vi.begin(), vi.end(), Foo([&](int i) {...})); // 推导 Foo<T>,其中 T 
                                                            // 是独有的 lambda 类型
template<class T>
struct X {
    X(T) {}
    auto operator<=>(const X&) const = default;
};
 
template<X x> struct Y { };
 
Y<0> y; // OK , Y<X<int>(0)>
(C++20 起)

隐式生成的推导指引

当函数式转型或变量声明使用主类模板 C 的名字为类型说明符,而不带实参列表时,以如下方式进行推导:

  • 若已定义 C,则对所指名的主模板(若其已定义)中所声明的每个构造函数(或构造函数模板)Ci,构造一个虚设的函数模板 Fi,使得
  • Fi 的模板形参是 C 的模板形参后随(若 Ci 是构造函数模板)Ci 的模板形参(亦包含默认模板实参)
  • Fi 的函数形参是构造函数形参
  • Fi 的返回类型是 C 后随环绕于 <> 中的类模板的模板形参
  • 若未定义 C 或未其声明任何构造函数,则添加一个导出自假想的构造函数 C() 的额外的虚设函数模板
  • 任何情况下,都添加一个导出自假想构造函数 C(C) 的额外的虚设函数模板,称之为复制推导候选。

然后,针对某个假想类类型的虚设对象的初始化,进行模板实参推导重载决议,对于组成重载集而言,该类的各构造函数的签名与各个指引(除了返回类型)相匹配,并且由进行类模板实参推导的语境提供其初始化器。但若其初始化器列表由单个(可为 cv 限定的)U 类型的表达式组成,其中 UC 的特化或派生自 C 的特化的类,则省去列表初始化的第一阶段(考虑初始化器列表构造函数)。

这些虚设构造函数是该假想类类型的公开成员。若推导指引从显式构造函数组成,则它们为 explicit。若重载决议失败,则程序非良构。否则,选中的 F 的返回类型就成为推导出的类模板特化。

template<class T> struct UniquePtr { UniquePtr(T* t); };
UniquePtr dp{new auto(2.0)};
// 一个声明的构造函数:
// C1:UniquePtr(T*);
// 隐式生成的推导指引集:
// F1:template<class T> UniquePtr<T> F(T *p);
// F2:template<class T> UniquePtr<T> F(UniquePtr<T>); // 复制推导候选
// 要初始化的假想类:
// struct X {
//     template<class T> X(T *p);          // 从 F1
//     template<class T> X(UniquePtr<T>);  // 从 F2
// };
// X 对象的直接初始化,以“new double(2.0)”为初始化器
// 选择对应于 T = double 的指引 F1 的构造函数
// 对于 T=double 的 F1,返回类型是 UniquePtr<double>
// 结果:
// UniquePtr<double> dp{new auto(2.0)}

或者,对于更加复杂的例子(注意:“S::N”无法编译:作用域解析限定符并非可推导内容):

template<class T> struct S {
  template<class U> struct N {
    N(T);
    N(T, U);
    template<class V> N(V, U);
  };
};
 
S<int>::N x{2.0, 1};
// 隐式生成的推导指引是(注意已知 T 是 int)
// F1:template<class U> S<int>::N<U> F(int);
// F2:template<class U> S<int>::N<U> F(int, U);
// F3:template<class U, class V> S<int>::N<U> F(V, U);
// F4:template<class U> S<int>::N<U> F(S<int>::N<U>); (复制推导候选)
// 以“{2.0, 1}”为初始化器的直接列表初始化的重载决议
// 选择 U=int 与 V=double 的 F3。
// 返回类型为 S<int>::N<int>
// 结果:
// S<int>::N<int> x{2.0, 1};

用户定义推导指引

用户定义推导指引的语法是带尾随返回类型的函数声明的语法,但它以类名为函数名:

explicit(可选) 模板名 ( 形参声明子句 ) -> 简单模板标识 ;

用户定义推导指引必须指名一个类模板,且必须在类模板的同一语义作用域(可以是命名空间或外围类)中引入,而且对于成员类模板,必须拥有同样的访问,但推导指引不成为该作用域的成员。

推导指引不是函数且没有函数体。推导指引不会被名字查找所找到,并且除了在推导类模板实参时与其他推导指引之间的重载决议之外,不参与重载决议。不能在同一翻译单元中为同一类模板再次声明推导指引。

// 模板的声明
template<class T> struct container {
    container(T t) {}
    template<class Iter> container(Iter beg, Iter end);
};
// 额外推导指引
template<class Iter>
container(Iter b, Iter e) -> container<typename std::iterator_traits<Iter>::value_type>;
// 使用
container c(7); // OK:用隐式生成的指引推出 T=int
std::vector<double> v = { /* ... */};
auto d = container(v.begin(), v.end()); // OK:推出 T=double
container e{5, 6}; // 错误:无 std::iterator_traits<int>::value_type

为重载决议而虚设的构造函数(如上文所述),若其对应于从显式构造函数组成的隐式生成的推导指引,或对应于使用关键词 explicit 的用户定义推导指引,则为 explicit。像往常一样,在复制初始化语境中忽略这些构造函数:

template<class T> struct A {
    explicit A(const T&, ...) noexcept; // #1
    A(T&&, ...);                        // #2
};
 
int i;
A a1 = { i, i }; // 错误:不能从 #2 的右值引用推导,且 #1 为 explicit,复制初始化中不予考虑。
A a2{i, i};      // OK,#1 推出 A<int> 并且初始化
A a3{0, i};      // OK,#2 推出 A<int> 并且初始化
A a4 = {0, i};   // OK,#2 推出 A<int> 并且初始化
 
template<class T> A(const T&, const T&) -> A<T&>; // #3
template<class T> explicit A(T&&, T&&)  -> A<T>;  // #4
 
A a5 = {0, 1};   // 错误:#3 推出 A<int&> 且 #1 & #2 生成形参相同的构造函数。
A a6{0,1};       // OK,#4 推出 A<int> 并以 #2 初始化
A a7 = {0, i};   // 错误:#3 推出 A<int&>
A a8{0,i};       // 错误:#3 推出 A<int&>

在构造函数或构造函数模板的形参列表中使用成员 typedef 或别名模板,此行为自身不会使隐式生成的指引的对应形参变为非推导语境。

template<class T> struct B {
    template<class U> using TA = T;
    template<class U> B(U, TA<U>);  //#1
};
 
// 从 #1 产生的隐式推导指引等价于
// template<class T, class U> B(U, T) -> B<T>;
// 而非
// template<class T, class U> B(U, typename B<T>::template TA<U>) -> B<T>;
// 这是无法推导的
 
B b{(int*)0, (char*)0}; // OK,推出 B<char*>

注解

仅当不存在模板实参列表时才进行类模板实参推导。若指定了模板实参列表,则不发生推导。

std::tuple t1(1, 2, 3);                // OK:推导
std::tuple<int, int, int> t2(1, 2, 3); // OK:提供所有实参
std::tuple<> t3(1, 2, 3);              // 错误:tuple<> 中无匹配的构造函数。
                                       //      不进行推导
std::tuple<int> t4(1, 2, 3);           // 错误

聚合体的类模板实参推导常常需要推导指引:

template<class A, class B> struct Agg {A a; B b; };
// 隐式生成的指引由默认、复制及移动构造函数组成
template<class A, class B> Agg(A a, B b) -> Agg<A, B>;
Agg agg{1, 2.0}; // 从用户定义指引推出 Agg<int, double>
 
template <class... T>
array(T&&... t) -> array<std::common_type_t<T...>, sizeof...(T)>;
auto a = array{1, 2, 5u}; // 从用户定义指引推出 array<unsigned, 3>

用户定义指引不必是模板:

template<class T> struct S { S(T); };
S(char const*) -> S<std::string>;
S s{"hello"}; // 推出 S<std::string>

在类模板的作用域中,无形参列表的模板名是注入类名,并可用作类型。这种情况下,不发生类模板推导,而必须显式提供其模板形参:

template<class T>
struct X {
  X(T) { }
  template<class Iter> X(Iter b, Iter e) { }
 
  template<class Iter>
  auto foo(Iter b, Iter e) { 
    return X(b, e); // 无推导:X 是当前的 X<T>
  }
  template<class Iter>
  auto bar(Iter b, Iter e) { 
    return X<typename Iter::value_type>(b, e); // 必须指定所需的实参
  }
  auto baz() {
    return ::X(0); // 非注入类名;推导为 X<int>
  }
};

重载决议中,部分排序对于函数模板是否从指引生成方面有优先性:若从构造函数生成的函数模板比从推导指引生成的更特殊,则选择从构造函数生成的。因为复制推导候选常常比包装构造函数更特殊,故此规则表明复制通常更优先于包装。

template<class T> struct A {
    A(T, int*);     // #1
    A(A<T>&, int*); // #2
    enum { value };
};
template<class T, int N = T::value> A(T&&, int*) -> A<T>; //#3
 
A a{1,0}; // 使用 #1 推出 A<int> 并以 #1 初始化
A b{a,0}; // 使用 #2(比 #3 更特殊)推出 A<int> 并以 #2 初始化

当之前的决胜规则(包括部分排序)无法分辨两个候选函数模板时,应用下列规则:

  • 由引导生成的函数模板比从构造函数或构造函数模板隐式生成的函数模板更受偏好。
  • 复制推导候选比所有其他从构造函数或构造函数模板隐式生成的函数模板更受偏好。
  • 从非模板构造函数函数模板的函数模板比从构造函数模板的隐式生成的函数模板更受偏好。
template <class T> struct A {
    using value_type = T;
    A(value_type); // #1
    A(const A&); // #2
    A(T, T, int); // #3
    template<class U> 
    A(int, T, U); // #4
}; // A(A); #5,复制推导候选
 
A x (1, 2, 3); // 使用 #3,从非模板构造函数生成
 
template <class T> A(T) -> A<T>;  // #6,比 #5 更不特殊
 
A a (42); // 使用 #6 推出 A<int> 并以 #1 初始化
A b = a;  // 使用 #5 推出 A<int> 并以 #2 初始化
 
template <class T> A(A<T>) -> A<A<T>>;  // #7,与 #5 一样特殊
 
A b2 = a;  // 使用 #7 推出 A<A<int>> 并以 #1 初始化

若模板形参是类模板形参,则到该形参的无 cv 限定的右值引用不是转发引用

template<class T> struct A {
    template<class U>
    A(T&&, U&&, int*);   // #1:T&& 不是转发引用
                         //     U&& 是转发引用
    A(T&&, int*); // #2:T&& 不是转发引用
};
 
template<class T> A(T&&, int*) -> A<T>; // #3:T&& 是转发引用
 
int i, *ip;
A a{i, 0, ip};  // 错误,不能从 #1 推导
A a0{0, 0, ip}; // 使用 #1 推出 A<int> 并以 #1 初始化
A a2{i, ip};    // 使用 #3 推出 A<int&> 并以 #2 初始化

当从类模板某个特化类型的单个实参进行的初始化有问题时,通常与默认的包装相比,更偏好复制推导:

std::tuple t1{1};   //std::tuple<int>
std::tuple t2{t1};  //std::tuple<int>,非 std::tuple<std::tuple<int>>
 
std::vector v1{1, 2};   // std::vector<int>
std::vector v2{v1};     // std::vector<int>,非 std::vector<std::vector<int>> (P0702R1)
std::vector v3{v1, v2}; // std::vector<std::vector<int>>

除了复制 VS. 包装的特殊情形外,列表初始化中保持对初始化器列表构造函数的强偏好。

std::vector v1{1, 2}; // std::vector<int>
 
std::vector v2(v1.begin(), v1.end());  // std::vector<int>
std::vector v3{v1.begin(), v1.end()};  // std::vector<std::vector<int>::iterator>

在引入类模板实参推导前,避免显式指定实参的常用手段是使用函数模板:

std::tuple p1{1, 1.0};             // std::tuple<int, double>,使用推导
auto p2 = std::make_tuple(1, 1.0); // std::tuple<int, double>,C++17 前

缺陷报告

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

DR 应用于 出版时的行为 正确行为
P0702R1 C++17 初始化器列表构造函数能架空复制推导候选,导致产生包装 复制时跳过初始化器列表阶段