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

< 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) 的额外的虚设函数模板,称之为复制推导候选。
  • 另外,若
  • C 有定义且满足聚合类型的要求(假设其任何依赖基类无虚函数或虚基类),
  • C 的用户定义推导指引,且
  • 该变量由非空的初始化器列表 arg1, arg2, ..., argn 初始化(可使用指派初始化器),
则可能添加聚合推导候选。按以下方式从聚合体元素类型产生聚合推导候选的形参列表:
  • ei 为要从 argi 初始化的(可能递归的)聚合体元素(公开基类/类成员/数组元素),其中
  • 仅对有非依赖类型或有非依赖边界的数组类型的成员考虑花括号消除,
  • C (或它的自身为聚合体的元素)拥有作为包展开的基类:
  • 若包展开是尾随的聚合体元素,则认为它匹配初始化器列表的所有剩余元素;
  • 否则,认为包为空。
  • 若无这种 ei ,则不添加聚合推导候选。
  • 否则,按以下方式确定聚合推导的形参列表 T1, T2, ..., Tn
  • ei 是数组且 argi 为花括号初始化器列表或字符串字面量,则 Ti 是到 ei 的类型的右值引用。
  • 否则, Tiei 的类型。
  • 若因为它是非尾随聚合体元素而跳过包,则在其原本的聚合体元素位置插入形为 Pj ... 的形参包。(这通常会导致推导失败。)
  • 若包为尾随的聚合体元素,则将对应它的尾随形参序列替换成形为 Tn ... 的单个形参。
聚合推导候选是从上述假想构造函数 C(T1, T2, ..., Tn) 派生的虚设函数模板。
在聚合推导候选的模板实参推导中,只有在尾随形参包的元素数无法从其他情况下推导时,才从尾随函数实参数推导。
template<class T> struct A { T t; struct { long a, b; } u; };
A a{1, 2, 3}; // 聚合推导候选: template<class T> A<T> F(T, long, long);
 
template<class... Args>
struct B : std::tuple<Args...>, Args... {};
B b{ std::tuple<std::any, std::string>{}, std::any{} };
// 聚合推导候选:
//   template<class... Args> B<Args...> F(std::tuple<Args...>, Args...);
// 推出 b 的类型为 B<std::any, std::string>
(C++20 起)

然后,针对某个假想类类型的虚设对象的初始化,进行模板实参推导重载决议,对于组成重载集而言,该类的各构造函数的签名与各个指引(除了返回类型)相匹配,并且由进行类模板实参推导的语境提供其初始化器。但若其初始化器列表由单个(可为 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*>

别名模版的推导

函数风格转型或变量定义以不带实参列表的别名模板 A 为类型说明符,其中 A 定义为 B<ArgList> 的别名,而 B 的作用域为非依赖且 B 为类模板或定义相似的别名模板时,推导将以同类模板的方式进行,除了替而从 B 的指引生成指引,方式如下:

  • 对于 B 的每个指引 f ,用模板实参推导B<ArgList> 推导 f 的返回类型的模板实参,除了若某些实参未被推出,推导也不会失败。
  • 替换上述推导结果到 f 中,若替换失败,则不生成指引;否则令 g 代表替换结果,构成指引 f' ,使得
  • f' 的形参类型与返回类型同 g
  • f 为模板,则 f' 是模板形参列表由出现于上述推导或(递归地)其默认模板实参的 A 的所有模板形参(包含其默认模板实参),后随 f 未被推导的模板形参(包含其默认模板实参)的函数模板;否则( f 非模板) f' 为函数
  • f' 的关联制约g 的关联制约和当且仅当 A 的实参可从结果类型推导的制约的合取。
template<class T> class unique_ptr { /* ... */ };
template<class T> class unique_ptr<T[]> { /* ... */ };
 
template<class T> unique_ptr(T*) -> unique_ptr<T>;   // #1
template<class T> unique_ptr(T*) -> unique_ptr<T[]>; // #2
 
template<class A> requires(!std::is_array_v<A>)
using unique_ptr_nonarray = unique_ptr<A>;
template<class A>
using unique_ptr_array = unique_ptr<A[]>;
 
// 对 unique_ptr_nonarray 生成的推导指引:
// 从 #1 ( unique_ptr<T> 从 unique_ptr<A> 的推导产生 T = A ):
//   template<class A>
//     requires(argument_of_unique_ptr_nonarray_is_deducible_from<unique_ptr<A>>)
//     auto F(A*) -> unique_ptr<A>;
// 从 #2 ( unique_ptr<T[]> 从 unique_ptr<A> 的推导不产生结果):
//   template<class T>
//     requires(argument_of_unique_ptr_nonarray_is_deducible_from<unique_ptr<T[]>>)
//     auto F(T*) -> unique_ptr<T[]>;
// 其中 argument_of_unique_ptr_nonarray_is_deducible_from 能定义为
//   template<class> class AA;
//   template<class A> class AA<unique_ptr_nonarray<A>> {};
//   template<class T>
//     concept argument_of_unique_ptr_nonarray_is_deducible_from =
//       requires { sizeof(AA<T>); };
 
// 对 unique_ptr_array 生成的推导指引:
// 从 #1 ( unique_ptr<T>从 unique_ptr<A[]> 的推导生成 T = A[] ):
//   template<class A>
//     requires(argument_of_unique_ptr_array_is_deducible_from<unique_ptr<A[]>>)
//     auto F(A(*)[]) -> unique_ptr<A[]>;
// 从 #2 ( unique_ptr<T[]> 从 unique_ptr<A[]> 的推导生成 T = A ):
//   template<class A>
//     requires(argument_of_unique_ptr_array_is_deducible_from<unique_ptr<A[]>>)
//     auto F(A*) -> unique_ptr<A[]>;
// 其中 argument_of_unique_ptr_array_is_deducible_from 能定义为
//   template<class> class BB;
//   template<class A> class BB<unique_ptr_array<A>> {};
//   template<class T>
//     concept argument_of_unique_ptr_array_is_deducible_from = requires { sizeof(BB<T>); };
 
// 用法:
unique_ptr_nonarray p(new int); // 推导出 unique_ptr<int>
// 从 #1 生成的推导指引返回 unique_ptr<int>
// 从 #2 生成的推导指引返回 unique_ptr<int[]> ,它被忽略,因为
//   argument_of_unique_ptr_nonarray_is_deducible_from<unique_ptr<int[]>> 得不到满足
 
unique_ptr_array q(new int[42]); // 推导出 unique_ptr<int[]>
// 从 #1 生成的推导指引失败(不能从 new int[42] 推出 A(*)[] 中的 A )
// 从 #2 生成的推导指引返回 unique_ptr<int[]>
(C++20 起)

注解

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

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>;
// ^ 此推导指引在 C++20 中能隐式生成
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>
(C++20 前)

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

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 初始化器列表构造函数能架空复制推导候选,导致产生包装 复制时跳过初始化器列表阶段