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

来自cppreference.com
< 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}; // 分配的类型是 is 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 类型

目录

[编辑] 自动推导指引

函数风格转型或变量声明以初等类模板 C 名称为类型指定符,而不带实参列表时,将以如下方式继续推导:

  • 若已定义 C ,则对每个声明于该具名初等模板的构造函数(或构造函数模板) Ci (若已定义之),构造一个虚设函数模板 Fi ,使得
  • Fi 的模板形参是 C 的模板形参后随(若 Ci 是构造函数模板) Ci 的模板形参(亦包含默认模板实参)
  • Fi 的函数参数是构造函数的参数
  • Fi 的返回类型是 C 后随环绕于 <> 的类模板的模板形参
  • 若未定义 C 或未声明任何构造函数,则添加一个额外的虚设函数模板,导出自假想构造函数 C()
  • 任何情况下,都添加导出自假想构造函数 C(C) 的额外虚设函数模板,称之为复制推导候选。

然后,为初始化假想类类型的虚设对象,进行模板实参推导重载决议 ,为了组成重载集的目的,该类的构造函数签名匹配指引(除了返回类型),并且由进行类模板实参推导的语境提供初始化器。除了若 initializer_list 由单个(可为 cv 限定的) U 类型表达式组成,其中 UC 的特化或导出自 C 的特化的类,则省去列表初始化的第一阶段(考虑 intializer_list 构造函数)。

虚设构造函数是假想类类型的公开成员。若推导指引从显式构造函数组成,则它们为 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) ”为初始化器的直接初始化
// 选择对应指引 F1 有 T = double 的构造函数
// 对于 F1 有 T=double ,返回类型是 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-declaration-clause ) -> simple-template-id ;

用户定义推导指引必须指名一个类模板,且必须引入于类模板的同一语义作用域(可以是命名空间或外围类),而且对于成员类模板,必须拥有同样的访问,但推导指引不成为该作用域的成员。

推导指引不是函数且无身体。推导指引不会为名称查找所找到,并且在推导类模板参数时不参与重载决议,除了与其他推导指引对立的重载决议。同一类模板的推导指引无法于同一翻译单元再度声明。

// 模板的声明
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*>

[编辑] 注意

类模板实参推导仅若不给出模板参数才进行。若指定了至少一个模板参数,则不发生推导。

std::tuple t(1, 2, 3);              // OK :推导
std::tuple<int,int,int> t(1, 2, 3); // OK :提供所有参数
std::tuple<int> t(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>;
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>

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

template<class T> struct S { S(T); };
S(char const*) -> S<std::string>;
S s{"hello"}; // 推出 S<std::string>

在类模板的作用域中,无参数列表的模板名是注入的类名,而且可用作类型。在该情况下,类模板推导不发生,且必须显式提供模板参数:

template<class T>
struct X {
  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<Iter::value_type>(b, e); // 必须指定我等所需
  }
};

重载决议中,在是否从指引生成函数木板上,偏序优先:若从构造函数生成的函数模板比从推导指引生成者更特化,则选择从构造函数生成者。

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};  // 错误,不能推导 from #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 包装的特殊情形外,对于 initializer_list 构造函数的强偏好保持于列表初始化中。

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>

[编辑] 缺陷报告

下列更改行为的缺陷报告追溯地应用于以前出版的 C++ 标准。

DR 应用于 出版时的行为 正确行为
P0702R1 C++17 initializer_list 构造函数能架空复制推导候选,导致包装 复制时跳过 initializer_list 阶段