12. Lambda 与 STL 算法:std::for_each, std::transform, std::remove_if 实战

说实话,C++ 里最让我觉得「爽」的组合,就是 Lambda 加上 STL 算法。你想想看,以前写个循环要单独写个函数对象,或者写个仿函数类,代码又长又散。现在一个 Lambda 直接怼进去,逻辑就在眼前,多清爽。

这一章,咱们就拿三个最常用的算法开刀:std::for_eachstd::transformstd::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
我曾经踩过的坑: 双输入版本要求第二个区间至少和第一个一样长。如果 ba 短,行为是未定义的。我建议先检查 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_iferase 是 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 作为「胶水」,把算法和业务逻辑粘在一起。

Lambda 表达式 std::for_each std::transform std::remove_if 遍历 + 副作用 打印/收集/修改 映射 + 转换 生成新序列 移除 + 过滤 erase-remove 惯用法 核心思想:Lambda 作为「胶水」,连接算法与业务逻辑

12.6 实战建议

最后,给你几个我总结的实战建议:

  • 优先用 Lambda,别写仿函数类——除非你需要复用,或者 Lambda 太长(超过 5 行)。
  • 注意捕获方式——按值捕获小心拷贝开销,按引用捕获小心悬空引用。
  • auto 参数——C++14 开始支持泛型 Lambda,写 [](const auto& x) { ... } 更灵活。
  • 组合小 Lambda——别写一个巨大的 Lambda,拆成多个小函数,再用 std::bind 或 Lambda 组合。
我的一个小技巧: 调试 Lambda 时,可以在里面加 std::cout 打印中间结果。等逻辑对了再删掉。别怕代码丑,调试阶段效率优先。

好了,这一章就到这里。记住这三个算法的核心区别,下次写循环时,先想想能不能用 STL 算法加 Lambda 替代。代码会更简洁,也更不容易出错。


公众号:蓝海资料掘金营,微信deep3321