Class template argument deduction (CTAD) (since C++17)
为了实例化一个 类模板 ,必须知晓所有模板参数,但并非所有模板参数都需要显式指定。在以下上下文中,编译器将从初始化器的类型推导模板参数:
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;
- new 表达式 :
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 { constexpr X(T) {} }; template<X x> struct Y {}; Y<0> y; // OK, Y<X<int>(0)> |
(C++20 起) |
目录 |
类模板实参推导
隐式生成的推导指引
当在函数式转型或变量声明中,类型说明符仅由主类模板
C
的名称构成(即没有伴随的模板实参列表)时,将按以下方式形成推导候选集:
-
若定义了
C,则对于命名主模板中声明的每个构造函数(或构造函数模板)C i,将构造一个虚构函数模板F i,使其满足以下所有条件:
-
-
F i的模板参数由C的模板参数组成,后接(若C i是构造函数模板)C i的模板参数(默认模板参数亦包含在内)。
-
|
(since C++20) |
-
-
F i的 形参列表 即为C i的形参列表。 -
F i的返回类型是C后接类模板的模板参数(用<>括起)。
-
-
如果
C未定义或未声明任何构造函数,则会添加一个额外的虚构函数模板,该模板由假设的构造函数C()按上述方式推导生成。
-
在任何情况下,都会额外添加一个虚构的函数模板,该模板如上所述从假设的构造函数
C(C)派生而来,称为复制推导候选函数。
-
对于每个
用户定义推导指引
G i,都会构造一个虚构函数或函数模板F i,使其满足以下所有条件:
-
-
F i的参数列表是G i的参数列表。 -
F i的返回类型是G i的简单模板标识符。 -
如果
G i具有模板参数(语法 (2) ),则F i是函数模板,其模板参数列表是G i的模板参数列表。否则,F i是函数。
-
template<class T> struct A { T t; struct { long a, b; } u; }; A a{1, 2, 3}; // aggregate deduction candidate: // 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{}}; // aggregate deduction candidate: // template<class... Args> // B<Args...> F(std::tuple<Args...>, Args...); // type of b is deduced as 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 // }; // 使用 "new double(2.0)" 作为初始化器 // 对 X 对象进行直接初始化 // 选择与指引 F1(T = double)对应的构造函数 // 对于 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}" 作为初始化器的直接列表初始化进行重载解析时 // 选择 F3,其中 U=int 且 V=double // 返回类型为 S<int>::N<int> // 结果: // S<int>::N<int> x{2.0, 1};
用户定义推导指南
用户定义推导指引的语法是带有尾随返回类型的函数(模板)声明语法,不同之处在于它使用类模板的名称作为函数名:
explicit
(可选)
template-name
(
parameter-list
)
->
simple-template-id
requires-clause
(可选)
;
|
(1) | ||||||||
template <
template-parameter-list
>
requires-clause
(可选)
explicit (可选) template-name
(
parameter-list
)
->
simple-template-id
requires-clause
(可选)
;
|
(2) | ||||||||
| template-parameter-list | - | 一个非空的逗号分隔的 模板形参 列表 |
| explicit | - |
一个
explicit
说明符
|
| template-name | - | 需要进行参数推导的类模板名称 |
| parameter-list | - | 一个(可能为空的) 形参列表 |
| simple-template-id | - | 一个 简单模板标识符 |
| requires-clause | - | (自 C++20 起) 一个 requires 子句 |
|
用户定义推导指引的参数不能具有占位符类型:不允许使用 简写函数模板 语法。 |
(since C++20) |
用户定义的推导指引必须命名一个类模板,且必须在类模板的同一语义作用域(可以是命名空间或外围类)中引入,对于成员类模板还必须具有相同的访问权限,但推导指引不会成为该作用域的成员。
推导指引并非函数,因此没有函数体。推导指引不会通过名称查找被发现,并且不参与重载决议,除非在推导类模板参数时 与其他推导指引进行重载决议 。对于同一类模板,在同一翻译单元中不能重复声明推导指引。
// 模板声明 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); // 正确:使用隐式生成的指南推导 T=int std::vector<double> v = {/* ... */}; auto d = container(v.begin(), v.end()); // 正确:推导 T=double container e{5, 6}; // 错误:不存在 std::iterator_traits<int>::value_type
用于重载决议目的的虚构构造函数(如上所述)在以下情况下是显式的:它们对应于由显式构造函数形成的隐式生成推导指引,或对应于被声明为 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}; // 正确,#1 推导为 A<int> 并完成初始化 A a3{0, i}; // 正确,#2 推导为 A<int> 并完成初始化 A a4 = {0, i}; // 正确,#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}; // 正确,#4 推导为 A<int> 并由 #2 完成初始化 A a7 = {0, i}; // 错误:#3 推导为 A<int&> A a8{0, i}; // 错误:#3 推导为 A<int&> // 注意:请查阅 https://github.com/cplusplus/CWG/issues/647,其中指出 // 示例 a7 和 a8 可能存在错误,可能应修正为: //A a7 = {0, i}; // 错误:#2 和 #3 同时匹配,重载决议失败 //A a8{i,i}; // 错误:#3 推导为 A<int&>, // // #1 和 #2 声明了相同的构造函数
在构造函数或构造函数模板的参数列表中使用成员类型定义或别名模板,其本身并不会使隐式生成的推导指引中的对应参数成为非推导语境。
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}; // 正确,推导出 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<A> 推导 unique_ptr<T> 得到 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<A> 推导 unique_ptr<T[]> 无结果): // 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<NonArray 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<A[]> 推导 unique_ptr<T> 得到 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<A[]> 推导 unique_ptr<T[]> 得到 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); // 正确:进行类型推导 std::tuple<int, int, int> t2(1, 2, 3); // 正确:提供了所有模板参数 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>>
除了复制与包装的特殊情况外,列表初始化中对初始化列表构造函数的强烈偏好仍然保持不变。
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之前版本
| 功能测试宏 | 值 | 标准 | 功能特性 |
|---|---|---|---|
__cpp_deduction_guides
|
201703L
|
(C++17) | 类模板的模板参数推导 |
201907L
|
(C++20) | 聚合体和类型别名的CTAD |
缺陷报告
以下行为变更缺陷报告被追溯应用于先前发布的C++标准。
| 缺陷报告 | 应用于 | 发布时的行为 | 正确行为 |
|---|---|---|---|
| CWG 2376 | C++17 | 即使声明变量的类型与将要推导参数的类模板不同,也会执行CTAD |
此情况下不执行
CTAD |
| CWG 2628 | C++20 | 隐式推导指引未传播约束条件 | 传播约束条件 |
| CWG 2697 | C++20 |
不确定是否允许在用户定义的推导指引中使用
简写函数模板语法 |
禁止使用 |
| CWG 2707 | C++20 | 推导指引不能包含尾随的 requires 子句 | 允许包含 |
| CWG 2714 | C++17 |
隐式推导指引未考虑
构造函数的默认参数 |
考虑默认参数 |
| CWG 2913 | C++20 |
CWG 2707
的解决方案导致推导指引
语法与函数声明语法不一致 |
调整了语法 |
| P0702R1 | C++17 |
初始化列表构造函数可能抢占
拷贝推导候选,导致包装行为 |
拷贝时跳过
初始化列表阶段 |