参数包是编译期类型/值占位符,非容器亦非数组,仅可展开不可直接访问;声明为ts...或args...,必须用...展开,支持类型包与值包共存,递归展开依赖模板重载与空包终止。

什么是参数包(parameter pack)
参数包不是容器,也不是数组,它是编译期的类型/值占位符,只能被展开(unpack),不能直接访问或遍历。你写 Ts... 或 args... 的那一刻,就声明了一个参数包——它本身不占运行时内存,也不支持 size()、operator[] 这类操作。
常见错误现象:error: parameter pack 'Ts' was not expanded with '...',说明你声明了包但忘了展开;或者试图对包做 sizeof(Ts)(没加 ...),编译直接失败。
- 参数包必须在声明处用
...标记,展开时也必须带...(位置决定展开方式) - 类型包(如
typename... Ts)和值包(如Ts&&... args)可共存,但不能混用同一组... - 包展开是“全展开”或“不展开”,没有部分展开语法(比如取前两个)
如何递归展开参数包(最常用模式)
靠函数模板重载 + 递归调用是最稳妥、最易理解的方式,尤其适合需要逐个处理参数的场景(比如日志打印、构造嵌套对象)。
核心逻辑:一个重载处理“至少一个参数”的情况,另一个处理“空包”作为递归终点。
立即学习“C++免费学习笔记(深入)”;
template<typename T, typename... Ts>
void print(T first, Ts... rest) {
std::cout << first << " ";
print(rest...); // 展开 rest,传给下一层
}
template<typename T>
void print(T last) {
std::cout << last << "\n";
}
注意:第二个重载不能写成 template void print() {...},否则空参数调用会匹配失败——因为 print() 没有参数,不满足第一个模板的 T, Ts... 约束,但也不匹配单参数版本(T 无法推导)。正确收尾是只留一个非包参数的重载。
如何用折叠表达式(C++17)一步展开
折叠表达式本质是编译器帮你自动展开成二元运算序列,适用于所有参数能统一参与同一操作的场景(求和、逻辑与、输出等),比递归更简洁、无调用开销。
两种形式:(expr op ...)(一元右折),(... op expr)(一元左折),以及带初始化的 (init op ... op expr)。
(std::cout 等价于 <code>std::cout-
(args + ...)要求所有args可相加,且至少一个参数;空包会编译失败 - 若需支持空包,改用带初值的折叠:
(0 + ... + args),此时空包结果为0 - 折叠只支持特定运算符(
+, -, *, /, %, &, |, ^, &&, ||, ==, !=, , >, ,等),不支持自定义函数调用
展开时容易忽略的引用和完美转发问题
参数包里的 T&&... 不是“右值引用包”,而是“万能引用包”(universal reference),其绑定行为取决于实参。直接展开可能丢失值类别,导致意外拷贝或绑定失败。
典型陷阱:写 func(args...) 时,如果 func 是重载函数,args... 展开后每个参数都按原类型传递,但若原调用传的是临时量,而 func 只接受左值引用,就会编译失败。
- 正确做法是用
std::forward<ts>(args)...</ts>保持原始值类别 - 模板参数必须是右值引用形式(
typename... Ts)+ 实参用Ts&&...声明,才能配合std::forward - 别写
std::move(args)...—— 它强制转右值,会破坏左值实参的语义 - 如果只是传给
printf这类 C 函数,或参数类型已知为值类型,可不转发,但务必确认无生命周期风险
参数包展开看着只是加三个点,但类型推导、引用折叠、SFINAE 交互都在背后咬着牙工作。漏掉一个 std::forward,或错放一个 ... 位置,编译器报的错往往离真相很远。










