C++11 可变模板参数总结:搞懂参数包、包扩展和 emplace
1. 为什么 C++11 需要可变模板参数?
在 C++11 之前,如果我们想写一个"参数个数可变、参数类型也可变"的函数,基本只能靠:
- 写很多重载
- 或者用
...(C 风格可变参数,类型不安全)
而可变模板参数(Variadic Templates)解决的是:
- 参数个数可变
- 参数类型可变
- 编译期类型安全
- 能和泛型、右值引用、完美转发组合
一句话:它是现代 C++ 泛型编程的基础设施。
2. 基础语法:模板参数包 & 函数参数包
2.1 模板参数包
cpp
template<class... Args>
Args 代表"0 个或多个类型"。
2.2 函数参数包
cpp
template<class... Args>
void Func(Args... args) {}
args 代表"0 个或多个函数参数"。
如果要支持完美转发,常见写法是:
cpp
template<class... Args>
void Func(Args&&... args) {}
3. sizeof...:统计参数包个数
cpp
#include <iostream>
using namespace std;
template<class... Args>
void PrintCount(Args&&... args)
{
cout << sizeof...(args) << endl; // 参数个数
//sizeof...(Args) 也可统计
}
int main()
{
double x = 2.2;
PrintCount(); // 0
PrintCount(1); // 1
PrintCount(1, string("xxxxx")); // 2
PrintCount(1.1, string("xxxxx"), x); // 3
}
4. 包扩展(最核心)
参数包不能像数组那样 args[i] 访问,能做的核心操作就是"展开(expand)"。
4.1 递归展开(C++11 经典写法)
cpp
#include <iostream>
#include <string>
using namespace std;
// 递归终止
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...);
}
int main()
{
Print();
Print(1);
Print(1, string("hello"));
Print(1, string("hello"), 2.2);
}
这段代码的本质:编译器会在编译期"展开"为多组具体函数调用。
4.2 模式扩展:Func(args...) 不是唯一玩法
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)...); // 包扩展模式:对每个 args 套一层 GetArg
}
关键理解:GetArg(args)... 等价于
GetArg(arg1), GetArg(arg2), GetArg(arg3)...
5. 可变模板参数 + 完美转发(高频考点)
5.1 为什么要 std::forward<Args>(args)...?
因为:有名字的变量都是左值表达式 。
如果你只写 args...,右值信息会丢失,可能触发错误重载或性能下降。
cpp
template<class... Args>
void Wrapper(Args&&... args)
{
Target(std::forward<Args>(args)...); // 保留原始值类别
}
5.2 一句话区分
std::move(x):无脑转右值std::forward<T>(x):按模板推导结果"有条件转发"
6. 真实价值场景:emplace 系列接口
emplace_back / emplace 本质是可变模板参数 + 完美转发。
cpp
template<class... Args>
void emplace_back(Args&&... args);
template<class... Args>
iterator emplace(const_iterator pos, Args&&... args);
相比 push_back:
push_back往往是"先构造对象,再拷贝/移动进容器"emplace_back可以"直接把构造参数传到底层,在容器内存上原地构造"
示例理解:
cpp
std::vector<std::pair<std::string, int>> v;
v.emplace_back("apple", 1); // 直接构造 pair<string,int>
这就是"参数包一路转发到节点/元素构造函数"的价值。
7. 可变模板参数常见坑
7.1 不能把参数包当数组用
错误思路:
cpp
// args[i] // 不支持
参数包不是运行时容器,而是编译期语法结构。
7.2 忘记递归终止函数
递归展开必须有"空包终止版本",不然会无限模板实例化报错。
7.3 忘记完美转发导致右值退化
Args&&... args + 下层调用 foo(args...),会把右值当左值传下去。
7.4 const 对象即使 move 也不一定能移动
const T 通常无法真正"偷资源",很多时候仍走拷贝语义。