类模板参数推导


C ++ 17标准向该语言添加了一个新功能: 类模板参数推导(CTAD) 。 除了C ++中的新功能外,传统上还增加了拍摄自己肢体的新方法。 在本文中,我们将了解CTAD是什么,它的用途,它如何简化生活以及它包含的陷阱。


让我们从远处开始


回忆一下模板参数推导的全部内容及其用途。 如果您对C ++模板有足够的信心,则可以跳过本节,然后立即继续下一节。


在C ++ 17之前,模板参数的输出仅应用于功能模板。 在实例化函数模板时,您可能未明确指定可以从实际函数参数的类型推断出的那些模板参数。 推导规则非常复杂,它们专用于标准[temp.deduct]中的第17.9.2节 (以下简称“ 标准草案的免费版本;在以后的版本中,节编号可能会有所变化,因此我建议按照中指定的助记码进行搜索方括号)。


我们不会详细分析这些规则的所有复杂性;只有编译器开发人员才需要使用它们。 对于实际使用而言,记住一个简单的规则就足够了:如果可以根据可用信息明确地完成此操作,则编译器可以独立派生函数模板的参数。 派生模板参数的类型时,将像调用常规函数时一样应用标准转换(从文字类型中丢弃const ,将数组简化为指针,将函数引用简化为函数指针,等等)。


