12. Lambda 与 STL 算法:std::for_each, std::transform, std::remove_if 实战
说实话,C++ 里最让我觉得「爽」的组合,就是 Lambda 加上 STL 算法。你想想看,以前写个循环要单独写个函数对象,或者写个仿函数类,代码又长又散。现在一个 Lambda 直接怼进去,逻辑就在眼前,多清爽。
这一章,咱们就拿三个最常用的算法开刀:std::for_each、std::transform 和 std::remove_if。我会结合我实际项目里的踩坑经历,把它们的用法和注意事项讲透。
12.1 std::for_each:遍历与副作用
std::for_each 是最简单的算法。它遍历一个区间,对每个元素执行一个操作。这个操作可以是打印、修改、收集数据——只要你有副作用(side effect),它就很合适。
基本用法长这样:
#include <iostream>
#include <vector>
#include <algorithm>
int main() {
std::vector<int> nums = {1, 2, 3, 4, 5};
// 用 Lambda 打印每个元素
std::for_each(nums.begin(), nums.end(),
[](int n) {
std::cout << n << " ";
}
);
// 输出: 1 2 3 4 5
return 0;
}
嗯,这里要注意:for_each 的 Lambda 参数是按值传递的。如果你想修改容器里的元素,必须传引用:
// 每个元素加 10
std::for_each(nums.begin(), nums.end(),
[](int& n) {
n += 10;
}
);
const auto& 避免拷贝;如果要修改,用 auto&。别偷懒写 auto,大对象拷贝起来很疼。
我在项目中遇到过一个问题:用 for_each 收集符合条件的元素。当时我写了个 Lambda,捕获一个 vector 的引用往里 push。结果因为 Lambda 默认是 const 的,编译报错。解决方案是加 mutable,或者更优雅地——捕获指针或引用:
std::vector<int> results;
std::for_each(nums.begin(), nums.end(),
[&results](int n) {
if (n % 2 == 0)
results.push_back(n);
}
);
// results 现在包含所有偶数
你看,捕获引用就搞定了,不需要 mutable。
12.2 std::transform:映射与转换
std::transform 是我个人用得最多的算法之一。它把一个区间的元素映射到另一个区间,或者同一个区间。说白了,就是「对每个元素做变换,输出到目标位置」。
最常见的是一对一映射:
#include <iostream>
#include <vector>
#include <algorithm>
int main() {
std::vector<int> src = {1, 2, 3, 4, 5};
std::vector<int> dst(src.size());
// 每个元素平方
std::transform(src.begin(), src.end(), dst.begin(),
[](int x) { return x * x; }
);
// dst: 1 4 9 16 25
return 0;
}
这里有个坑:dst 必须有足够的空间。如果 dst 是空的,用 dst.begin() 会崩溃。我建议用 std::back_inserter:
std::vector<int> dst;
std::transform(src.begin(), src.end(), std::back_inserter(dst),
[](int x) { return x * x; }
);
这样 dst 会自动增长,安全又省心。
还有一种双输入版本的 transform,把两个区间的对应元素组合:
std::vector<int> a = {1, 2, 3};
std::vector<int> b = {10, 20, 30};
std::vector<int> c;
std::transform(a.begin(), a.end(), b.begin(), std::back_inserter(c),
[](int x, int y) { return x + y; }
);
// c: 11 22 33
b 比 a 短,行为是未定义的。我建议先检查 b.size() >= a.size(),或者用 std::min 限制遍历长度。
12.3 std::remove_if:移除与 erase-remove 惯用法
std::remove_if 这个名字其实有点误导。它并不真的删除元素,而是把符合条件的元素移到区间末尾,返回一个指向新逻辑结尾的迭代器。要真正删除,必须配合容器的 erase 方法。这就是经典的 erase-remove 惯用法。
看个例子:
#include <iostream>
#include <vector>
#include <algorithm>
int main() {
std::vector<int> nums = {1, 2, 3, 4, 5, 6, 7, 8};
// 移除所有偶数
auto new_end = std::remove_if(nums.begin(), nums.end(),
[](int n) { return n % 2 == 0; }
);
// 真正删除
nums.erase(new_end, nums.end());
// nums: 1 3 5 7
return 0;
}
为什么不能直接 erase 在循环里?因为 erase 会使迭代器失效,容易写出 bug。而 remove_if 加 erase 是 O(n) 的,且安全。
std::remove_if 不改变容器大小,只移动元素。erase 才真正释放内存。两者必须配合使用。
我遇到过一个问题:用 remove_if 移除满足多个条件的元素。当时我写了一个复杂的 Lambda,里面嵌套了好几个 if。后来我改成用 std::logical_or 组合多个谓词,代码清晰多了:
auto is_even = [](int n) { return n % 2 == 0; };
auto is_gt_10 = [](int n) { return n > 10; };
auto new_end = std::remove_if(nums.begin(), nums.end(),
[&](int n) { return is_even(n) || is_gt_10(n); }
);
nums.erase(new_end, nums.end());
你看,把条件拆成小 Lambda,再用一个 Lambda 组合它们,可读性瞬间提升。
12.4 三者对比与选择
这三个算法各有侧重。我整理了一个表格,方便你快速决策:
| 算法 | 用途 | 返回值 | 典型场景 |
|---|---|---|---|
std::for_each |
遍历并执行操作(副作用) | 函数对象(可忽略) | 打印、收集、修改元素 |
std::transform |
映射转换,生成新序列 | 指向输出末尾的迭代器 | 类型转换、数学变换、数据清洗 |
std::remove_if |
按条件移除元素 | 新逻辑结尾的迭代器 | 过滤、去重、清理无效数据 |
你想想看,如果只是打印,用 for_each;如果要生成新数据,用 transform;如果要删除,用 remove_if + erase。别混用,也别硬套。
12.5 知识体系图
下面这张图展示了本章的核心逻辑:Lambda 作为「胶水」,把算法和业务逻辑粘在一起。
12.6 实战建议
最后,给你几个我总结的实战建议:
- 优先用 Lambda,别写仿函数类——除非你需要复用,或者 Lambda 太长(超过 5 行)。
- 注意捕获方式——按值捕获小心拷贝开销,按引用捕获小心悬空引用。
- 用
auto参数——C++14 开始支持泛型 Lambda,写[](const auto& x) { ... }更灵活。 - 组合小 Lambda——别写一个巨大的 Lambda,拆成多个小函数,再用
std::bind或 Lambda 组合。
std::cout 打印中间结果。等逻辑对了再删掉。别怕代码丑,调试阶段效率优先。
好了,这一章就到这里。记住这三个算法的核心区别,下次写循环时,先想想能不能用 STL 算法加 Lambda 替代。代码会更简洁,也更不容易出错。