ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

C++中std::for_each的使用

C++中std::for_each的使用

前言


std::for_each是<algorithm>里最简单的一个算法:对区间内的每个元素调用一次给定的函数对象,仅此而已。它没有返回值语义上的「结果」,也不改变容器大小,常被拿来和范围 for 循环比较。正因为简单,它的细节反而容易被忽略——参数按值还是按引用、返回值有什么用、并行版本有什么额外约束,都是实际写代码时会踩的点。


常见误解有三个。第一,以为for_each里的修改会自动作用到容器上。如果 lambda 的参数写成int x,拿到的是元素的副本,改它不会影响原容器。第二,以为可以像普通循环一样break。for_each没有中断机制,想提前结束只能用std::find_if之类的算法,或者退回手写循环。第三,以为 C++17 的并行重载只是「跑得更快」,忽略了它要求函数对象线程安全、执行顺序不确定、异常会直接终止程序。


本文讲清函数签名、四种典型用法、与范围 for 的取舍,以及并行重载的限制。示例以 C++17 为准。


一、签名与返回值


标准库中的声明大致是:


template<class InputIt, class UnaryFunction>
UnaryFunction for_each(InputIt first, InputIt last, UnaryFunction f);

它接受一对输入迭代器和一个一元函数对象,对[first, last)中的每个元素调用f(*it),最后返回函数对象本身。C++11 起返回值是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不支持,需换算法

下标需自己维护同样需自己维护

子区间不方便传任意迭代器区间即可

返回值无可返回带状态的函数对象

并行不支持C++17 起有并行重载

异常正常传播串行版正常传播,并行版终止



如果只是遍历整个容器、循环体里可能提前退出,范围 for 更合适。如果需要对一个子区间操作、需要把状态通过返回值带出、或者要切换到并行执行,for_each更合适。两者不是替代关系,而是同一件事的两种表达。


C++17 还提供了std::for_each_n,只处理区间的前 n 个元素,适合「只知道起点和数量」的场景。


四、并行重载


C++17 为一批算法增加了执行策略重载,for_each也在其中。用法是第一个参数传std::execution::par,并包含<execution>:


// 需要 C++17 及并行执行后端
#include <algorithm>
#include <execution>
#include <vector>

void scale(std::vector<double>& v) {
std::for_each(std::execution::par, v.begin(), v.end(),
[](double& x) { x *= 2.0; });
}

并行版本有三条额外约束。第一,函数对象会被多个线程同时调用,必须线程安全;访问共享变量要加锁或用原子操作,否则是数据竞争,属于未定义行为。第二,元素的处理顺序不确定,不能依赖「先处理前面的、再处理后面的」。第三,如果函数对象抛出异常,标准规定调用std::terminate,而不是把异常传播给调用者;需要在函数内部捕获并处理。


还有一点是工具链支持。<execution>和并行算法需要标准库实现和底层后端(例如 GCC 需要 TBB)配合,不同平台、不同版本的支持程度不一样,使用前应当查编译器支持表并确认链接配置。没有并行后端时,代码可能编译不过或退化为串行执行。


五、完整示例


// 适用于 C++17
#include <algorithm>
#include <cstddef>
#include <iostream>
#include <vector>

struct Sum {
long long total = 0;
void operator()(int x) { total += x; }
};

int main() {
std::vector<int> 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_cast<int>(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::iota(C++20)或手写索引循环。


坑点七:忽略返回值导致状态丢失。


❌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的定位是「对每个元素做一件事」,它不负责筛选、不负责中断、也不负责改变容器结构。把参数类型、捕获方式和返回值这三处写对,它就是一个比手写迭代器循环更清晰的工具;需要短路或并行时,再根据上表的边界换用其他算法。




RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进