template <typename T> void func(T t) { // ... } int some_func(double d) { return static_cast<int>(d); } int main() { const int i = 123; func(i); // func<int> char arr[] = "Some text"; func(arr); // func<char *> func(some_func); // func<int (*)(double)> return 0; } 

所有这些简化了功能模板的使用,但是,可惜,这完全不适用于类模板。 实例化类模板时,必须显式指定所有非默认模板参数。 由于这个令人不愉快的特性,在标准库中出现了带有make_前缀的整个免费函数家族: make_uniquemake_sharedmake_pairmake_tuple等。


 //  auto tup1 = std::tuple<int, char, double>(123, 'a', 40.0); //   auto tup2 = std::make_tuple(123, 'a', 40.0); 

C ++ 17的新功能


在新标准中,通过类似于功能模板的参数,类模板的参数从调用的构造函数的参数派生:


 std::pair pr(false, 45.67); // std::pair<bool, double> std::tuple tup(123, 'a', 40.0); // std::tuple<int, char, double> std::less l; // std::less<void>,     std::less<> l template <typename 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)); //       template <typename T> struct F { F(T); } std::for_each(vi.begin(), vi.end(), Foo([&](int i) {...})); // F<lambda> 

立即值得一提的是在C ++ 17时适用的CTAD限制(也许这些限制将在标准的将来版本中删除):


  • CTAD不适用于模板别名:

 template <typename X> using PairIntX = std::pair<int, X>; PairIntX p{1, true}; //   

  • CTAD不允许部分输出参数(这对于常规的模板参数推导是如何工作的):

 std::pair p{1, 5}; // OK std::pair<double> q{1, 5}; // ,   std::pair<double, int> r{1, 5}; // OK 

同样,编译器将无法推断与构造函数参数的类型没有明确关联的模板参数的类型。 最简单的示例是一个容器构造函数,它接受一对迭代器:


 template <typename T> struct MyVector { template <typename It> MyVector(It from, It to); }; std::vector<double> dv = {1.0, 3.0, 5.0, 7.0}; MyVector v2{dv.begin(), dv.end()}; //     T   It 

类型TT没有直接关系,尽管我们开发人员确切地知道如何获得它。 为了告诉编译器如何直接输出不相关的类型,C ++ 17中出现了一种新的语言构造- 演绎指南 ,我们将在下一节中对其进行讨论。


奉献指南


对于上面的示例, 推导指南如下所示:


 template <typename It> MyVector(It, It) -> MyVector<typename std::iterator_traits<It>::value_type>; 

在这里,我们告诉编译器,对于具有两个相同类型参数的构造函数,可以使用构造std::iterator_traits<It>::value_type来确定T的类型。 请注意, 推论指南不在类定义之内,这使您可以自定义外部类的行为,包括C ++标准库中的类。


C ++标准17第17.10[temp.deduct.guide]中给出了演绎指南语法的正式描述:


 [explicit] template-name (parameter-declaration-clause) -> simple-template-id; 

推导指南之前的显式关键字禁止将其与copy-list-initialization一起使用


 template <typename It> explicit MyVector(It, It) -> MyVector<typename std::iterator_traits<It>::value_type>; std::vector<double> dv = {1.0, 3.0, 5.0, 7.0}; MyVector v2{dv.begin(), dv.end()}; //  MyVector v3 = {dv.begin(), dv.end()}; //   

顺便说一句, 演绎指南不必是模板:


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

详细的CTAD算法


C ++ Standard 17的第16.3.1.8节[over.match.class.deduct]中详细描述了派生类模板参数的形式规则。 让我们尝试找出它们。


因此,我们有一个应用CTAD的模板类型C。 为了选择要调用的构造函数和参数,对于C ,根据以下规则形成了许多模板函数:


  • 对于每个Ci构造函数,都会生成一个虚拟Fi模板函数。 Fi模板参数是C参数,后跟Ci模板参数(如果有),包括具有默认值的参数。 Fi函数的参数类型与Ci构造函数的参数类型相对应。 返回一个伪函数Fi类型C ,其参数与C模板参数匹配。

伪代码:


 template <typename T, typename U> class C { public: template <typename V, typename W = A> C(V, W); }; //    template <typename T, typename U, typename V, typename W = A> C<T, U> Fi(V, W); 

  • 如果未定义类型C或未指定构造函数,则上述规则适用于假设的构造函数C()
  • 构造函数生成了一个附加的伪函数;为此,他们甚至想出了一个特殊的名称: 复制归约候选
  • 对于每个推导指南 ,还将使用模板参数和推导指南参数以及对应于推导指南中->右侧类型的返回值生成虚拟函数Fi (在正式定义中,其称为simple-template-id )。

伪代码:


 template <typename T, typename V> C(T, V) -> C<typename DT<T>, typename DT<V>>; //    template <typename T, typename V> C<typename DT<T>, typename DT<V>> Fi(T,V); 

此外,对于所得的Fi虚拟函数集,将应用输出模板参数和重载分辨率的常用规则,但有一个例外:当使用初始化列表调用虚拟函数时,该初始化列表由单个类型为cv U的参数组成,其中U为特化C或从特化C继承的类型(以防万一,我将澄清cv == const volatile ;这样的记录意味着类型Uconst Uvolatile Uconst volatile U的处理方式相同),该规则优先考虑构造函数C(std::initializer_list<>) (已跳过有关列表初始化的详细信息 可以在C ++标准17的第16.3.1.7节[over.match.list]中找到具体化 。 一个例子:


 std::vector v1{1, 2}; // std::vector<int> std::vector v2{v1}; // std::vector<int>,   std::vector<std::vector<int>> 

最后,如果可以选择唯一最合适的伪函数,则选择相应的构造函数或推论指南 。 如果没有合适的程序,或者有几个同样合适的程序,则编译器将报告错误。


陷阱


CTAD用于初始化对象,传统上,初始化是C ++语言中非常令人困惑的部分。 随着C ++ 11中统一初始化的增加,射击的方式只会增加。 现在,您可以调用带有圆括号和大括号的对象的构造函数。 在许多情况下,这两个选项的工作原理相同,但并非总是如此:


 std::vector v1{8, 15}; // [8, 15] std::vector v2(8, 15); // [15, 15, … 15] (8 ) std::vector v3{8}; // [8] std::vector v4(8); //   

到目前为止,一切似乎都很合乎逻辑: v1v3调用采用std::initializer_list<int>的构造函数,从参数推断出int; v4找不到仅采用int类型的一个参数的构造函数。 但是这些仍然是花朵,前面是浆果:


 std::vector v5{"hi", "world"}; // [“hi”, “world”] std::vector v6("hi", "world"); // ?? 

如预期的那样, v5的类型将为std::vector<const char*>并使用两个元素进行初始化,但是下一行的功能完全不同。 对于向量,只有一个构造函数采用两个相同类型的参数:


 template< class InputIt > vector( InputIt first, InputIt last, const Allocator& alloc = Allocator() ); 

多亏了std::vector推导指南 std::vector “ hi”和“ world”将被视为迭代器,并且“之间”的所有元素都将添加到std::vector<char>类型的std::vector<char> 。 如果我们很幸运,并且这两个字符串常量连续存储在内存中,那么三个元素将落入向量中:'h','i','\ x00',但是最有可能的是,这样的代码将导致违反内存保护的行为并导致程序崩溃。


所用材料


草案标准C ++ 17
CTAD
CppCon 2018:Stephan T. Lavavej“所有人的类模板参数推导”

Source: https://habr.com/ru/post/zh-CN461963/


All Articles