8、Lambda 表达式:语法详解、捕获列表的多种形式、泛型 Lambda(C++14)、在 STL 算法中的应用。
Lambda 表达式,说白了就是 C++ 里的匿名函数对象。我第一次见到它时,心里想的是:“这不就是语法糖吗?” 后来在项目中重构了一段回调代码,才发现这玩意儿真能救命。它让你在需要函数对象的地方,直接“就地”写一个,不用再单独定义一个仿函数类。
嗯,咱们今天就把 Lambda 彻底聊透。从最基本的语法,到捕获列表的各种玩法,再到 C++14 引入的泛型 Lambda,最后看看它在 STL 算法里怎么大显身手。
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; });
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 的四个核心方面串在了一起。语法是骨架,捕获列表是灵魂,泛型 Lambda 是扩展,STL 应用是实战。掌握了这四点,Lambda 你就基本拿下了。
公众号:蓝海资料掘金营,微信deep3321