C++17 折叠表达式(Fold Expressions)详解
1. 它是什么?为什么需要?
可变参数模板(template<class... Ts>
)在展开时,常见需求是把一串参数用某个二元运算符“折叠”为一个表达式(例如求和、逻辑与/或、连续输出、连续调用等)。
折叠表达式提供了内置语法把“参数包 + 运算符”展开成一个结合到左侧或右侧的链式表达式,写法简洁、顺序可控、还能支持短路语义。
C++17 之前常用
int dummy[] = { (f(args), 0)... };
来“遍历”参数;有了折叠表达式,就用((f(args)), ...)
或(std::cout << ... << args)
一句到位。
2. 语法与四种形态(最重要)
设 pack
表示参数包对应的表达式(如 args...
),op
为一个二元运算符,init
是一个初始值表达式。
形态 | 写法 | 展开(以 E1, E2, ..., En 表示包内各项) |
---|---|---|
一元左折(unary left fold) | (... op pack) | ((E1 op E2) op ... ) op En |
一元右折(unary right fold) | (pack op ...) | E1 op (E2 op ( ... op En)) |
二元左折(binary left fold) | (init op ... op pack) | (...((init op E1) op E2) op ... ) op En |
二元右折(binary right fold) | (pack op ... op init) | E1 op (E2 op ( ... op (En op init))) |
必备要点:
- 必须加括号。折叠表达式的语法要求外层
()
。 - 只对一个参数包折叠(但 pack 的每个元素内部可以是任意复杂表达式)。
- 左折/右折要与运算符语义配合(比如
<<
连续输出通常用“二元左折”:(os << ... << args)
)。
3. 空参数包时的规则
-
一元折叠:如果包为空,一般不合法,但有三例外,编译器会给出“单位元”:
(... && pack)
→true
(... || pack)
→false
(... , pack)
→void()
(逗号折叠结果为void()
)
-
二元折叠:如果包为空,结果就是
init
(因此常用二元折叠为“空集合”提供返回值)。
4. 支持哪些运算符?
几乎所有二元运算符:算术 + - * / %
,位运算 ^ & | << >>
,比较 == != < > <= >=
,逻辑 && ||
,逗号 ,
,成员指针 .* ->*
等都可用。
不建议用纯赋值
=
做折叠(左值/顺序等问题多);复合赋值也少见。
5. 典型用法与模板范式
5.1 累加 / 累乘 / 最值
template<class... Ts>
auto sum(Ts... xs) { return (xs + ...); } // 一元右折:(((x1 + x2) + x3) + ...)
template<class T, class... Ts>
auto sum_with(T init, Ts... xs) { return (init + ... + xs); } // 二元左折:((init + x1) + x2) + ...template<class... Ts>
auto product(Ts... xs) { return (xs * ...); }template<class T, class... Ts>
auto max_of(T init, Ts... xs) { return std::max({init, xs...}); } // 或用比较折叠:((init < x1 ? x1 : init) < x2 ? ...)
5.2 全部满足 / 任意满足(短路生效)
template<class... Preds>
bool all(Preds... ps) { return (ps && ...); } // 空包时为 true
template<class... Preds>
bool any(Preds... ps) { return (ps || ...); } // 空包时为 false
- 使用
&&
/||
时,短路语义和求值顺序与展开后的表达式一致(见 §7)。
5.3 连续输出(<<
)
template<class... Ts>
std::ostream& print_line(std::ostream& os, Ts&&... xs) {return (os << ... << xs) << '\n'; // 二元左折,形如 (((os<<x1)<<x2)<<...<<xn)
}
5.4 逐个调用函数(替代 initializer_list 技巧)
template<class F, class... Ts>
void for_each(F&& f, Ts&&... xs) {( (f(std::forward<Ts>(xs))), ... ); // 逗号的一元右折:f(x1), f(x2), ..., f(xn)
}
5.5 将每个元素映射后再折叠(map + reduce)
template<class... Ts>
size_t total_bytes(Ts const&... xs) {return (size_t{0} + ... + sizeof(xs)); // 先 sizeof(xs),再相加
}
5.6 连接字符串并自定义分隔符
template<class... Ts>
void join_print(std::ostream& os, std::string_view sep, Ts const&... xs) {bool first = true;( (first ? (void)(first=false) : (void)(os << sep), os << xs), ... );// 利用逗号运算符控制是否先打分隔符再输出元素
}
5.7 与完美转发、移动语义结合
template<class F, class... Ts>
decltype(auto) call_forward(F&& f, Ts&&... xs) {return (f(std::forward<Ts>(xs)), ...);
}
5.8 成员指针(进阶)
struct Pose { double x, y, z; };
template<class Obj, class... Members>
double sum_members(Obj const& o, Members... mptrs) {return (0.0 + ... + (o.*mptrs)); // 形如 0 + o.*x + o.*y + o.*z
}
6. 左折 vs 右折:选择的准则
-
IO 流/累加器模式:通常用左折(从初始状态一路“左结合”推进),如
(os << ... << xs)
、(init + ... + xs)
。 -
函数组合/右结合运算:更适合右折(尤其是有右结合语义的运算)。
-
非结合/非交换运算(如减法/除法/移位):左折与右折结果不同,要明确选择:
(xs - ...)
展开为(((x1 - x2) - x3) - ...)
(... - xs)
展开为(x1 - (x2 - (x3 - ...)))
7. 求值顺序、短路与副作用
-
折叠只是规定括号结构,求值顺序遵循对应运算符规则:
&&
/||
保持从左到右的短路求值;,
明确先左后右求值;- 其它运算符的求值顺序遵循语言规定(C++17 起,部分运算的顺序规则更严格,但仍应避免含糊副作用)。
-
若每个元素都有副作用(如输出/写日志),首选
,
或<<
的折叠,能清晰保证顺序。
8. 常见坑点与规约
- 忘记括号:
... + args
是错的,必须(args + ...)
或(... + args)
等合法形态。 - 空包:一元折叠仅对
&&
、||
、,
有单位元;否则空包会编译失败。需要空容错时用二元折叠并提供init
。 - 非结合运算:减法、除法、移位等请显式选择左/右折并写注释。
- 类型与单位元:不要指望“自动 0/1/~0”这类单位元(类型不一,语义不统一);该你给
init
的就给。 - 赋值折叠:
(x = ... = xs)
这类写法问题多(左值要求、顺序、易读性差),避免。 - 完美转发:涉及引用折叠时,务必
std::forward
,否则可能引入额外拷贝或移动。 - 异常与强保证:如果折叠的运算可能抛异常,注意左折/右折在部分求值时的对象状态一致性(例如输出流 vs. 容器操作)。
- 运算符优先级:折叠外层还有别的运算时,再加一层括号避免优先级陷阱。
9. 与 std::apply
、initializer_list
的关系
std::apply
:把tuple
展成参数后再交给可调用对象,适合“把一个元组当实参包”使用;而折叠表达式是“把一个表达式模式作用到实参包再规约”。两者常配合:std::apply([&](auto&&... xs){ return (xs + ...); }, tup);
initializer_list
技巧:C++17 前“遍历参数”的通用套路,但副作用靠逗号表达式隐式串起来,可读性差。折叠表达式就是更干净的官方替代。
10.简单示例
1. 累加 / 累乘
// 一元折叠
template<class... Ts>
auto sum(Ts... xs) { return (xs + ...); } // (((x1+x2)+x3)+...)// 带初值(避免空包)
template<class T, class... Ts>
auto sum_with(T init, Ts... xs) { return (init + ... + xs); }
2. 全部满足(all_of)
template<class... Ts>
bool all_of(Ts... xs) { return (xs && ...); }
// 空包时为 true
3. 任意满足(any_of)
template<class... Ts>
bool any_of(Ts... xs) { return (xs || ...); }
// 空包时为 false
4. 逐个调用(for-each)
template<class F, class... Ts>
void for_each(F&& f, Ts&&... xs) {((f(std::forward<Ts>(xs))), ...);
}
5. 连续输出
template<class... Ts>
std::ostream& print_line(std::ostream& os, Ts&&... xs) {return (os << ... << xs) << '\n';
}
6. 带分隔符输出
template<class... Ts>
void join_print(std::ostream& os, std::string_view sep, Ts&&... xs) {bool first = true;((first ? (first=false) : (void)(os << sep), os << xs), ...);
}
7. 成员指针求和
struct Pose { double x, y, z; };template<class Obj, class... Members>
double sum_members(const Obj& o, Members... mptrs) {return (0.0 + ... + (o.*mptrs));
}int main() {Pose p{1,2,3};std::cout << sum_members(p, &Pose::x, &Pose::y, &Pose::z); // 6
}
11.高级应用示例
示例 1:检查所有矩阵是否是方阵
#include <Eigen/Dense>
#include <iostream>template<typename... Mats>
bool all_square(const Mats&... mats) {return ((mats.rows() == mats.cols()) && ...);
}int main() {Eigen::Matrix3d A, B;Eigen::MatrixXd C(3, 4);std::cout << all_square(A, B) << "\n"; // truestd::cout << all_square(A, C) << "\n"; // false
}
折叠逻辑: (cond1 && cond2 && cond3 && ...)
示例 2:批量插入点云数据(避免 for 循环)
#include <vector>
#include <iostream>struct Point { double x, y, z; };template<typename... Pts>
void insert_points(std::vector<Point>& cloud, Pts&&... pts) {(cloud.push_back(std::forward<Pts>(pts)), ...); // 逗号折叠
}int main() {std::vector<Point> cloud;insert_points(cloud, {1,2,3}, {4,5,6}, {7,8,9});std::cout << cloud.size() << "\n"; // 3
}
折叠逻辑: push_back(p1), push_back(p2), push_back(p3), ...
示例 3:多线程 join 简化
#include <thread>
#include <vector>template<typename... Ts>
void join_all(Ts&... threads) {(threads.join(), ...);
}int main() {std::thread t1([]{ /* ... */ });std::thread t2([]{ /* ... */ });std::thread t3([]{ /* ... */ });join_all(t1, t2, t3); // 逐个 join
}
折叠逻辑: t1.join(), t2.join(), t3.join()
示例 4:记录所有参数的类型名
#include <iostream>
#include <typeinfo>template<typename... Ts>
void print_types(const Ts&... xs) {((std::cout << typeid(xs).name() << " "), ...);std::cout << "\n";
}int main() {print_types(1, 3.14, "SLAM");
}
折叠逻辑: cout<<typeid(x1).name(), cout<<typeid(x2).name(), ...
示例 5:逻辑混合(类似多传感器一致性检测)
#include <iostream>template<typename... Checks>
bool consensus(Checks... checks) {// 至少两个 true 才算通过int count = (0 + ... + (checks ? 1 : 0));return count >= 2;
}int main() {bool imu_ok = true, lidar_ok = false, cam_ok = true;std::cout << consensus(imu_ok, lidar_ok, cam_ok) << "\n"; // true
}
折叠逻辑: (0 + (c1?1:0) + (c2?1:0) + (c3?1:0))