如何使用类模板参数推断

类模板参数推导(CTAD)是一种C++ 17核心语言特性,它减少代码冗长。C++ 17的标准库也支持CTAD,因此在升级工具集后,可以使用STD类型:STO::STO::vector:其他库中的类模板和您自己的代码将部分受益于CTAD自动,但有时它们需要一些新代码(扣除指南)才能充分受益。幸运的是,尽管模板元编程有着可怕的名声,但使用CTAD和提供演绎指南都非常容易!

null

CTAD支持在VS 2017 15.7及更高版本中提供 /std:c++17 以及/std:c++latest 编译器选项。

模板参数推导

C++ 98到C++ 14为函数模板执行模板参数推导。给定一个函数模板 template <typename RanIt> void sort(RanIt first, RanIt last); ,您可以而且应该排序 std::vector<int> 没有明确说明 RanIt std::vector<int>::iterator . 当编译器看到 sort(v.begin(), v.end()); ,它知道 v.begin() v.end() 所以它可以确定 RanIt 应该是。为模板参数确定模板参数的过程(根据标准中的规则,将函数参数的类型与函数参数进行比较)称为模板参数推导,这使函数模板比其他模板更有用。

然而,类模板并没有从这些规则中受益。如果你想建造一个 std::pair 从2开始 int s、 你不得不说 std::pair<int, int> p(11, 22); ,尽管编译器已经知道 11 22 int . 此限制的解决方法是使用函数模板参数推断: std::make_pair(11, 22) 退货 std::pair<int, int> . 与大多数解决方法一样,这也是有问题的,原因有几个:定义这样的助手函数通常涉及到模板元编程( std::make_pair() 需要执行完美的转发和衰减等),编译器吞吐量降低(因为前端必须实例化helper,而后端必须对其进行优化),调试更烦人(因为您必须逐步完成helper函数),而且仍有冗长的开销(额外的 make_ 前缀,如果你想要一个局部变量而不是临时变量,你需要说 auto ).

你好,CTAD世界

C++ 17将模板参数推导扩展为只给定一个类模板名称的对象的构造。现在,你可以说 std::pair(11, 22) 这相当于 std::pair<int, int>(11, 22) . 下面是一个完整的例子,C++ 17 static_assert 正在验证声明的 p 与相同 std::pair<int, const char *> :

C:Temp>type meow.cpp
#include <type_traits>
#include <utility>
int main() {
    std::pair p(1729, "taxicab");
    static_assert(std::is_same_v<decltype(p), std::pair<int, const char *>>);
}
C:Temp>cl /EHsc /nologo /W4 /std:c++17 meow.cpp
meow.cpp
C:Temp>

CTAD使用括号和大括号,命名变量和无名临时变量。

另一个例子:数组和更大的

C:Temp>type arr.cpp
#include <algorithm>
#include <array>
#include <functional>
#include <iostream>
#include <string_view>
#include <type_traits>
using namespace std;
int main() {
    array arr = { "lion"sv, "direwolf"sv, "stag"sv, "dragon"sv };
    static_assert(is_same_v<decltype(arr), array<string_view, 4>>);
    sort(arr.begin(), arr.end(), greater{});
    cout << arr.size() << ": ";
    for (const auto& e : arr) {
        cout << e << " ";
    }
    cout << "";
}
C:Temp>cl /EHsc /nologo /W4 /std:c++17 arr.cpp && arr
arr.cpp
4: stag lion dragon direwolf

这展示了一些巧妙的东西。首先,CTAD std::array 推断其元素类型和大小。第二,CTAD使用默认的模板参数; greater{} 构造类型的对象 greater<void> 因为它被宣布为 template <typename T = void> struct greater; .

你自己类型的CTAD

C:Temp>type mypair.cpp
#include <type_traits>
template <typename A, typename B> struct MyPair {
    MyPair() { }
    MyPair(const A&, const B&) { }
};
int main() {
    MyPair mp{11, 22};
    static_assert(std::is_same_v<decltype(mp), MyPair<int, int>>);
}
C:Temp>cl /EHsc /nologo /W4 /std:c++17 mypair.cpp
mypair.cpp
C:Temp>

在这种情况下,CTAD自动为 MyPair . 编译器看到 MyPair 正在构造,因此它对 MyPair 的构造函数。签字后( const A&, const B& )以及类型的参数 int , A B 推断为 int ,这些模板参数用于类和构造函数。

然而, MyPair{} 将发出编译器错误。那是因为编译器会试图推断 A B ,但没有构造函数参数和默认模板参数,因此无法猜测是否需要 MyPair<int, int> MyPair<Starship, Captain> .

扣款指南

一般来说,当类模板具有其签名提及所有类模板参数(如 MyPair 上面)。然而,有时构造函数本身是模板化的,这会破坏CTAD所依赖的连接。在这些情况下,类模板的作者可以提供“推断指南”,告诉编译器如何从构造函数参数推断类模板参数。

