• A.6 变参模板
    • A.6.1 扩展参数包

    A.6 变参模板

    变参模板:就是可以使用不定数量的参数进行特化的模板。就像你接触到的变参函数一样,printf就接受可变参数。现在,就可以给你的模板指定不定数量的参数了。变参模板在整个C++线程库中都有使用,例如:std::thread的构造函数就是一个变参类模板。从使用者的角度看,仅知道模板可以接受无限个参数就够了,不过当要写这么一个模板或对其工作原理很感兴趣时,就需要了解一些细节。

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

    1. template<typename ... ParameterPack>
    2. class my_template
    3. {};

    即使主模板不是变参模板,模板进行部分特化的类中,也可以使用可变参数模板。例如,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...作为包的扩展。

    和可变参函数一样,变参部分可能什么都没有,也可能有很多类型项。例如,std::packaged_task<my_class()>中ReturnType参数就是my_class,并且Args参数包是空的,不过std::packaged_task<void(int,double,my_class&,std::string*)>中,ReturnType为void,并且Args列表中的类型就有:int, double, my_class&和std::string*。

    A.6.1 扩展参数包

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

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

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

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

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

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

    类型表达式会比较复杂,提供的参数包是在类型表达式中产生,并且表达式中使用...作为扩展。当参数包已经扩展 ,包中的每一项都会代替对应的类型表达式,在结果列表中产生相应的数据项。因此,当参数包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);

    这将会创建一个新参数包args,其是一组函数参数,而非一组类型,并且这里...也能像之前一样进行扩展。例如,可以在std::thread的构造函数中使用,使用右值引用的方式获取函数所有的参数(见A.1节):

    1. template<typename CallableType,typename ... Args>
    2. thread::thread(CallableType&& func,Args&& ... args);

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

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

    注意一下这个例子,包扩展包括对类型包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. std::forward<std::string>(args_3));
    10. }

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

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

    下面的函数会返回参数的数量:

    1. template<typename ... Args>
    2. unsigned count_args(Args ... args)
    3. {
    4. return sizeof... (Args);
    5. }

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