当前位置: 首页 > news >正文

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. 常见坑点与规约

  1. 忘记括号... + args 是错的,必须 (args + ...)(... + args) 等合法形态。
  2. 空包:一元折叠仅对 &&||, 有单位元;否则空包会编译失败。需要空容错时用二元折叠并提供 init
  3. 非结合运算:减法、除法、移位等请显式选择左/右折并写注释。
  4. 类型与单位元:不要指望“自动 0/1/~0”这类单位元(类型不一,语义不统一);该你给 init 的就给。
  5. 赋值折叠(x = ... = xs) 这类写法问题多(左值要求、顺序、易读性差),避免。
  6. 完美转发:涉及引用折叠时,务必 std::forward,否则可能引入额外拷贝或移动。
  7. 异常与强保证:如果折叠的运算可能抛异常,注意左折/右折在部分求值时的对象状态一致性(例如输出流 vs. 容器操作)。
  8. 运算符优先级:折叠外层还有别的运算时,再加一层括号避免优先级陷阱。

9. 与 std::applyinitializer_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))


http://www.dtcms.com/a/360588.html

相关文章:

  • ConcurrentHashMap在扩容的过程中又有新的数据写入是怎么处理的
  • 《Bishop PRML》10.1 (3) 理解VAE reconstruction loss
  • Redis 中的 Bitmap 与 Bitfield 及 Java 操作实践
  • python如何下载svg图片
  • 【Proteus仿真】数码管控制系列仿真——单个数码管控制/多数码管控制
  • leetcode 260 只出现一次的数字III
  • 你的数据是如何被保护的?
  • Linux系统的进程管理
  • vue3+vite+ts 发布npm 组件包
  • 查看所有装在c盘软件的方法
  • [知识点记录]SQLite 数据库和MySQL 数据库有什么区别?
  • DuckDB 内嵌分析:ABP 的「本地 OL盘快照」
  • 福彩双色球第2025100期号码推荐
  • 福彩双色球第2025100期数据统计
  • 吴恩达机器学习作业十一:异常检测
  • Docker 容器(二)
  • 机器视觉学习-day15-图像轮廓特征查找
  • Wi-Fi技术——OSI模型
  • 深度学习量化双雄:PTQ 与 QAT 的技术剖析与实战
  • 开源协作白板 – 轻量级多用户实时协作白板系统 – 支持多用户绘图、文字编辑、图片处理
  • globals() 小技巧
  • C++ 模板全览:从“非特化”到“全特化 / 偏特化”的完整原理与区别
  • Prometheus之启用--web.enable-remote-write-receiver
  • 基于muduo库的图床云共享存储项目(三)
  • 前端常见安全问题 + 防御方法 + 面试回答
  • 「数据获取」《中国工会统计年鉴》(1991-2013)(获取方式看绑定的资源)
  • 【人工智能99问】Qwen3简介(33/99)
  • 浅析NVMe协议:DIF
  • 多线程使用场景一(es数据批量导入)
  • 林曦词典|老死不相往来