暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

C++高级编程(第4版) 个人笔记 22.5 - VariaDic templates 可变参数模板

东拉西扯 2020-04-09
863

参考书籍: 《C++高级编程》(第4版)》


1.看代码粗学.

代码:

    #include <iostream>
    #include <bitset>


    void print(){/*不执行任何操作。*/}


    template <typename T, typename... Types>
    void print(const T& firstArg, const Types&... args)
    {
    std::cout << "args_size: " << sizeof...(args) << std::endl;
    std::cout << firstArg << std::endl;
    std::cout << "---------------" << std::endl;
    print(args...);
    }


    int main() {
    print(10.04, "hello", std::bitset<16>(1124), 629);
    return 0;
    }

    运行结果:

    ... 就是一个所谓的pack(包)

    用于template parameters,就是template parameters pack(模板参数);

    用于function parameter types,就是function parameter types pack(函数参数类型); 用于function parameters,就是function parameters pack(函数参数类型);

    sizeof... 运算符 查询形参包中的元素数量。语法;:sizeof...( 形参包 ) (C++11 起) 解释:返回形参包中的元素数量。

    2.细节:

    普通模板只可采取固定数量的模板参数。可变参数模板(variadic template)可接收可变数目的模板参数。例如,下面的代码定义了一个模板,它可以接收任何数目的模板参数,使用称为Types的参数(parameter pack):

      template<typename... Types>
      class MyVariadicTemplate {};

      注意:typename之后的三个点并非错误。这是为可变参数模板定义参数包的语法。参数包可以接收可变数目的参数。在三个点的前后允许添加空格。

      可用任何数量的类型实例化MyVariadicTemplalte,例如:


        MyVariadicTemplate<int> instance1;
        MyVariadicTemplate<std::stringdoublestd::list<int>> instance2;

        甚至可用零个模板参数实例化MyVariadicTemplalte

          MyVariadicTemplate<> instance3;

          为避免用零个模板参数实例化可变参数模板,可以像下面这样编写模板:

            template<typename T1,typename... Types>
            class MyVariadicTemplate {};

            有了这个定义后,试图通过零个模板参数实例化MyVariadicTemplate会导致编译错误。例如,JetBrains CLion会给出如下错误:

              // Error: wrong number of template arguments (0, should be at least 1) 

              不能直接遍历传给可变参数模板的不同参数。唯一方法是借助模板递归的帮助。下面通过两个例子来说明如何使用可变参数模板。

              2.1类型安全的变长参数列表.

              可变参数模板允许创建类型安全的变长参数列表。下面的例子定义了一个可变参数processValues(),它允许以类型安全的方式接收不同类型的可变数目的参数。函数processValues() 会处理变长参数列表中的每个值,对每个参数执行 handleValue() 函数。这意味着必须对每种要处理的类型编写handleValue() 函数,例如下例中的intdoublestring

                #include <iostream>


                using namespace std;


                void handleValue(int value) {
                cout << "Integer: " << value << endl;
                }


                void handleValue(double value) {
                cout << "Double: " << value << endl;
                }


                void handleValue(string_view value) {
                cout << "String: " << value << endl;
                }


                void processValues(){/*不执行任何操作。*/}


                template<typename T1, typename... Tn>
                void processValues(T1 arg1, Tn... args) {
                handleValue(arg1);
                processValues(args...);
                }


                int main() {
                processValues(1, 2, 3.1415926, "test_string", 1.1f);
                }

                在前面的例子中,三点运算符“...”用了两次。这个运算符出现在3个地方,有两个不同的含义。首先用在模板参数列表中typename的后面以及函数参数列表中类型Tn的后面。在这两种情况下,它都表示参数包。参数包可接收可变数目的参数。

                ... 运算符的第二种用法是在函数体中参数名args的后面。这种情况下,它表示参数包扩展。这个运算符会解包展开参数包,得到各个参数。它基本上提取出运算符左边的内容,为包中的每个模板参数重复该内容,并用逗号隔开。从前面的例子中取出以下行:

                  processValues(args...) ;

                  这一行将args参数包解包(或扩展)为不同的参数,通过逗号分隔参数,然后用这些展开的参数调用processValues() 函数。模板总是需要至少一个模板参数: T1。通过arg... 递归调用 processValues() 的结果是:每次调用都会少一个模板参数。

                  由于processValues() 函数的实现是递归的,因此需要采用一种方法来停止递归。为此,实现一个processValues() 函数,要求它接收零个参数。

                  测试结果:

                  这个例子生成的递归调用是:

                        processValues(1, 2, 3.1415926, "test_string", 1.1f);
                    handleValue(1);
                    processValues(2, 3.1415926, "test_string", 1.1f);
                    handleValue(2);
                    processValues(3.1415926, "test_string", 1.1f);
                    handleValue(3.1415926);
                    processValues( "test_string", 1.1f);
                    handleValue("test_string");
                    processValues( 1.1f);
                    handleValue(1.1f);
                                        processValues();

                    重要的是要记住,这种变长参数列表是完全类型安全的。processValues() 函数会根据实际类型自动调用正确的handleValue() 重载版本。C++中也会像通常那样自动执行类型转换。例如,前面例子中1.1f的类型为floatprocessValues() 函数会调用handleValue(double value), 因为从floatdouble的转换没有任何损失。然而,如果调用 processValues() 时带有某种类型的参数,而这种类型没有对应的handleValue() 函数,编译器会产生错误。

                    前面的实现存在一个小问题。由于这是一个递归的实现,因此每次递归调用processValues() 时都会复制参数。根据参数的类型,这种做法的代价可能会很高。你可能会认为,向processValues() 函数传递引用而不使用按值传递方法,就可以避免这种复制问题。遗憾的是,这样也无法通过字面量调用processValues() 了,因为不允许使用字面量引用,除非使用const引用。

                    为了在使用非const引用的同时也能使用字面量值,可使用转发引用(forwarding references)。以下实现使用了转发引用T&&,还使用st::forward() 完美转发所有”意味着,如果把 rvalue传递给processValues(),就将它作为rvalue引用转发;如果把lvalueIvalue引用传递给processValues(),就将它作为lvalue引用转发。

                      void processValues(){/*不执行任何操作。*/}


                      template<typename T1, typename... Tn>
                      void processValues(T1&& arg1, Tn&&... args) {
                      handleValue(std::forward<T1>(arg1));
                      processValues(std::forward<Tn>(args)...);
                      }

                      std::forward通常是用于完美转发的,它会将输入的参数原封不动地传递到下一个函数中,这个“原封不动”指的是,如果输入的参数是左值,那么传递给下一个函数的参数的也是左值;如果输入的参数是右值,那么传递给下一个函数的参数的也是右值。

                      有一行代码需要做进一步解释:

                        processValues(std::forward<Tn>(args)...);

                        ... ”运算用于解开参数包,它在参数包中的每个参数上使用std::forward用逗号把它们隔开。例如,假设args是一个参数包,有三个参数(a1a2a3),分别对应三种类型(A1A2A3)。扩展后的调用如下:

                          processValues(std::forward<Al>(a1),
                          std::forward<A2> (a2),
                                  std:: forward<A3> (a3)) ;

                          在使用了参数包的函数体中,可通过以下方法获得参数包中参数的个数:

                            int numOfArgs = sizeof... (args) ;

                            一个使用变长参数模板的实际例子是编写一个类似于 printf() 版本的安全且类型安全的函数模板。这是实践变长参数模板的一次不错练习。


                            欢迎关注公众号:c_302888524 发送:"C++高级编程(第3版)" 获取电子书


                            文章转载自东拉西扯,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

                            评论