C++ 变参模板
更新时间:2026-08-27。本文是
languages/cpp/主题高手层第 6 篇。C 语言里有变参函数printf,但它是运行时靠va_list一个个读参数,类型安全完全靠自觉。C++11 的变参模板(variadic templates)把"任意多个参数"搬到了编译期——参数个数和类型都在编译时确定,类型错误直接编译报错。std::tuple、std::make_unique的地基就是它。
本文要回答的问题
- 变参模板长什么样?"参数包"和"包展开"是什么意思?
- 递归展开和折叠表达式,两种展开方式怎么选?
sizeof...和sizeof有什么区别?std::tuple是怎么用变参模板装下任意多个类型不同的值的?
从 printf 说起:C 的做法
printf("%d %s\n", 42, "hello"); // C:运行时解析格式串C 的变参函数有三个致命问题:类型不安全(传错类型编译期发现不了)、需要格式串约定、运行时开销(va_list 遍历)。看一个经典翻车:
printf("%d", 3.14); // 传 double 却按 int 读,输出垃圾值C++ 变参模板从根上解决这个问题:每个参数的类型在编译期都已知,printf 式错误变成编译错误。
变参模板基本语法
2.1 参数包的两个位置
// 模板参数包:任意多个"类型"
template <typename... Ts>
void f(Ts... args); // 函数参数包:任意多个"值"
template <typename... Ts>
struct Tuple; // 类模板的参数包typename... Ts:模板参数包,装任意多个类型。Ts... args:函数参数包,装任意多个值(每个值对应一个类型)。- 展开语法:
Ts...在表达式中会把包展开成逗号分隔的序列。
2.2 sizeof... 操作符
template <typename... Ts>
void count(Ts... args) {
std::cout << sizeof...(Ts) << " 个类型, "
<< sizeof...(args) << " 个参数\n";
}
count(); // 0 个类型, 0 个参数
count(1, 2.5, 'a'); // 3 个类型, 3 个参数注意区分:sizeof(T) 是类型占多少字节;sizeof...(Ts) 是包里有几个元素。
| 操作符 | 作用 | 示例 |
|---|---|---|
sizeof(T) | 类型 T 占多少字节 | sizeof(int) = 4 |
sizeof...(Ts) | 参数包里有几个类型 | sizeof...(Ts) = 3 |
包展开:两种主流姿势
3.1 递归展开(C++11 原始方案)
参数包没法直接"全拿出来用",经典做法是每次拆一个 + 递归:
// 终止条件:0 个参数
void print_all() {}
// 递归:拿出第一个参数打印,剩下的继续展开
template <typename T, typename... Ts>
void print_all(T first, Ts... rest) {
std::cout << first << " ";
print_all(rest...); // 剩余参数递归展开
}
print_all(1, 2.5, "hi"); // 输出: 1 2.5 hi每次实例化拆掉第一个参数:print_all(1, 2.5, "hi") → print_all(2.5, "hi") → print_all("hi") → print_all()(终止)。编译期生成 4 个重载实例。
3.2 折叠表达式(C++17 更优雅)
C++17 引入折叠表达式,对"对全部参数做同一个操作"的场景大幅简化:
template <typename... Ts>
auto sum(Ts... args) {
return (args + ...); // 一元右折叠:a + b + c + ...
}
sum(1, 2, 3, 4); // 10四种折叠形式:
| 形式 | 写法 | 含义 |
|---|---|---|
| 一元右折叠 | (args + ...) | a + (b + (c)) |
| 一元左折叠 | (... + args) | ((a + b) + c) |
| 二元右折叠 | (init + ... + args) | init + (a + (b + c)) |
| 二元左折叠 | (... + args + init) | ((init + a) + b) + c |
用折叠表达式重写 print_all:
template <typename... Ts>
void print_all(Ts... args) {
((std::cout << args << " "), ...); // 逗号折叠:依次执行
}递归适合"每个参数逻辑不同",折叠适合"所有参数同一逻辑"。工程上折叠更简洁、实例化更少。
std::tuple:变参模板的巅峰应用
std::tuple 的核心:一个类模板装下任意多个不同类型。
std::tuple<int, double, std::string> t{42, 3.14, "hi"};
auto [i, d, s] = t; // C++17 结构化绑定简化实现思路(示意):
template <typename... Ts>
struct Tuple {};
template <typename T, typename... Ts>
struct Tuple<T, Ts...> : Tuple<Ts...> { // 递归继承
T value;
};
Tuple<int, double, std::string> t; // 继承链装下所有成员
std::make_unique<T>(args...) 同理:把任意多个构造参数包完美转发给 T 的构造函数(这里就接上了上一篇的 std::forward):
template <typename T, typename... Args>
unique_ptr<T> make_unique(Args&&... args) {
return unique_ptr<T>(new T(std::forward<Args>(args)...));
}forward<Args>(args)... 是包展开 + 完美转发的组合技:每个参数各自恢复值类别后传给构造函数。
变参模板 vs C 变参函数
| 对比项 | C va_list | C++ 变参模板 |
|---|---|---|
| 时机 | 运行时 | 编译期 |
| 类型安全 | 无(靠格式串) | 强(编译器检查) |
| 参数个数 | 运行时解析 | 编译期确定 |
| 性能 | 有运行时遍历开销 | 零开销 |
| 代表应用 | printf 系列 | tuple、make_unique、emplace_back |
C++11 之后新增 std::make_unique、emplace_back、结构化绑定等都依赖变参模板——它是现代 C++ 泛型库的"基础设施"。
与本站性能主线衔接
- 零成本抽象:变参模板的展开完全在编译期,运行时不产生额外循环或分支——与 L4 编译器选项 的模板机制同源。
- STL 内部:
emplace_back用变参模板 + 完美转发就地构造,避免临时对象,见 vector 与 make_unique。 - 代码体积:递归展开每层都生成实例,参数多时二进制膨胀,可用 nm 查看符号,衔接 L5 ELF。
一句话总结
变参模板把"任意多个参数"搬到编译期:参数包收集、sizeof... 数个数、递归或折叠表达式展开、配合 std::forward 完美转发,是 std::tuple、make_unique、emplace_back 的底层魔法;对比 C 的 va_list,它用编译期检查换来了类型安全和零运行时开销。