可变参数模板

写在前面

C++11的新特性–可变模版参数(variadic templates)是C++11新增的最强大的特性之一,它对参数进行了高度泛化,它能表示0到任意个数、任意类型的参数。相比C++98/03,类模版和函数模版中只能含固定数量的模版参数,可变模版参数无疑是一个巨大的改进。然而由于可变模版参数比较抽象,使用起来需要一定的技巧,所以它也是C++11中最难理解和掌握的特性之一。虽然掌握可变模版参数有一定难度,但是它却是C++11中最有意思的一个特性

主要内容

语法

可变参数模板和普通模板的语义是一样的,只是写法上稍有区别,声明可变参数模板时需要在typename或class后面带上省略号“…”

template <class... T>
void f(T... args);

上面的参数args前面有省略号,所以它就是一个可变模版参数,我们把带省略号的参数称为“参数包”,它里面包含了0到N(N>=0)个模版参数。我们无法直接获取参数包args中的每个参数的,只能通过展开参数包的方式来获取参数包中的每个参数,这是使用可变模版参数的一个主要特点,也是最大的难点,即如何展开可变模版参数。

可以这样理解 这个变量名的前面有…说明这个变量是个参数包参数包的类型是…前面的类型。
参数包分为模板参数包和函数参数包。

一个简单的可变模版参数函数:

template <class... T>
void f(T... args)
{    
    cout << sizeof...(args) << endl; //打印变参的个数
}

f();        //0
f(1, 2);    //2
f(1, 2.5, "");    //3

上面的例子中,f()没有传入参数,所以参数包为空,输出的size为0,后面两次调用分别传入两个和三个参数,故输出的size分别为2和3。由于可变模版参数的类型和个数是不固定的,所以我们可以传任意类型和个数的参数给函数f。这个例子只是简单的将可变模版参数的个数打印出来,如果我们需要将参数包中的每个参数打印出来的话就需要通过一些方法了。

展开可变模版参数函数的方法一般有两种:
一种是通过递归函数来展开参数包,
另外一种是通过逗号表达式来展开参数包。

递归函数展开

#include <iostream>
using namespace std;
//递归终止函数
void print()
{
   cout << "empty" << endl;
}
//展开函数
template <class T, class ...Args>
void print(T head, Args... rest)
{
   cout << "parameter " << head << endl;
   print(rest...);
}


int main(void)
{
   print(1,2,3,4);
   return 0;
}

展开函数至少需要一个参数所以到展开到参数为0的时候将调用的是递归终止函数输出empty。
同时展开函数需要有一个指定的参数这样每次递归的时候将会减少一个参数。
递归调用的过程是这样的:

print(1,2,3,4);
print(2,3,4);
print(3,4);
print(4);
print();

可变模板参数求和的例子

template<typename T>
T sum(T t)
{
    return t;
}
template<typename T, typename ... Types>
T sum (T first, Types ... rest)
{
    return first + sum<T>(rest...);//这里的语法将rest的模式进行展开后形成,分隔的参数再进行传递
}

sum(1,2,3,4); //10

可以将终止函数设置为一个模板参数的函数模板这样递归到一个参数的时候递归结束。
至于最后没有参数的时候为什么选取的是非可变参数版本是因为同样都可以接受的情况下非可变参数模板比可变参数模板更加的特化所以编译器会选择更加特化的版本。
可见通过递归展开和参数包的语法支持可以实现任意参数的函数。这使得不用为了没有可变参数的函数而头疼。
对比python等动态类型的语言,本身语法层面就支持可变参数的高级特性,此时c++也将可以实现。
我个人理解的就是,虽然c++并没有内置这样的语法,但是c++11之后添加了可变模板参数这样一个特性之后使得c++也具备更多的高级特性,只是需要手动的实现。

模式… 展开语法

c++语言支持复杂的按照给定的模式进行参数包展开的语法。
将每个参数按照…前面的模式进行展开,展开成逗号分隔的列表。

逗号表达式展开参数包

递归函数展开参数包是一种标准做法,也比较好理解,但也有一个缺点,就是必须要一个重载的递归终止函数,即必须要有一个同名的终止函数来终止递归,这样可能会感觉稍有不便。有没有一种更简单的方式呢?其实还有一种方法可以不通过递归方式来展开参数包,这种方式需要借助逗号表达式和初始化列表。比如前面print的例子可以改成这样:

template <class T>
void printarg(T t)
{
   cout << t << endl;
}

template <class ...Args>
void expand(Args... args)
{
   int arr[] = {(printarg(args), 0)...};//实现的语法:...在这里就是将后面的参数按照前面这样的形式进行展开。
}

expand(1,2,3,4);

这个例子将分别打印出1,2,3,4四个数字。这种展开参数包的方式,不需要通过递归终止函数,是直接在expand函数体中展开的, printarg不是一个递归终止函数,只是一个处理参数包中每一个参数的函数。这种就地展开参数包的方式实现的关键是逗号表达式。我们知道逗号表达式会按顺序执行逗号前面的表达式,比如:

d = (a = b, c); 

这个表达式会按顺序执行:b会先赋值给a,接着括号中的逗号表达式返回c的值,因此d将等于c。
逗号表达式会从左往右计算每个子表达式的结果,并将最后一个逗号后面的表达式的结果返回。

expand函数中的逗号表达式:(printarg(args), 0),也是按照这个执行顺序,先执行printarg(args),再得到逗号表达式的结果0。执行printarg(args)的过程当中会提取args当中的一个参数,将剩余的参数往后展开。注意这里…的语法,也就是起一个省略的作用,编译器识别这样的语法应该是没有歧义的。

同时还用到了C++11的另外一个特性——初始化列表,通过初始化列表来初始化一个变长数组, {(printarg(args), 0)…}将会展开成((printarg(arg1),0), (printarg(arg2),0), (printarg(arg3),0), etc… ),最终会创建一个元素值都为0的数组int arr[sizeof…(Args)]。由于是逗号表达式,在创建数组的过程中会先执行逗号表达式前面的部分printarg(args)打印出参数,也就是说在构造int数组的过程中就将参数包展开了,这个数组的目的纯粹是为了在数组构造的过程展开参数包。

猜你喜欢

转载自blog.csdn.net/zhc_24/article/details/82085860