C:Temp>type guides.cpp
#include <iterator>
#include <type_traits>
template <typename T> struct MyVec {
    template <typename Iter> MyVec(Iter, Iter) { }
};
template <typename Iter> MyVec(Iter, Iter) -> MyVec<typename std::iterator_traits<Iter>::value_type>;
template <typename A, typename B> struct MyAdvancedPair {
    template <typename T, typename U> MyAdvancedPair(T&&, U&&) { }
};
template <typename X, typename Y> MyAdvancedPair(X, Y) -> MyAdvancedPair<X, Y>;
int main() {
    int * ptr = nullptr;
    MyVec v(ptr, ptr);
    static_assert(std::is_same_v<decltype(v), MyVec<int>>);
    MyAdvancedPair adv(1729, "taxicab");
    static_assert(std::is_same_v<decltype(adv), MyAdvancedPair<int, const char *>>);
}
C:Temp>cl /EHsc /nologo /W4 /std:c++17 guides.cpp
guides.cpp
C:Temp>

以下是STL中演绎指南最常见的两种情况:迭代器和完美转发。 MyVec 像一个 std::vector 因为它是以元素类型为模板的 T ,但它是可以从迭代器类型构造的 Iter . 调用范围构造函数提供了所需的类型信息,但是编译器不可能实现 Iter T . 这就是《扣除指南》的作用所在。在类模板定义之后,语法 template <typename Iter> MyVec(Iter, Iter) -> MyVec<typename std::iterator_traits<Iter>::value_type>; 告诉编译器“当你运行CTAD MyVec ,尝试对签名执行模板参数推断 MyVec(Iter, Iter) . 如果成功,则要构造的类型是 MyVec<typename std::iterator_traits<Iter>::value_type> . 这实际上是取消对迭代器类型的引用,以获得所需的元素类型。

另一种情况是完美的转发,在哪里 MyAdvancedPair 有一个完美的转发构造函数 std::pair 做。同样,编译器看到 A B T U 是不同的类型,它不知道它们之间的关系。在这种情况下,我们需要应用的转换是不同的:我们想要 decay (如果你不熟悉 decay ,您可以跳过此)。有趣的是,我们不需要 decay_t ,尽管我们可以使用这个类型特征,如果我们想要额外的冗长。相反,扣除指南 template <typename X, typename Y> MyAdvancedPair(X, Y) -> MyAdvancedPair<X, Y>; 足够了。这告诉编译器“当你运行CTAD的时候 MyAdvancedPair ,尝试对签名执行模板参数推断 MyAdvancedPair(X, Y) ,就好像它按值获取参数一样。这样的推论是腐朽的。如果成功,则要构造的类型为 MyAdvancedPair<X, Y> .”

这说明了一个关键的事实,CTAD和扣除指南。CTAD查看类模板的构造器,以及它的演绎指南,以确定要构造的类型。该演绎要么成功(确定唯一类型),要么失败。一旦选择了要构造的类型,重载解析将决定正常调用哪个构造函数。 CTAD不影响构造函数的调用方式。 为了 MyAdvancedPair (和 std::pair ),演绎指南的签名(从概念上按值获取参数)影响CTAD选择的类型。然后,重载解析选择完美转发构造函数,该构造函数通过完美转发获取其参数,就像类类型是用显式模板参数编写的一样。

CTAD和扣除指南也是非侵入性的。为类模板添加演绎指南不会影响现有代码,而现有代码以前是提供显式模板参数所必需的。这就是为什么我们能够在不破坏一行用户代码的情况下为许多STL类型添加演绎指南。

执行

在极少数情况下,您可能希望演绎指南拒绝某些代码。下面是方法 std::array 它是否:

C:Temp>type enforce.cpp
#include <stddef.h>
#include <type_traits>
template <typename T, size_t N> struct MyArray {
    T m_array[N];
};
template <typename First, typename... Rest> struct EnforceSame {
    static_assert(std::conjunction_v<std::is_same<First, Rest>...>);
    using type = First;
};
template <typename First, typename... Rest> MyArray(First, Rest...)
    -> MyArray<typename EnforceSame<First, Rest...>::type, 1 + sizeof...(Rest)>;
int main() {
    MyArray a = { 11, 22, 33 };
    static_assert(std::is_same_v<decltype(a), MyArray<int, 3>>);
}
C:Temp>cl /EHsc /nologo /W4 /std:c++17 enforce.cpp
enforce.cpp
C:Temp>

就像 std::array , MyArray 是一个没有实际构造函数的聚合,但是CTAD仍然通过演绎指南为这些类模板工作。 MyArray 的指南执行模板参数推导 MyArray(First, Rest...) ,强制所有类型相同,并根据有多少个参数来确定数组的大小。

类似的技术可用于使某些构造函数或所有构造函数的CTAD完全病态。不过,STL本身并不需要显式地这样做(CTAD不受欢迎的只有两类: unique_ptr shared_ptr . C++ 17支持两个 unique_ptrs shared_ptrs 到数组,但两者都是 new T new T[N] 返回 T * . 因此,没有足够的信息来安全地推断 unique_ptr shared_ptr 由原始指针构造的。碰巧的是,由于 unique_ptr 对花哨指针和 shared_ptr 的类型擦除支持,这两种方法都以阻止CTAD工作的方式更改构造函数签名。)

