导览:
- 本章将从可变参数模板的概念开始讲起,到其究竟是如何做到实例化的
- 再从实例出发,探究该如何编写可变参数模板
- 最后涉及可变参数模板的运用
什么是可变参数模板
让我们先见一下可变参数模板
template<typename ...Args>
void test(Args... args)
{//...
}
概念:
一个可变参数模板(variadic template)就是一个接受可变数目参数的模板函数或模板类。可变数目的参数称为参数包(parameter packet)。存在两种参数包:模板参数包(template parameter packet),表示零个或多个模板参数;函数参数包(function parameter packet),表示零个或多个函数参数。
几个问题:
让我们带着以下几个问题去学习可变参数模板
- 可变参数模板如何实例化
- 如何书写可变参数模板
- 可变参数模板的运用—emplace系列
- 可变参数模板的运用—包扩展
- 可变参数模板的运用—转发参数包
可变参数模板如何实例化
上代码:
template<typename T, typename ...Args>
void func(const T& t)
{cout << t << endl << endl;
}
template<typename T, typename ...Args>
void func(const T& t, const Args&... args)
{cout << t << endl;func(args...);
}
int main()
{func(1,2,'c',4);func(1,2,'c');func(1,2);func(1);return 0;
}
代码思路:
这里书写了一个名为func()的函数,其形参中包含了一个函数参数包args。在main函数中,分别以不同的参数数目调用了func(),然后打印结果如下
1
2
c
41
2
c1
21
解释:
这里拿func(1,2,'c',4)
来说明。
先明确一点,这些实例化需要在编译阶段进行,到了运行阶段就挨个去调用参数符合的函数就行了。
可以这样理解:
当我func传参时,我的接收方是
func(const T& t, const Args&... args)
,我传递的是一个参数包,但其实这个包就是一连串的参数(args… -> {int,int,char,int}),接收时将我传递包中的第一个元素给给t,后面的元素又形成了一个新的参数包,然后一直递归去调用实例化,最后调用到只剩一个参数时就会去调用我们写的func(),不再实例化生成新的func()(我们特意增加了单参数的func(),此时编译器就不会自己实例化生成新的,而这个func()里面不会再递归实例化任何别的版本)。
func这个函数调用总共实例化生成了4个不同版本的函数,他们分别是:
void func(const int&,const int&,const char&,const int&);
void func(const int&,const char&,const int&);
void func(const char&,const int&);
void func(const int&);
如下是运行中的调用逻辑:
调用 | t | args… | 包中参数个数 | 向下传参 |
---|---|---|---|---|
func(1,2,‘c’,4) | 1 | 2,‘c’,4 | 3个参数的包 | 2,‘c’,4 |
func(2,‘c’,4) | 2 | ‘c’,4 | 2个参数的包 | ‘c’,4 |
func(‘c’,4) | ‘c’ | 4 | 1个参数的包 | 4 |
func(4) | 4 | null | 空包 | 无传参 |
解决几个小问题:
先引入一个运算符:sizeof...
这个运算符可以计算包中含有多少个元素,其返回值是一个常量表达式。具体代码如下:sizeof…(args)
-
为什么实例化递归时不能用
if
和sizeof...
判断参数包中的元素是否为空,进而跳出循环。答:因为if是运行时判断,而实例化是在编译阶段进行的,所以不能用if判断
-
既然args…是一个参数包,那么我能不能用args[i]的方式打印这个包中的某个参数
答:不能。同样,[]是运行时解析,而编译完后args…就不再是一个包,而是一堆参数了,自然也就不可以下标访问
如何书写可变参数模板
书写模板参数一直都是一件很苦恼的事 ,因为这个...
总是不知道放在何处
建议在写的时候多试试几种方式,哪种不报错就用哪种。以下给出几点建议,以供参考:
...
总是跟在参数名或类型名后面- 如果想要声明模板参数包,则跟在类型名后面
- 如果想要展开函数参数包,则跟在参数名后面
- 还有某些特殊情况,多换换位置也就过了
例子:
template<typename T, typename... Args> //声明模板参数包
void fooHelper(T t, Args... args) { //声明模板参数包// 处理t fooHelper(args...); // 递归调用,展开剩余参数
}
template<typename T>
void fooHelper(T t) { // 终止递归的情况
}
template<typename... Args>
void foo(Args... args) { fooHelper(args...); // 初始调用,展开所有参数
}
可变参数模板的运用
emplace系列
这是C++11后STL库中新增的函数
template <class... Args>void emplace_back (Args&&... args);
template <class... Args>
iterator emplace (const_iterator position, Args&&... args);template <class... Args>void emplace_back (Args&&... args);
template <class... Args>iterator emplace (const_iterator position, Args&&... args);
//...
STL库中对此的解释是:Construct and insert element
,构造同时插入元素
这里涉及了右值引用(但不是重点),如果想了解右值引用可以去看我的另一篇文章[C++11]右值引用
我们给出一个最简单的例子来说明:
class Date
{
public:Date(int a = 1, int b = 1, int c = 1):_a(a), _b(b), _c(c){cout << "Date()" << endl;}Date(const Date& d){cout << "const Date&" << endl;}Date(Date&& d){cout << "const Date&&" << endl;}
private:int _a;int _b;int _c;
};
int main()
{list<Date> lt1;list< pair<Date, Date>> lt2;Date d1(10,10,10);lt1.push_back(d1);lt1.push_back(move(d1));cout << "==================================" << endl;lt1.emplace_back(d1);lt1.emplace_back(move(d1));cout << "==================================" << endl;lt1.push_back({10,10,10}); cout << endl;lt1.emplace_back(10,10,10);cout << "==================================" << endl;pair<Date, Date> pr({1,1,1},{1,1,1});lt2.push_back(pr);cout << endl;lt2.push_back(move(pr));cout << "==================================" << endl;lt2.emplace_back(pr);cout << endl;lt2.emplace_back(move(pr));cout << "==================================" << endl;lt2.push_back({ (1, 1, 1), (1, 1, 1) });cout << endl;lt2.emplace_back((1, 1, 1), (1, 1, 1));return 0;
}
命令行打印结果:
Date() --构造
const Date& --拷贝构造
const Date&& --移动构造
==================================
const Date& --拷贝构造
const Date&& --移动构造
==================================
Date() --构造
const Date&& --移动构造Date() --构造
==================================
Date() --构造
Date() --构造
const Date& --拷贝构造
const Date& --拷贝构造
const Date& --拷贝构造
const Date& --拷贝构造const Date&& --移动构造
const Date&& --移动构造
==================================
const Date& --拷贝构造
const Date& --拷贝构造const Date&& --移动构造
const Date&& --移动构造
==================================
Date() --构造
Date() --构造
const Date&& --移动构造
const Date&& --移动构造Date() --构造
Date() --构造
插入方式 | vector lt1 | vector<pair<Date,Date>> lt1 |
---|---|---|
push_back(d1),push左值 | const Date& --拷贝构造 | const Date& --拷贝构造 const Date& --拷贝构造 |
emplace_back(d1),emplace左值 | const Date& --拷贝构造 | const Date& --拷贝构造 const Date& --拷贝构造 |
push_back(move(d1)),push右值 | const Date&& --移动构造 | const Date&& --移动构造 const Date&& --移动构造 |
emplace_back(move(d1)),emplace右值 | const Date&& --移动构造 | const Date&& --移动构造 const Date&& --移动构造 |
push_back({10,10,10}),push用initial_list | Date() --构造 const Date&& --移动构造 | null |
emplace_back(10,10,10),emplace用可变模板参数 | Date() --构造 | null |
push_back({(10,10,10),(10,10,10)}),push用initial_list | null | Date() --构造 Date() --构造 const Date&& --移动构造 const Date&& --移动构造 |
emplace_back((1, 1, 1), (1, 1, 1)),emplace用可变模板参数 | null | Date() --构造 Date() --构造 |
如果有朋友下定决心也尝试一下,结果可能会发现,哎?…怎么我的push和emplace一点规律都没有???甚至会发生错误!
这很有可能是你用的不是list,而是vector之类的顺序容器,这就不得不谈到vector的扩容问题了,对!这都是由于扩容搞的鬼,扩容导致了资源的重新分配。请记住:因为扩容问题导致vector和list的push、emplace的底层实现是很不同的。
是不是看起来很复杂,其实一点也不简单!但是我们只需要记住如下几点,就能明白emplace是用来干嘛的了
-
记住!emplace的作用就是利用可变模板参数优化效率
-
对于类创建的对象,无论是用push还是用emplace他们的结果都一样(例如上表前4个)。这是由于push里面也实现了右值引用版本(很显然,他们的效率只和左值、右值引用相关),并且用对象初始化与今天所讲的参数包没有任何关系。
-
最后4个,两两一组相比较,很明显能发现push和emplace版本相差甚大,这就是我们的可变参数模板优化的成果!其原理也很简单:
emplace中用接受的参数包直接去构造Date
而push版本中则需要在进push前先构造Date,再将参数传入
但其实也还好,毕竟push都实现了右值引用版本,传入参数的时候会调用移动构造,开销也不会太大。
包扩展
- 包扩展(Pack Expansion)是C++编程中的一个概念,它主要应用在模板元编程中,特别是可变参数模板函数中。包扩展允许程序员将参数包(parameter pack)展开为一系列独立的参数,以便在函数模板或类模板中使用。
- 对于一个参数包,除了获取其大小外,我们能对他做的唯一的事情就是
扩展
(expand)它。当扩展一个包时,我们还要提供用于每个扩展元素的模式
。
大家回顾一下前面的内容,其实我们一开始实例化打印的例子,就是一种包扩展
举一个比实例化打印更复杂的例子:
//如果你需要对某些信息进行处理(如错误信息),我们可以实现出将这个包中的每一个参数都当作实参传进一个函数
template <typename... Args>
ostream& errorMsg(ostream &os,const Args&...rest)
{return print(os,debug(rest)...);//debug是一个函数
}
//上述print就好像我们写如下代码
print(os,debug(x1),debug(x2),debug(x2)...);//这里的省略号就是省略号
转发参数包
这里就简要说明一下
转发参数包(Forwarding Parameter Pack)是C++模板编程中的一个高级特性,它允许你将参数包原封不动地传递给其他函数或模板,同时保持参数的原始类型和值类别(左值或右值)。这在编写通用包装器(wrappers)或代理(delegates)时特别有用,因为你可以确保参数在传递过程中不会被不必要地拷贝或移动。像前面的emplace就是使用该技术。