模板一直是C++泛型编程的基石,但C++98的模板参数个数是固定的。碰到参数个数不确定的场景,只能写一堆重载,或者用宏。C++11引入了可变参数模板,把类型和个数的泛化同时解决了。
目录
[1. 基本语法](#1. 基本语法)
[2. 包扩展](#2. 包扩展)
[3. emplace系列:比push_back更进一步](#3. emplace系列:比push_back更进一步)
1. 基本语法
class ...Args定义模板参数包,Args... args定义函数参数包。
cpp
template <class ...Args>
void Print(Args&&... args) {
cout << sizeof...(args) << endl; // 包中参数个数
}
调用时,编译器会根据实参实例化出对应个数和类型的函数:
cpp
Print(); // 实例化 void Print();
Print(1); // 实例化 void Print(int&&);
Print(1, string("hello")); // 实例化 void Print(int&&, string&&);
本质上,可变参数模板还是一对一的实例化,只是帮我们把重复劳动交给了编译器。
2. 包扩展
参数包本身不能像数组一样遍历,能用它做的事情几乎只有一件事:扩展。
最常见的展开方式是递归地取第一个参数,把剩余包继续往下传:
cpp
// 递归终止:参数包为空
void ShowList() {
cout << endl;
}
template <class T, class ...Args>
void ShowList(T x, Args... args) {
cout << x << " ";
ShowList(args...); // 包扩展:把剩余参数展开传给下一层
}
template <class ...Args>
void Print(Args... args) {
ShowList(args...);
}
编译时递归展开的过程:
text
Print(1, string("hello"), 2.2);
→ ShowList(1, string("hello"), 2.2);
→ cout << 1 << " "; ShowList(string("hello"), 2.2);
→ cout << "hello" << " "; ShowList(2.2);
→ cout << 2.2 << " "; ShowList(); // 终止
还有一种更"现代"的写法,把参数包直接作为实参交给另一个函数:
cpp
template <class T>
const T& GetArg(const T& x) {
cout << x << " ";
return x;
}
template <class ...Args>
void Arguments(Args... args) {}
template <class ...Args>
void Print(Args... args) {
Arguments(GetArg(args)...);
// 包扩展为:Arguments(GetArg(x1), GetArg(x2), ...)
}
这个写法抽象但有美感:GetArg(args)...在每个参数上调用GetArg,再把结果打包传给Arguments。
3. emplace系列:比push_back更进一步
C++11给STL容器新增了emplace_back、emplace系列接口。它们也是可变参数模板,使用方式上兼容push_back和insert,但多了一个关键能力:直接用构造对象的参数,在容器内部原地构造。
cpp
list<demo::string> lt;
demo::string s1("hello");
lt.emplace_back(s1); // 拷贝
lt.emplace_back(move(s1)); // 移动
lt.emplace_back("hello world"); // 直接用const char*构造,零拷贝
最后一行是push_back做不到的。push_back("hello world")会先用const char*构造一个临时string对象,再移动插入;而emplace_back直接把"hello world"作为参数包传下去,在容器节点的数据对象上进行一次构造。少了一次临时对象的创建和析构。
对于pair这类多参数对象,优势更明显:
cpp
list<pair<demo::string, int>> lt1;
lt1.emplace_back("apple", 1); // 直接在节点内构造pair,无需拼接pair对象
实现原理也不复杂。以list为例:
cpp
template <class... Args>
void emplace_back(Args&&... args) {
insert(end(), forward<Args>(args)...); // 完美转发参数包
}
template <class... Args>
iterator insert(iterator pos, Args&&... args) {
Node* newnode = new Node(forward<Args>(args)...); // 构造节点时转发
// ... 链接节点 ...
}
节点构造函数也是可变参数模板:
cpp
template <class... Args>
ListNode(Args&&... args)
: _next(nullptr), _prev(nullptr),
_data(forward<Args>(args)...) // 转发给_data的构造
{}
一环扣一环,参数包被完美转发到最终存储位置。中途没有临时对象,也没有多余的移动或拷贝。这就是emplace系列的效率来源。
结合完美转发传递参数包 是emplace实现的关键。展开参数包时需要写成forward<Args>(args)...,这样才能保留每个参数的原始值类别。如果忘了forward,参数包中的右值到达节点构造时就会变成左值,退化成拷贝。
在实际工程中,emplace_back已经基本可以取代push_back,可读性不降,效率有提升的可能。写C++11以上的代码,习惯用它。