A.6 变参模板

    和变参函数一样,变参部分可以在参数列表章使用省略号...代表,变参模板需要在参数列表中使用省略号:

    即使主模板不是变参模板,模板进行部分特化的类中,也可以使用可变参数模板。例如,std::packaged_task<>(见4.2.1节)的主模板就是一个简单的模板,这个简单的模板只有一个参数:

    1. template<typename FunctionType>
    2. class packaged_task;

    不过,并不是所有地方都这样定义;对于部分特化模板来说,其就像是一个“占位符”:

    1. template<typename ReturnType,typename ... Args>
    2. class packaged_task<ReturnType(Args...)>;

    部分特化的类就包含实际定义的类;在第4章,可以写一个std::packaged_task<int(std::string,double)>来声明一个以std::string和double作为参数的任务,当执行这个任务后结果会由std::future<int>进行保存。

    声明展示了两个变参模板的附加特性。第一个比较简单:普通模板参数(例如ReturnType)和可变模板参数(Args)可以同时声明。第二个特性,展示了Args...特化类的模板参数列表中如何使用,为了展示实例化模板中的Args的组成类型。实际上,因为这是部分特化,所以其作为一种模式进行匹配;在列表中出现的类型(被Args捕获)都会进行实例化。参数包(parameter pack)调用可变参数Args,并且使用Args...作为包的扩展。

    变参模板主要依靠包括扩展功能,因为不能限制有更多的类型添加到模板参数中。首先,列表中的参数类型使用到的时候,可以使用包扩展,比如:需要给其他模板提供类型参数。

    1. template<typename ... Params>
    2. struct dummy
    3. {
    4. std::tuple<Params...> data;
    5. };

    成员变量data是一个std::tuple<>实例,包含所有指定类型,所以dummy的成员变量就为std::tuple<int, double, char>。可以将包扩展和普通类型相结合:

    这次,元组中添加了额外的(第一个)成员类型std::string。其优雅指出在于,可以通过包扩展的方式创建一种模式,这种模式会在之后将每个元素拷贝到扩展之中,可以使用来表示扩展模式的结束。例如,创建使用参数包来创建元组中所有的元素,不如在元组中创建指针,或使用std::unique_ptr<>指针,指向对应元素:

    1. template<typename ... Params>
    2. struct dummy3
    3. {
    4. std::tuple<std::unique_ptr<Params> ...> unique_pointers;
    5. };

    类型表达式会比较复杂,提供的参数包是在类型表达式中产生,并且表达式中使用...作为扩展。当参数包已经扩展 ,包中的每一项都会代替对应的类型表达式,在结果列表中产生相应的数据项。因此,当参数包Params包含int,int,char类型,那么std::tuple<std::pair<std::unique_ptr<Params>,double> ... >将扩展为std::tuple<std::pair<std::unique_ptr<int>,double>,std::pair<std::unique_ptr<int>,double>,std::pair<std::unique_ptr<char>, double> >。如果包扩展被当做模板参数列表使用,那么模板就不需要变长的参数了;如果不需要了,参数包就要对模板参数的要求进行准确的匹配:

    1. template<typename ... Types>
    2. struct dummy4
    3. {
    4. std::pair<Types...> data;
    5. };
    6. dummy4<int,char> a; // 1 ok,为std::pair<int, char>
    7. dummy4<int> b; // 2 错误,无第二个类型
    8. dummy4<int,int,int> c; // 3 错误,类型太多

    可以使用包扩展的方式,对函数的参数进行声明:

    1. template<typename ... Args>
    2. void foo(Args ... args);

    函数参数包也可以用来调用其他函数,将制定包扩展成参数列表,匹配调用的函数。如同类型扩展一样,也可以使用某种模式对参数列表进行扩展。例如,使用std::forward()以右值引用的方式来保存提供给函数的参数:

    1. template<typename ... ArgTypes>
    2. {
    3. foo(std::forward<ArgTypes>(args)...);
    4. }

    注意一下这个例子,包扩展包括对类型包ArgTypes和函数参数包args的扩展,并且省略了其余的表达式。当这样调用bar函数:

    1. int i;
    2. bar(i,3.141,std::string("hello "));

    将会扩展为

    1. template<>
    2. void bar<int&,double,std::string>(
    3. int& args_1,
    4. double&& args_2,
    5. std::string&& args_3)
    6. {
    7. foo(std::forward<int&>(args_1),
    8. std::forward<double>(args_2),
    9. }

    这样就将第一个参数以左值引用的形式,正确的传递给了foo函数,其他两个函数都是以右值引用的方式传入的。

    最后一件事,参数包中使用sizeof...操作可以获取类型参数类型的大小,sizeof...(p)就是p参数包中所包含元素的个数。不管是类型参数包或函数参数包,结果都是一样的。这可能是唯一一次在使用参数包的时候,没有加省略号;这里的省略号是作为sizeof...操作的一部分,所以不算是用到省略号。

    就像普通的sizeof操作一样,的结果为常量表达式,所以其可以用来指定定义数组长度,等等。