55 / 80 · C++17 · 约 9 分钟
参数包与 fold expression:先处理空包
可变参数模板保留每个实参的类型,折叠表达式把参数包组合为一个表达式。可靠的设计需要先决定空包结果、初始值类型与求值顺序,不能把省略号当成自动安全的循环或任意长度数组。
参数包不是运行期容器
C++11 的 class... Ts 声明类型参数包,Ts... values 形成对应函数参数包。包可以有零个元素,每个元素类型也可以不同;sizeof...(Ts) 给出数量。展开会在允许的位置生成一组语法结构,不是生成一个可随意用下标访问的数组。
与 C 风格省略号不同,可变参数模板保留类型信息,因此可以在实例化时检查每个实参的约束。若数据本来就是运行期同类型序列,vector 或 span 往往更合适;不能为了少写一个循环而把运行期问题变成无限增加的模板实例。
折叠方向与初值都影响结果
C++17 的 (init + ... + values) 是带初值的左折叠,相当于从 init 开始逐项结合。减法等非结合运算尤其要区分左折叠与右折叠;即使加法,浮点舍入和用户自定义运算符也可能让不同结合方式得到不同结果。
空包必须先设计好。没有初值的一元折叠只有内建逻辑与、逻辑或和逗号具有规定的空包结果;求和通常应显式提供零。示例使用 0LL 并先把实参转为 long long,避免仅靠初始 int 零无意确定较窄的中间运算类型。
顺序依赖运算符,不依赖外观
括号规定结合结构,不会给所有运算符凭空增加从左到右的求值保证。若展开的操作有副作用,要选择本身具有所需顺序规则的运算符,例如对返回 void 的操作做逗号折叠;对内建 bool 的逻辑折叠则可以利用短路求值。
示例只处理明确可表示的小整数,并用编译期断言检查空包与多个实参。宽类型也不是无限精度:long long 求和仍可能溢出。泛型工具应限制输入或做溢出检查,不能把“模板支持很多类型”误解成“对任意值都数值安全”。
容易答错的地方
(... + values)在空包时没有合法结果;如果接口允许零参数,应显式提供合适的初值。- 折叠的结合方向不等于操作数求值顺序;在加法折叠里修改共享状态会让代码难以正确推理。
运行一个例子
最低标准 C++17 · 完整程序 · 下载 .cpp
#include <cassert>
#include <iostream>
#include <type_traits>
template<class... Ts>
constexpr long long sum(Ts... values) {
static_assert((std::is_integral_v<Ts> && ...), "integer inputs only");
return (0LL + ... + static_cast<long long>(values));
}
template<class... Ts>
constexpr bool all_positive(Ts... values) {
return ((values > 0) && ...);
}
int main() {
static_assert(sum() == 0);
static_assert(sum(1, 2, 3) == 6);
static_assert(all_positive());
assert(all_positive(1, 2, 3));
assert(!all_positive(1, 0, 3));
std::cout << sum() << ' ' << sum(1, 2, 3) << '\n';
}
在本地编译
g++ -std=c++17 -Wall -Wextra -Wpedantic -pthread modern-variadic.cpp -o example && ./example预期结果
0 6
CHECK YOUR UNDERSTANDING
合上答案,试着解释。
把 sum 改为乘积函数,允许空参数包时应选什么初值?空包为什么不应直接返回零?
查看参考答案
使用 (1LL * ... * static_cast<long long>(values)),让空包返回乘法单位元 1。这样 product(2, 3) 为 6,product() 为 1,合并两个参数组也保持乘积规则。初值零会使任何非空乘积都被吞成零;类型约束与溢出边界仍需保留。
继续查证
标准草案链接会随工作草案更新;本文版本标记对应示例最低要求,不表示草案中的所有新规则都适用于旧标准。