17、C++17 折叠表达式:左折叠与右折叠、一元与二元折叠、在可变参数模板中的应用
可变参数模板是 C++11 引入的利器,但处理参数包一直有点麻烦。你想想看,要对一堆参数做求和、逻辑与、逗号分隔这些操作,以前得写递归模板或者用初始化列表技巧。C++17 的折叠表达式,说白了就是给参数包配了个「一键处理」的语法糖。
我个人习惯把折叠表达式理解为:把参数包里的元素,用同一个操作符挨个「折叠」起来。就像叠被子,一层一层叠上去,最后变成一个整体。
左折叠 vs 右折叠:方向决定结果
先看最基础的概念。折叠表达式分左折叠和右折叠,区别在于参数包从哪边开始「叠」。
左折叠:从左往右依次结合。语法是 ( ... op pack ) 或 ( init op ... op pack )。
右折叠:从右往左依次结合。语法是 ( pack op ... ) 或 ( pack op ... op init )。
举个例子,求和操作:
template<typename... Args>
auto leftSum(Args... args) {
return ( ... + args ); // 左折叠:((1+2)+3)+4
}
template<typename... Args>
auto rightSum(Args... args) {
return ( args + ... ); // 右折叠:1+(2+(3+4))
}
// 调用
auto result1 = leftSum(1, 2, 3, 4); // 结果 10
auto result2 = rightSum(1, 2, 3, 4); // 结果也是 10
嗯,这里要注意:对于加法这种满足结合律的操作,左右折叠结果一样。但换成减法试试?
auto r1 = leftSum(1, 2, 3, 4); // ((1-2)-3)-4 = -8
auto r2 = rightSum(1, 2, 3, 4); // 1-(2-(3-4)) = -2
结果完全不同!我在项目中遇到过这种坑——写日志格式化时用了左折叠拼接字符串,后来改成右折叠才发现顺序反了。所以一定要根据操作符的结合性选择方向。
一元折叠 vs 二元折叠:有无初始值
折叠表达式还分一元和二元,区别在于有没有初始值。
一元折叠:直接对参数包操作,没有初始值。语法就是 ( ... op pack ) 或 ( pack op ... )。
二元折叠:带一个初始值,参数包和初始值一起折叠。语法是 ( init op ... op pack ) 或 ( pack op ... op init )。
看代码更清楚:
// 一元左折叠
template<typename... Args>
auto sum(Args... args) {
return ( ... + args ); // 参数包不能为空!
}
// 二元左折叠(带初始值 0)
template<typename... Args>
auto sumWithInit(Args... args) {
return ( 0 + ... + args ); // 参数包可以为空,返回 0
}
// 调用
auto s1 = sum(1, 2, 3); // 6
// auto s2 = sum(); // 编译错误!一元折叠参数包不能为空
auto s3 = sumWithInit(); // 0,安全
auto s4 = sumWithInit(1, 2); // 3
我曾经在写配置解析器时,用一元折叠处理参数列表,结果用户传了空参数包,编译直接报错。后来改成二元折叠加个默认值,世界清净了。
在可变参数模板中的实战应用
折叠表达式最爽的地方,就是让可变参数模板的代码变得极其简洁。我挑几个常用场景说说。
场景一:打印所有参数
template<typename... Args>
void printAll(Args... args) {
(std::cout << ... << args) << std::endl;
}
// 调用
printAll(1, "hello", 3.14, 'A'); // 输出:1hello3.14A
注意这里用了二元左折叠,std::cout 是初始值。我以前用递归模板实现这个功能,代码量翻三倍不止。
场景二:检查所有条件是否成立
template<typename... Args>
bool allTrue(Args... args) {
return ( ... && args ); // 一元左折叠,逻辑与
}
// 调用
bool result = allTrue(true, 1, "hello"); // true,非零值和非空指针都转 true
逻辑与操作符有短路特性——一旦遇到 false 就停止求值。折叠表达式完美保留了这一点。
场景三:任意一个条件成立
template<typename... Args>
bool anyTrue(Args... args) {
return ( ... || args ); // 一元左折叠,逻辑或
}
同样支持短路求值。我在写参数校验时经常用这个,比手写一堆 if 优雅多了。
场景四:逗号表达式批量操作
template<typename... Args>
void processEach(Args... args) {
(process(args), ...); // 一元右折叠,逗号表达式
}
// 或者带初始值的版本
template<typename... Args>
void processEachWithInit(Args... args) {
(void(std::cout << args << " "), ...); // 二元左折叠
}
逗号表达式是折叠表达式的「瑞士军刀」——你可以对每个参数执行任意操作,然后丢弃返回值。我在日志系统里用这个批量格式化参数,一行代码搞定以前十几行的循环。
知识体系图
下面这张图帮你理清折叠表达式的核心脉络:
总结几个实用技巧
- 优先用二元折叠:除非你确定参数包永远不为空,否则给个初始值更安全。
- 左折叠是默认选择:大部分场景下我们期望从左到右处理,左折叠更符合直觉。
- 逗号折叠是万能工具:想对每个参数执行任意操作?用逗号折叠加 lambda 搞定。
- 注意操作符重载:如果自定义类型重载了操作符,折叠表达式会自动使用重载版本。
折叠表达式让可变参数模板从「能用」变成了「好用」。我个人觉得这是 C++17 最实用的特性之一,值得花时间掌握。
公众号:蓝海资料掘金营,微信 deep3321