类模板实参推导(CTAD)(C++17 起)
为了实例化一个类模板,需要知晓但不需要指定每个模板实参。在下列语境中,编译器会从初始化器的类型推导缺失的模板实参:
- 任意指定变量及变量模板初始化的声明,其中声明的类型是要推导实参的模板(可有 cv 限定):
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_insert_iterator<T>, // 其中 T 是容器 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)
的额外的虚设函数模板,它被称为复制推导候选。
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
类型的表达式组成,其中 U
是 C
的特化或派生自 C
的特化的类,那么跳过列表初始化的第一阶段(考虑初始化器列表构造函数)。
这些虚设构造函数是该假想类类型的公开成员。如果推导指引从显式的构造函数组成,那么它们也是显式的。如果重载决议失败,那么程序非良构。否则,选中的 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
的用户定义推导指引时是 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*>
别名模版的推导当函数式转型或变量定义用到了以不带实参列表的别名模板
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 T> concept NonArray = !std::is_array_v<T>; template<NonArray 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 }; // #5,复制推导候选 A(A); 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++ 标准。
缺陷报告 | 应用于 | 出版时的行为 | 正确行为 |
---|---|---|---|
CWG 2376 | C++17 | 即使在声明的变量的类型和要推导实参的类模板不同时也会进行类模板实参推导 | 此时不进行类模板实参推导 |
P0702R1 | C++17 | 初始化器列表构造函数能架空复制推导候选,导致产生包装 | 复制时跳过初始化器列表阶段 |