C++中std::for_each的使用
前言std::for_each是algorithm里最简单的一个算法对区间内的每个元素调用一次给定的函数对象仅此而已。它没有返回值语义上的「结果」也不改变容器大小常被拿来和范围 for 循环比较。正因为简单它的细节反而容易被忽略——参数按值还是按引用、返回值有什么用、并行版本有什么额外约束都是实际写代码时会踩的点。常见误解有三个。第一以为for_each里的修改会自动作用到容器上。如果 lambda 的参数写成int x拿到的是元素的副本改它不会影响原容器。第二以为可以像普通循环一样break。for_each没有中断机制想提前结束只能用std::find_if之类的算法或者退回手写循环。第三以为 C17 的并行重载只是「跑得更快」忽略了它要求函数对象线程安全、执行顺序不确定、异常会直接终止程序。本文讲清函数签名、四种典型用法、与范围 for 的取舍以及并行重载的限制。示例以 C17 为准。一、签名与返回值标准库中的声明大致是templateclass InputIt, class UnaryFunctionUnaryFunction for_each(InputIt first, InputIt last, UnaryFunction f);它接受一对输入迭代器和一个一元函数对象对[first, last)中的每个元素调用f(*it)最后返回函数对象本身。C11 起返回值是std::move(f)也就是把内部使用的那个副本移出来。这个返回值不是摆设如果函数对象带有状态比如累加和、计数可以通过接收返回值把最终状态取回来。参数按值传递意味着调用for_each时会拷贝一份函数对象算法内部修改的是这份副本。传入 lambda 时lambda 的捕获列表也随之拷贝。如果 lambda 按值捕获了外部变量内部修改的只是副本外部变量不变要影响外部必须按引用捕获。返回类型与传入的函数对象类型相同。传入的是 lambda 时返回的也是那个 lambda 类型通常用auto接收。传入的是自定义结构体时可以直接赋给同类型的变量。二、四种典型用法只读访问。参数写成int x或const int x都可以。前者拷贝后者避免拷贝对int这种小类型差别可以忽略对大对象应当用 const 引用。原地修改。参数必须写成int x或auto x。写成值类型时修改的是副本容器纹丝不动。这是最常见的错误而且编译器不会报错只会在结果不符合预期时才发现。通过引用捕获累加。如果想把结果写到外部变量lambda 的捕获列表要写[sum]参数按值接收即可。注意for_each是顺序执行的非并行重载所以这种累加是安全的如果换成并行重载多个线程同时写sum就是数据竞争。利用返回值取回状态。如果不想用引用捕获可以传一个带成员变量的函数对象算法返回它的副本再从返回值里读结果。这种写法把状态封装在函数对象内部避免了外部变量的生命周期问题也更适合复用。还有一个常见需求是带下标处理。for_each本身不提供下标可以在外部维护一个计数器并按引用捕获在 lambda 里自增。这只在顺序执行时成立并行版本里计数器的自增顺序不确定不能用来生成下标。三、与范围 for 的取舍范围 for 循环range-based for和for_each都能遍历区间选择时看几个因素维度范围 forstd::for_each可读性更接近自然语言需要写起止迭代器提前退出支持break、continue不支持需换算法下标需自己维护同样需自己维护子区间不方便传任意迭代器区间即可返回值无可返回带状态的函数对象并行不支持C17 起有并行重载异常正常传播串行版正常传播并行版终止如果只是遍历整个容器、循环体里可能提前退出范围 for 更合适。如果需要对一个子区间操作、需要把状态通过返回值带出、或者要切换到并行执行for_each更合适。两者不是替代关系而是同一件事的两种表达。C17 还提供了std::for_each_n只处理区间的前 n 个元素适合「只知道起点和数量」的场景。四、并行重载C17 为一批算法增加了执行策略重载for_each也在其中。用法是第一个参数传std::execution::par并包含execution// 需要 C17 及并行执行后端#include algorithm#include execution#include vectorvoid scale(std::vectordouble v) {std::for_each(std::execution::par, v.begin(), v.end(),[](double x) { x * 2.0; });}并行版本有三条额外约束。第一函数对象会被多个线程同时调用必须线程安全访问共享变量要加锁或用原子操作否则是数据竞争属于未定义行为。第二元素的处理顺序不确定不能依赖「先处理前面的、再处理后面的」。第三如果函数对象抛出异常标准规定调用std::terminate而不是把异常传播给调用者需要在函数内部捕获并处理。还有一点是工具链支持。execution和并行算法需要标准库实现和底层后端例如 GCC 需要 TBB配合不同平台、不同版本的支持程度不一样使用前应当查编译器支持表并确认链接配置。没有并行后端时代码可能编译不过或退化为串行执行。五、完整示例// 适用于 C17#include algorithm#include cstddef#include iostream#include vectorstruct Sum {long long total 0;void operator()(int x) { total x; }};int main() {std::vectorint v {1, 2, 3, 4, 5};// 1. 只读访问std::for_each(v.begin(), v.end(), [](int x) {std::cout x ;});std::cout \n;// 2. 原地修改参数必须是非 const 引用std::for_each(v.begin(), v.end(), [](int x) { x * 2; });// 3. 引用捕获累加到外部变量long long sum 0;std::for_each(v.begin(), v.end(), [sum](int x) { sum x; });std::cout sum sum \n;// 4. 通过返回值取回函数对象的状态Sum s std::for_each(v.begin(), v.end(), Sum{});std::cout total s.total \n;// 5. 借外部计数器生成下标std::size_t index 0;std::for_each(v.begin(), v.end(), [index](int x) {x static_castint(index);index;});for (int x : v) std::cout x ;std::cout \n;return 0;}按语义推演。初始序列是 1 到 5第一步原样输出。第二步每个元素乘 2序列变成 2、4、6、8、10。第三步求和sum为 30。第四步再用Sum累加一遍total也是 30。第五步按下标累加2 加 0、4 加 1、6 加 2、8 加 3、10 加 4最终序列是 2、5、8、11、14。第 4 步的Sum{}是临时对象for_each按值接收后内部持有一份副本遍历结束返回这份副本赋给s。如果写成Sum s; std::for_each(..., s);算法修改的是s的副本s.total仍然是 0必须用返回值重新赋值。常见坑点坑点一lambda 参数按值接收却想修改元素。❌std::for_each(v.begin(), v.end(), [](int x) { x * 2; });容器不变。✅std::for_each(v.begin(), v.end(), [](int x) { x * 2; });。坑点二按值捕获外部变量并修改。❌int sum 0; std::for_each(..., [sum](int x) mutable { sum x; });外部sum仍是 0。✅ 按引用捕获[sum]或使用返回值接收状态。坑点三在for_each中增删容器元素。❌ lambda 里调用v.push_back(...)迭代器失效未定义行为。✅ 先记录要处理的结果循环结束后统一修改容器。坑点四想在for_each中提前退出。❌ 在 lambda 里写break编译失败用抛异常退出又会打乱控制流。✅ 改用std::find_if、std::any_of等带短路语义的算法或手写循环。坑点五并行版本里共享未同步的状态。❌std::for_each(std::execution::par, ..., [](int x) { sum x; });数据竞争。✅ 用std::atomic、互斥量或让每个线程维护局部结果再合并。坑点六并行版本里依赖处理顺序。❌ 用外部计数器生成下标并行执行时下标与元素不对应。✅ 并行版本只做与顺序无关的计算需要下标时用std::views::iotaC20或手写索引循环。坑点七忽略返回值导致状态丢失。❌Sum s; std::for_each(v.begin(), v.end(), s);之后读s.total。✅Sum s std::for_each(v.begin(), v.end(), Sum{});或s std::for_each(..., s);。坑点八并行版本抛异常期待被捕获。❌ 在 lambda 里抛出异常指望调用方try/catch接住。✅ 并行算法中未捕获的异常会调用std::terminate应在 lambda 内部处理。总结需求写法注意点只读遍历[](const auto x)大对象避免拷贝原地修改[](auto x)参数必须是引用累加结果[sum](auto x)或返回函数对象并行时不能共享写提前退出不用for_each改用find_if或手写循环子区间传对应迭代器不要求从头开始并行执行std::execution::par线程安全、无序、异常终止std::for_each的定位是「对每个元素做一件事」它不负责筛选、不负责中断、也不负责改变容器结构。把参数类型、捕获方式和返回值这三处写对它就是一个比手写迭代器循环更清晰的工具需要短路或并行时再根据上表的边界换用其他算法。