专家的角落案例:非演绎语境

这里有一些高级的例子,不打算被模仿;相反,它们旨在说明CTAD在复杂场景中的工作原理。

编写函数模板的程序员最终会了解“非推导上下文”。例如,函数模板 typename Identity<T>::type 无法推断 T 从那个函数参数。既然CTAD已经存在,非推导上下文也会影响类模板的构造函数。

C:Temp>type corner1.cpp
template <typename X> struct Identity {
    using type = X;
};
template <typename T> struct Corner1 {
    Corner1(typename Identity<T>::type, int) { }
};
int main() {
    Corner1 corner1(3.14, 1729);
}
C:Temp>cl /EHsc /nologo /W4 /std:c++17 corner1.cpp
corner1.cpp
corner1.cpp(10): error C2672: 'Corner1': no matching overloaded function found
corner1.cpp(10): error C2783: 'Corner1<T> Corner1(Identity<X>::type,int)': could not deduce template argument for 'T'
corner1.cpp(6): note: see declaration of 'Corner1'
corner1.cpp(10): error C2641: cannot deduce template argument for 'Corner1'
corner1.cpp(10): error C2514: 'Corner1': class has no constructors
corner1.cpp(5): note: see declaration of 'Corner1'

在corner1.cpp中, typename Identity<T>::type 阻止编译器推断 T 应该是 double .

这里有一个例子,一些但不是所有的构造函数都提到 T 在非推断的情况下:

C:Temp>type corner2.cpp
template <typename X> struct Identity {
    using type = X;
};
template <typename T> struct Corner2 {
    Corner2(T, long) { }
    Corner2(typename Identity<T>::type, unsigned long) { }
};
int main() {
    Corner2 corner2(3.14, 1729);
}
C:Temp>cl /EHsc /nologo /W4 /std:c++17 corner2.cpp
corner2.cpp
corner2.cpp(11): error C2668: 'Corner2<double>::Corner2': ambiguous call to overloaded function
corner2.cpp(7): note: could be 'Corner2<double>::Corner2(double,unsigned long)'
corner2.cpp(6): note: or       'Corner2<double>::Corner2(T,long)'
        with
        [
            T=double
        ]
corner2.cpp(11): note: while trying to match the argument list '(double, int)'

在corner2.cpp中,CTAD成功,但构造函数重载解析失败。CTAD忽略构造函数 (typename Identity<T>::type, unsigned long) 由于没有推导出的上下文,所以CTAD只使用 (T, long) 扣除。与任何函数模板一样,比较参数 (T, long) 到参数类型 double, int 推断 T 成为 double . (int可转换为 long ,足以进行模板参数推导;在CTAD确定之后 Corner2<double> 应该构造,构造函数重载解析同时考虑两个签名 (double, long) (double, unsigned long) 在替换之后,这些对于参数类型是不明确的 double, int (因为 int 两者均可转换 long unsigned long ,标准不喜欢这两种转换)。

专家转角案例:首选演绎指南

C:Temp>type corner3.cpp
#include <type_traits>
template <typename T> struct Corner3 {
    Corner3(T) { }
    template <typename U> Corner3(U) { }
};
#ifdef WITH_GUIDE
    template <typename X> Corner3(X) -> Corner3<X *>;
#endif
int main() {
    Corner3 corner3(1729);
#ifdef WITH_GUIDE
    static_assert(std::is_same_v<decltype(corner3), Corner3<int *>>);
#else
    static_assert(std::is_same_v<decltype(corner3), Corner3<int>>);
#endif
}
C:Temp>cl /EHsc /nologo /W4 /std:c++17 corner3.cpp
corner3.cpp
C:Temp>cl /EHsc /nologo /W4 /std:c++17 /DWITH_GUIDE corner3.cpp
corner3.cpp
C:Temp>

CTAD的工作原理是对一组由类模板的构造函数和演绎指南生成的候选演绎(假设函数模板)执行模板参数演绎和重载解析。特别是,它遵循重载解析的常规规则,只需添加几个附加项。重载解析仍然倾向于更专业的东西(N4713 16.3.3[over.match.best]/1.7)。当事情同样专业化时,有一个新的突破点:首选演绎指南(/1.12)。

在corner3.cpp中,如果没有扣除指南 Corner3(T) 构造函数用于CTAD(而 Corner3(U) 没有用于CTAD,因为它没有提到),和 Corner3<int> 是构造的。添加扣除指南时,签名 Corner3(T) Corner3(X) 同样专业化,因此第/1.12段中的步骤和首选的扣除指南。这说明要构造 Corner3<int *> (然后呼叫 Corner3(U) 具有 U = int ).

报告错误

请让我们知道您对VS的看法。您可以通过IDE的“报告问题”报告错误,也可以通过web报告错误:转到 VS开发者社区 然后点击C++标签。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享