Namespaces
Variants

Class template argument deduction (CTAD) (since C++17)

From cppreference.net
C++ language
General topics
Flow control
Conditional execution statements
Iteration statements (loops)
Jump statements
Functions
Function declaration
Lambda function expression
inline specifier
Dynamic exception specifications ( until C++17* )
noexcept specifier (C++11)
Exceptions
Namespaces
Types
Specifiers
constexpr (C++11)
consteval (C++20)
constinit (C++20)
Storage duration specifiers
Initialization
Expressions
Alternative representations
Literals
Boolean - Integer - Floating-point
Character - String - nullptr (C++11)
User-defined (C++11)
Utilities
Attributes (C++11)
Types
typedef declaration
Type alias declaration (C++11)
Casts
Memory allocation
Classes
Class-specific function properties
Special member functions
Templates
Miscellaneous

为了实例化一个 类模板 ,必须知晓所有模板参数,但并非所有模板参数都需要显式指定。在以下语境中,编译器将从初始化器的类型推导模板参数:

  • 任何指定变量及变量模板初始化的 声明 ,其声明类型为该类模板(可能为 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
{
    constexpr X(T) {}
};
template<X x>
struct Y {};
Y<0> y; // OK, Y<X<int>(0)>
(since C++20)

目录

类模板实参推导

隐式生成的推导指引

当在函数式转换或变量声明中,类型说明符仅由主类模板 C 的名称构成(即没有伴随的模板实参列表)时,将按以下方式形成推导候选集:

  • 若定义了 C ,则对于命名主模板中声明的每个构造函数(或构造函数模板) C i ,将构造一个虚构函数模板 F i ,使其满足以下所有条件:
  • F i 的模板参数由 C 的模板参数后接(若 C i 是构造函数模板) C i 的模板参数组成(默认模板参数亦包含在内)。
  • F i 关联约束 C 的关联约束与 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 是函数。
  • 此外,如果
  • C 被定义且满足 聚合类型 的要求(假设任何依赖基类无虚函数或虚基类),
  • C 没有用户定义的推导指引,且
  • 变量从非空初始化器列表 arg1, arg2, ..., argn 初始化(可使用 指派初始化器 ),
则可能添加聚合推导候选。聚合推导候选的参数列表按如下方式从聚合元素类型生成:
  • e i 为将从 arg i 初始化的(可能递归的) 聚合元素 ,其中
  • C (或其本身为聚合的元素)具有作为 包展开 的基类:
  • 若包展开是尾随聚合元素,则视为匹配初始化器列表的所有剩余元素;
  • 否则,该包视为空。
  • 若不存在这样的 e i ,则不添加聚合推导候选。
  • 否则,按如下方式确定聚合推导候选的参数列表 T 1 , T 2 , ..., T n
  • e i 是数组且 arg i 花括号初始化列表 ,则 T i e i 声明类型的右值引用。
  • e i 是数组且 arg i 字符串字面量 ,则 T i e i 常量限定声明类型的左值引用。
  • 否则, T i e i 的声明类型。
  • 若因包是非尾随聚合元素而被跳过,则在原始聚合元素位置插入形式为 P j ... 的附加参数包(通常将导致推导失败)。
  • 若包是尾随聚合元素,则对应参数的尾随序列被替换为形式为 T n ... 的单个参数。
聚合推导候选是通过上述方式从虚构构造函数 C(T 1 , T 2 , ..., T n ) 派生的虚构函数模板。
在对聚合推导候选进行模板实参推导时,尾随参数包中的元素数量仅当无法通过其他方式推导时,才根据剩余函数实参的数量推导。
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
// };
// 使用 "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*>

别名模板的推导

当函数式转换或变量声明使用不带实参列表的别名模板 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 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<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<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);                // 正确:进行类型推导
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 初始化列表构造函数可能抢占拷贝推导候选,导致包装行为 拷贝时跳过初始化列表阶段