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

来自cppreference.com
< cpp‎ | language

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

  • 任意指定变量及变量模板初始化的声明,其中声明的类型是要推导实参的模板(可有 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) 的额外的虚设函数模板,它被称为复制推导候选。
  • 另外,如果
  • C 有定义且满足聚合类型的要求,并假设它的任何待决基类都没有虚函数或虚基类),
  • 没有 C 的用户定义推导指引,且
  • 该变量由非空的初始化器列表 arg1, arg2, ..., argn 初始化(可使用指派初始化器),
那么可能会添加聚合推导候选。按以下方式从聚合体元素的类型产生聚合推导候选的形参列表:
  • ei 为要从 argi 初始化的(可能递归的)聚合体元素,其中
  • 如果 C(或它的自身就是聚合体的元素)拥有作为包展开的基类:
  • 如果该包展开是尾随的聚合体元素,那么认为它匹配初始化器列表的所有剩余元素;
  • 否则,认为该包为空。
  • 如果没有这种 ei,那么不添加聚合推导候选。
  • 否则,按以下方式确定聚合推导候选的形参列表 T1, T2, ..., Tn
  • 如果 ei 是数组且 argi花括号初始化器列表,那么 Ti 是到 ei 的声明类型的右值引用。
  • 如果 ei 是数组且 argi字符串字面量,那么 Ti 是到具有 const 限定的 ei 的声明类型的左值引用。
  • 否则,Tiei 的声明类型。
  • 如果有包因为因为它是非尾随聚合体元素而被跳过,那么在它原本的聚合体元素的位置插入形为 Pi ... 的形参包。(这通常会导致推导失败。)
  • 如果包是尾随的聚合体元素,那么将对应它的尾随形参序列替换成形为 Ti ... 的单个形参。
聚合推导候选是从假想构造函数 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 的特化的类,那么跳过列表初始化的第一阶段(考虑初始化器列表构造函数)。

这些虚设构造函数是该假想类类型的公开成员。如果推导指引从显式的构造函数组成,那么它们也是显式的。如果重载决议失败,那么程序非良构。否则,选中的 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*>

别名模版的推导

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

  • 对于 B 的每个指引 f,用模板实参推导B<ArgList> 推导 f 的返回类型的模板实参,但推导即使在有实参没有被推导出的时候也不会失败。如果推导因为其他原因失败,那么以空集作为推导出的模板实参集。
  • 替换上述推导结果到 f 中,如果替换失败,那么就不会生成指引;否则以 g 代表替换结果构成指引 f',使得:
  • f' 的形参类型和返回类型与 g 的相同
  • 如果 f 是模板,那么 f' 是模板形参列表包含了以下内容的函数模板;否则(f 不是模板)f' 是函数:
  • 在上述推导出现的 A 的所有模板形参(包含其默认模板实参)
  • 在前一条找到的默认模板实参(如果存在)中出现的 A 的所有模板形参(包含其默认模板实参)
  • 如果再次找到默认模板实参,那么递归地包含在这些默认模板实参中出现的 A 的所有模板形参(包含其默认模板实参)
  • 后随 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<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 初始化器列表构造函数能架空复制推导候选,导致产生包装 复制时跳过初始化器列表阶段