作者:@小萌新
专栏:@C++进阶
作者简介:大二学生 希望能和大家一起进步!
本篇博客简介:介绍C++11的可变参数模板
可变参数模板
- 可变参数模板的概念
- 可变参数模板的定义方式
- 参数包两种解开方式
- 递归展开参数包
- 逗号表达式展开参数包
可变参数模板的概念
可变参数是C++11新增的比较重要的特性之一
它对参数高度泛化 能够让我们创建接受可变参数的函数模板还有类模板
- 在C++11之前模板只能使用固定个数参数 所以可变参数模板特性无疑是一个巨大的进步 但是可变参数模板比较抽象 使用起来需要一定的技巧
- 其实在C++11之前就已经有了可变参数这个概念 比如说库函数中的printf函数 它就可以接受多个参数 但是这只是函数参数的可变参数 并不是模板的可变参数
可变参数模板的定义方式
在C++11中 可变参数模板的定义方式如下
template<class …Args>
返回类型 函数名(Args… args)
{
//函数体
}
我们写出一个特化版本的函数
template<class ...Args>
void ShowList(Args... args)
{}
对于上面的代码我们这里予以解释
- 模板参数 …Args 前面的省略号代表这是一个可变参数模板 我们将带有省略号的参数称为参数包 参数包里面可以有0~N(N > 0)个参数 args是参数包的形参
- 它们的名字并不是一定要叫Args和args 可以修改为别的名称
使用了这个模板之后我们就可以使用多种参数来调用这个函数
ShowList();
ShowList(1);
ShowList(1, 'A');
ShowList(1, 'A', string("hello"));
我们还可以通过sizeof来计算函数到底调用了多少个参数
template<class ...Args>
void ShowList(Args... args)
{
cout << sizeof...(args) << endl;
}
这里有一点特别重要 sizeof后面的…不能省略 否则编译器会报错
修改正确后的运行结果如下
虽然我们能很简单的通过sizeof关键字来获取参数包中参数的个数
但是我们缺很难知道参数包中每个参数的类型 这也是参数包使用中的一大难点
需要注意的是 我们不能通过下标访问操作符【】来访问参数包
接下来我们会介绍两种获取参数包中各个参数的方式
参数包两种解开方式
递归展开参数包
展开参数包的思路如下
- 我们给函数模板增加一个参数T 它的作用是分离出一个参数出来
- 在函数模板中递归调用该函数模板 调用时传入剩下的参数包
- 依次递归 直到所有的参数都被分离一次
代码表示如下
template<class T,class ...Args>
void ShowList(T value,Args... args)
{
cout <<typeid(value).name() << endl;
ShowList(args...);
}
但是这里我们会面临一个问题 这个问题是假如我们递归到了最后的无参
就没有ShowList函数能够匹配这个实例了
所以说我们必须还要再加一个无参版本的ShowList函数
void ShowList()
{
cout <<endl;
}
这样一来递归到最后的无参版本就会终止递归
这里还有两个注意点
- 如果外部调用ShowList函数时就没有传入参数 那么就会直接匹配到无参的递归终止函数
- 我们本意是想让外部调用ShowList函数时匹配的都是函数模板 并不是让外部调用时直接匹配到这个递归终止函数
所以说我们可以针对代码做以下改进
void ShowListArgs()
{
cout << endl;
}
template<class T,class ...Args>
void ShowListArgs(T value,Args... args)
{
cout <<typeid(value).name() << endl;
ShowListArgs(args...);
}
template<class ...Args>
void ShowList(Args ...args)
{
ShowListArgs(args...);
}
我们可以将展开函数和递归调用函数的函数名改为ShowListArgs
并且重新写一个模板函数 使用函数包作为参数
这样子不管传入多少的参数匹配的函数就都是ShowList了
演示结果如下
逗号表达式展开参数包
我们都知道 数组可以使用列表初始化
比如说像这样
int a[] = {1,2,3,4}
除此之外 如果参数包中各个参数的类型都是整型 那么也可以把这个参数包放到列表当中初始化这个整型数组 此时参数包中参数就放到数组中了
代码表示如下
template<class ...Args>
void ShowList(Args ...args)
{
int arr[] = { args... };
// 这里展开args省略号要在后面
for (auto e : arr)
{
cout << typeid(e).name() << " ";
}
cout << endl;
}
此时我们就可以像ShowList中传入多参数了 代码和演示结果如下
ShowList(1);
ShowList(1,2,3);
ShowList(1,2,3,4);
但是C++规定 一个容器中只能容纳一种类型的数据 所以当我们传入多种类型的参数就会发生下面的情况
这个时候提出的一个解决方案就是逗号表达式
逗号表达式 运行完所有的参数之后返回最后一个值
这样子只要我们设置最后一个值为一个整型 然后再创建一个新的函数来接受参数包的每个数据就好了
代码表示如下
// 模板函数打印出参数包中每个参数的类型
template<class T>
void Print(T& t)
{
cout << typeid(t).name() << " ";
}
template<class ...Args>
void ShowList(Args ...args)
{
// 这里展开args省略号要在后面
int arr[] = { (Print(args),0)... };
cout << endl;
}
试验数据和演示结果如下
ShowList(1,string("hello"));
ShowList(1, 1.1);
ShowList(1, 2, 3, 4);
但是此时仍然存在者一个问题 那就是在C++中 这段代码是会报错的
int arr[] = {};
C++语法规定 我们不能分配常量大小为0的数组
这个问题反应到我们现在写的代码上就是我们无法在无参的情况下调用函数
解决这个问题的方式也很简单 我们写出一个特化版本的函数就可以
void ShowList()
{
cout << endl;
}
还有就是关于省略逗号表达式的问题
逗号表达式存在的意义只是为了让容器中的类型统一 那么只要类型原本就是统一的 我们就能够省略逗号表达式
具体的思路如下
我们让Print函数返回一个int类型的值 之后在列表中展开即可
代码表示如下
template<class T>
int Print(T& t)
{
cout << typeid(t).name() << " ";
return 0;
}
void ShowList()
{
cout << endl;
}
template<class ...Args>
void ShowList(Args ...args)
{
// 这里展开args省略号要在后面
int arr[] = { Print(args)... };
cout << endl;
}
我们还是用之前相同的测试样例 测试后结果如下