可变参数模板
4.1 基本语法及原理
基本语法
我们用省略号来指出一个模板参数或函数参数的表示一个包,在模板参数列表中,class...
或typename...
指出接下来的参数表示零或多个类型列表;在函数参数列表中,类型名后面跟...
指出接下来表示零或多个形参对象列表;函数参数包可以用左值引用或右值引用表示,跟前面普通模板一样,每个参数实例化时遵循引用折叠规则。
template <class ...Args>
void Func(Args... args) {} // 值传递版本
template <class ...Args>
void Func(Args&... args) {} // 左值引用版本
template <class ...Args>
void Func(Args&&... args) {} // 通用引用版本(可接受左值或右值)
参数包类型
有两种参数包:
- 模板参数包:表示零个或多个模板参数
- 函数参数包:表示零个或多个函数参数
计算参数包大小
可以使用sizeof...
运算符计算参数包中参数的个数:
template <class ...Args>
void Print(Args&&... args) {
cout << sizeof...(args) << endl; // 打印参数个数
}
原理
可变参数模板的原理与普通模板类似,编译器会根据实际调用情况实例化出对应参数个数和类型的函数。例如:
// 通过参数包个数实例化出函数模板
template <class T1>
void Print(T1&& arg1);
template <class T1, class T2>
void Print(T1&& arg1, T2&& arg2);
template <class T1, class T2, class T3>
void Print(T1&& arg1, T2&& arg2, T3&& arg3);
//编译本质这⾥会结合引⽤折叠规则实例化出以下四个函数
Print(1); // 实例化 Print(int&&)
Print(1, string("xxxxx")); // 实例化 Print(int&&, string&&)
Print(1.1, string("xxxxx"), x); // 实例化 Print(double&&, string&&, double&)
4.2 包扩展
包扩展是将参数包分解并应用模式的过程,通过在模式右边加...
来触发。
递归展开示例
输出结果
// 本质编译器将可变参数模板通过模式的包扩展,编译器推导的以下三个重载函数函数
//void ShowList(double x)
//{
// cout << x << " ";
// ShowList();
//}
//
//void ShowList(string x, double z)
//{
// cout << x << " ";
// ShowList(z);
//}
//
//void ShowList(int x, string y, double z)
//{
// cout << x << " ";
// ShowList(y, z);
//}
//void Print(int x, string y, double z)
//{
// ShowList(x, y, z);
//}
复杂包扩展
可以直接将参数包作为另一个函数的参数:
//GetArg(args)... 这是参数包展开的语法。对于Print(x,y,z)的调用,它会被展开为:
//GetArg(x), GetArg(y), GetArg(z)
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) {
// 注意GetArg必须返回或者到的对象,这样才能组成参数包给Arguments
Arguments(GetArg(args)...); // 对每个参数调用GetArg
}
本质可以理解为编译器编译时,包的扩展模式
将上面的函数模板扩展实例化为下面的函数
这相当于编译器生成了类似下面的代码:
void Print(int x, string y, double z) {
Arguments(GetArg(x), GetArg(y), GetArg(z));
}
//可以把...想象成一个"解包"操作符,它会把参数包中的每个元素都应用到前面的表达式
//这里是GetArg()上。这种模式在函数式编程中类似于"map"操作。
4.3 emplace系列接口
C++11为STL容器新增了emplace系列接口,它们使用可变参数模板实现更高效的插入操作。
emplace_back示例
list<pair<string, int>> lt;
// 传统方式:构造pair + 拷贝/移动构造
pair<string, int> kv("苹果", 1);
lt.push_back(kv); // 拷贝构造
lt.push_back(move(kv)); // 移动构造
// emplace方式:直接构造
lt.emplace_back("苹果", 1); // 直接在容器内构造pair
emplace实现原理
emplace接口通过将参数包一直传递到容器内部节点构造中:
使用完美转发std::forward
保持参数的值类别(左值/右值)
template <class... Args>
void emplace_back(Args&&... args) {
insert(end(), std::forward<Args>(args)...);
}
template <class... Args>
iterator insert(iterator pos, Args&&... args) {
Node* newnode = new Node(std::forward<Args>(args)...);
// ... 插入操作
}
为什么emplace更高效
-
避免临时对象:可以直接在容器内部构造对象,省去了构造临时对象再拷贝/移动的开销
-
灵活性:可以直接传递构造对象所需的参数,而不需要先构造对象