背景
在 C++11 之前,无论是类模板还是函数模板,都只能按其指定的样子,接受一组固定数量的模板参数;而C++11 加入了新的表示方法,允许任意个数、任意类别的模板参数,同时也不需要在定义时将参数的个数固定。
可变参数模板和普通模板的语义是一样的,只是写法上稍有区别,声明可变参数模板时需要在typename或class后面带上省略号“...”
template<typename... Ts>
void f(T... args);
上面的可变模版参数的定义当中,省略号的作用有两个:
- 声明一个参数包T... args,这个参数包中可以包含0到任意个模板参数;
- 在模板定义的右边,可以将参数包展开成一个一个独立的参数。
我们把带省略号的参数称为“参数包”,它里面包含了0到N(N>=0)个模版参数。我们无法直接获取参数包args中的每个参数的,只能通过展开参数包的方式来获取参数包中的每个参数,这是使用可变模版参数的一个主要特点,也是最大的难点,即如何展开可变模版参数。
那么我们定义了变长的模板参数,如何对参数进行解包呢?
首先,我们可以使用 sizeof...
来计算参数的个数:
#include <iostream>
template<typename... Args>
void f(Args... args) {
std::cout << sizeof...(args) << std::endl;
}
int main()
{
f(); // 输出0
f(1); // 输出1
f(1, ""); // 输出2
}
其次,如果我们需要将参数包中的每个参数打印出来的话怎么办呢?
那我们就需要对参数进行解包,到目前为止还没有一种简单的方法能够处理参数包,但有两种经典的处理手法:
递归模板函数
递归模板函数是一种标准的做法,但缺点显而易见的在于必须定义一个终止递归的函数。
展开参数包的函数有两个,一个是递归函数,另外一个是递归终止函数,参数包Args...在展开的过程中递归调用自己,每调用一次参数包中的参数就会少一个,直到所有的参数都展开为止,当没有参数时,则调用非模板函数print终止递归过程。
下例会输出每一个参数,直到为空时输出empty。
#include <iostream>
using namespace std;
//递归终止函数
void f()
{
cout << "empty" << endl;
}
//展开函数
template <class T, class ...Args>
void f(T head, Args... rest)
{
cout << "parameter " << head << endl;
f(rest...);
}
int main(void)
{
f(1, 2, 3, 4);
return 0;
}
递归调用的过程是这样的:
f(1,2,3,4);
f(2, 3, 4);
f(3, 4);
f(4);
f();
上面的递归终止函数还可以写成这样:
#include <iostream>
template<typename T>
void f(T value) {
std::cout << value << std::endl;
}
template<typename T, typename... Args>
void f(T value, Args... args) { //该函数不接受0个参数
std::cout << value << std::endl;
f(args...); //迭代f(T value, Args... args)本身,当参数剩下一个将调用f(T value)
}
int main() {
f(1, 2, "123", 1.1); //递归模板函数
return 0;
}
修改递归终止函数后,上例中的调用过程是这样的
f(1, 2, 3, 4);
f(2, 3, 4);
f(3, 4);
f(4);
借助逗号表达式和初始化列表
相关知识
逗号表达式:这种就地展开参数包的方式实现的关键是逗号表达式,逗号表达式会按顺序执行逗号前面的表达式。逗号表达式会按顺序执行逗号前面的表达式,比如:d = (a = b, a+c); 这个表达式会按顺序执行:b会先赋值给a,接着括号中的逗号表达式返回a+c的值,因此d将等于a+c。
初始化列表:通过初始化列表来初始化一个变长数组, { (func(args), 0)... }将会展开成( (func(arg1), 0), (func(arg2), 0), (func(arg3), 0), etc... ), 最终会创建一个元素值都为0的数组int arr[sizeof...(Args)]。
lambda表达式:定义[](){},声明并调用[](){}().
#include <iostream>
using namespace std;
int main()
{
[](){ //这里的()没有参数可以省略
cout << "Hello,World\n";
}();
}
实现代码:
#include <iostream>
template <class T>
void func(T t)
{
std::cout << t << std::endl;
}
template <class ...Args>
void test(Args... args)
{
int arr[] = { (func(args), 0)... };
}
int main() {
test(1, 2, 3, 4);
return 0;
}
其中的func并不是一个递归终止函数,只是一个处理参数包中每一个参数的函数。
改进代码
我们可以把上面的例子再进一步改进一下,将函数作为参数,就可以支持lambda表达式了,从而可以少写一个递归终止函数了,具体代码如下:
#include <iostream>
template<class F, class... Args>
void test(const F& f, Args&&...args)
{
std::initializer_list<int>{(f(std::forward<Args>(args)), 0)...};
}
int main() {
test([](int i) {std::cout << i << std::endl; }, 1, 2, 3);
return 0;
}
通过初始化列表,(lambda 表达式, value)... 将会被展开。由于逗号表达式的出现,首先会执行前面的 lambda 表达式,完成参数的输出。唯一不美观的地方在于如果不使用 return 编译器会给出未使用的变量作为警告。
改进代码(C++14)
C++14的新特性泛型lambda表达式.更泛化的lambda表达式,适配auto.编译下面这个代码需要开启 -std=c++14
#include <iostream>
template<typename T, typename... Args>
auto print(T value, Args... args) {
std::cout << value << std::endl;
return std::initializer_list<T>{([&](){
std::cout << args << std::endl;
}(), value)...};
}
int main() {
print(1, 2.1, "test"); //使用初始化列表
return 0;
}