8、Lambda 表达式:语法详解、捕获列表的多种形式、泛型 Lambda(C++14)、在 STL 算法中的应用。

Lambda 表达式,说白了就是 C++ 里的匿名函数对象。我第一次见到它时,心里想的是:“这不就是语法糖吗?” 后来在项目中重构了一段回调代码,才发现这玩意儿真能救命。它让你在需要函数对象的地方,直接“就地”写一个,不用再单独定义一个仿函数类。

嗯,咱们今天就把 Lambda 彻底聊透。从最基本的语法,到捕获列表的各种玩法,再到 C++14 引入的泛型 Lambda,最后看看它在 STL 算法里怎么大显身手。

核心要点: Lambda 的本质是一个未命名的函数对象(闭包类型)。编译器会为每个 Lambda 生成一个独一无二的类。

8.1 语法详解:从骨架到血肉

一个完整的 Lambda 表达式长这样:

[捕获列表] (参数列表) mutable(可选) noexcept(可选) -> 返回类型(可选) { 函数体 }

咱们拆开来看:

  • 捕获列表:决定 Lambda 能访问外部作用域的哪些变量,以及怎么访问(值/引用)。
  • 参数列表:和普通函数的参数一样,C++14 开始可以用 auto
  • mutable:默认情况下,Lambda 的 operator() 是 const 的。加上 mutable 后,就可以修改按值捕获的变量了。
  • noexcept:声明 Lambda 是否抛出异常。
  • 返回类型:可以省略,编译器会自动推导。如果函数体包含多个返回语句,可能需要显式指定。
  • 函数体:就是你要执行的代码。

最简单的例子:

auto hello = []() { std::cout << "Hello, Lambda!" << std::endl; };
hello(); // 调用

我个人习惯把 Lambda 当作一个“即用即弃”的小工具。如果逻辑超过 5 行,我建议还是提取成普通函数或命名函数对象,否则可读性会下降。

8.2 捕获列表的多种形式

捕获列表是 Lambda 最灵活、也最容易踩坑的地方。我见过不少同事因为捕获方式不对,导致悬空引用或意外的拷贝开销。

捕获形式 含义 示例
[] 不捕获任何外部变量 []() { return 42; }
[x] 按值捕获变量 x [x]() { return x + 1; }
[&x] 按引用捕获变量 x [&x]() { x += 1; }
[=] 按值捕获所有用到的外部变量 [=]() { return a + b; }
[&] 按引用捕获所有用到的外部变量 [&]() { a += b; }
[=, &x] 默认按值,x 按引用 [=, &x]() { x = y; }
[&, x] 默认按引用,x 按值 [&, x]() { a = x; }
[this] 按值捕获当前对象的指针(this) [this]() { memberFunc(); }
[*this] C++17:按值捕获当前对象的副本 [*this]() { /* 操作副本 */ }
避坑指南: 我曾经在异步回调里用了 [&] 捕获所有局部变量,结果 Lambda 执行时,那些局部变量早已销毁。程序崩溃得莫名其妙。从那以后,我坚持显式捕获每个变量,除非 Lambda 生命周期完全在作用域内。

你想想看,按引用捕获就像借了本书,书还了你还想读,那肯定出问题。按值捕获则是复印了一份,安全但费内存。

8.3 泛型 Lambda(C++14)

C++14 给 Lambda 加了一个大招:参数类型可以用 auto。说白了,就是让 Lambda 变成模板函数。

auto generic_lambda = [](auto a, auto b) {
    return a + b;
};

std::cout << generic_lambda(1, 2) << std::endl;       // 3
std::cout << generic_lambda(1.5, 2.5) << std::endl; // 4.0
std::cout << generic_lambda(std::string("Hello "), std::string("World")) << std::endl; // Hello World

编译器会为每个不同的参数类型组合生成一个实例化版本。这在实际项目中非常有用,尤其是配合 STL 算法时。

我记得有一次需要写一个通用的比较器,用于多种容器和数据类型。用泛型 Lambda,一行搞定:

auto cmp = [](const auto& lhs, const auto& rhs) { return lhs < rhs; };
std::set<int, decltype(cmp)> s1(cmp);
std::set<std::string, decltype(cmp)> s2(cmp);

嗯,这里要注意:泛型 Lambda 的 auto 参数和模板函数一样,会推导出引用和 const 限定。如果你需要完美转发,可以用 decltype 配合 std::forward,但那就有点复杂了,日常用 auto 就够了。

8.4 在 STL 算法中的应用

Lambda 和 STL 算法是天作之合。以前写算法,你得定义一个仿函数类,或者写一堆全局函数。现在,直接在调用处写 Lambda,代码更紧凑、意图更清晰。

咱们看几个常见场景:

8.4.1 std::sort 自定义排序

std::vector<int> v = {3, 1, 4, 1, 5, 9, 2, 6};
std::sort(v.begin(), v.end(), [](int a, int b) {
    return a > b; // 降序
});

8.4.2 std::find_if 查找满足条件的元素

auto it = std::find_if(v.begin(), v.end(), [](int x) {
    return x > 5;
});

8.4.3 std::for_each 遍历并操作

std::for_each(v.begin(), v.end(), [](int& x) {
    x *= 2;
});

8.4.4 std::transform 转换容器

std::vector<int> result;
std::transform(v.begin(), v.end(), std::back_inserter(result),
               [](int x) { return x * x; });

8.4.5 std::accumulate 自定义累加

int sum = std::accumulate(v.begin(), v.end(), 0,
                          [](int acc, int x) { return acc + x; });
个人经验: 我在项目中经常用 Lambda 配合 std::remove_if 做条件删除。比如删除 vector 中所有偶数:v.erase(std::remove_if(v.begin(), v.end(), [](int x){ return x % 2 == 0; }), v.end());。这比手写循环简洁多了。

你想想看,如果没有 Lambda,每个算法调用你都得定义一个函数或仿函数,代码会变得支离破碎。Lambda 把逻辑内聚在调用点,阅读代码时不用跳来跳去。

8.5 知识体系图

下面这张图总结了 Lambda 表达式的核心知识结构,方便你快速回顾:

Lambda 表达式 语法结构 捕获列表 [ ] 参数列表 ( ) mutable / noexcept 返回类型 -> 类型 函数体 { } 捕获列表形式 [] 不捕获 [x] 按值捕获 [&x] 按引用捕获 [=] / [&] 默认捕获 [this] / [*this] 泛型 Lambda 参数使用 auto 编译器生成模板 支持多种类型 C++14 引入 STL 算法中的应用 sort / find_if / for_each transform / accumulate remove_if / count_if / ...

这张图把 Lambda 的四个核心方面串在了一起。语法是骨架,捕获列表是灵魂,泛型 Lambda 是扩展,STL 应用是实战。掌握了这四点,Lambda 你就基本拿下了。

总结一下: Lambda 表达式是现代 C++ 中不可或缺的工具。它让代码更简洁、更内聚、更易维护。但也要注意捕获列表的生命周期问题,以及不要滥用导致代码可读性下降。我个人建议:短小精悍的逻辑用 Lambda,复杂逻辑还是提取成命名函数。

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