一、为什么需要 Lambda 表达式?
在 C++ 编程的早期阶段,我们处理"行为"的主要方式只有几种:
- 普通函数
- 函数指针
- 仿函数(Functor / 函数对象)
这些方式各有优缺点,但它们都有一个共同的问题:
当某段逻辑只在局部使用时,代码会变得臃肿、不直观、难以维护。
举一个非常典型的例子:排序。
cpp
#include <algorithm>
#include <vector>
bool cmp(int a, int b)
{
return a > b;
}
int main()
{
std::vector<int> v = {1, 3, 5, 2, 4};
std::sort(v.begin(), v.end(), cmp);
}
问题来了:
cmp这个函数只为 sort 服务- 它却被"提升"为了一个全局或静态函数
- 代码的局部性极差
为了解决这类"只用一次的行为 "问题,C++11 正式引入了 ------ Lambda 表达式。
一句话总结:
Lambda 表达式 = 可以在代码中"就地定义"的匿名函数
二、Lambda 表达式是什么?
2.1 最直观的理解
Lambda 表达式本质上就是一个匿名函数对象。
cpp
auto f = []() {
std::cout << "hello lambda" << std::endl;
};
f();
你可以把它理解成:
- 没有名字的函数
- 可以像变量一样传递
- 可以捕获当前作用域中的变量
2.2 Lambda 的基本语法
C++ 中 Lambda 的完整语法形式如下:
cpp
[capture](params) mutable -> return_type {
function_body;
}
我们逐个拆解:
| 部分 | 含义 |
|---|---|
[] |
捕获列表(Capture List) |
() |
参数列表 |
mutable |
是否允许修改捕获变量 |
-> return_type |
返回值类型(可省略) |
{} |
函数体 |
最简形式:
cpp
[]{};
三、Lambda 的最基本用法
3.1 无参数、无返回值
cpp
auto print = []() {
std::cout << "Hello Lambda" << std::endl;
};
print();
print()的类型是编译器自动生成的、独一无二的匿名闭包类型(closuretype),没有可直接书写的标准名称,无法用普通类型名显式声明。
3.2 带参数
cpp
auto add = [](int a, int b) {
return a + b;
};
std::cout << add(3, 4) << std::endl; // 7
3.3 自动推导返回值
从 C++11 开始,只要 return 唯一,编译器就可以推导返回类型。
cpp
auto max = [](int a, int b) {
return a > b ? a : b;
};
四、Lambda 捕获列表(核心重点)
Lambda 的灵魂在于捕获(capture)
4.1 为什么要捕获?
普通函数:
- 只能访问参数
- 不能直接访问外部局部变量
Lambda:
- 可以"抓住"当前作用域中的变量
cpp
int x = 10;
auto f = []() {
// std::cout << x; // ❌ 编译错误
};
必须通过捕获:
cpp
int x = 10;
auto f = [x]() {
std::cout << x << std::endl;
};
4.2 值捕获(Capture by Value)
cpp
int x = 10;
auto f = [x]() {
std::cout << x << std::endl;
};
x = 20;
f(); // 10
特点:
- 捕获的是 一份拷贝
- Lambda 内部看不到外部后续修改
- 默认是 const 的
4.3 引用捕获(Capture by Reference)
cpp
int x = 10;
auto f = [&x]() {
x = 100;
};
f();
std::cout << x << std::endl; // 100
特点:
- 捕获的是引用
- 可以修改外部变量
- 生命周期必须保证安全
4.4 混合捕获
cpp
int a = 1, b = 2;
auto f = [a, &b]() {
// a 是值捕获,b 是引用捕获
};
4.5 默认捕获方式
cpp
[a, b] // 显式值捕获
[&a, &b] // 显式引用捕获
[=] // 所有变量按值捕获
[&] // 所有变量按引用捕获
⚠️ 强烈建议:
能显式写就别用默认捕获(可读性 + 安全性)
五、mutable 关键字
默认情况下:
值捕获的变量在 Lambda 内部是只读的
cpp
int x = 10;
auto f = [x]() {
// x++; // ❌ 错误
};
使用 mutable表示可以修改捕获变量:
cpp
int x = 10;
auto f = [x]() mutable {
x++;
std::cout << x << std::endl;
};
f(); // 11
std::cout << x << std::endl; // 10
本质:
- 修改的是 Lambda 内部的拷贝
- 外部变量不受影响
六、返回值类型与尾置返回
6.1 返回类型推导失败的情况
cpp
auto f = [](int x) {
if (x > 0)
return x;
else
return -x;
};
这种可以推导。
但下面这种不行:
cpp
auto f = [](bool flag) {
if (flag)
return 1;
else
return 1.5; // ❌
};
必须显式指定返回类型:
cpp
auto f = [](bool flag) -> double {
return flag ? 1 : 1.5;
};
七、Lambda 的类型本质
7.1 Lambda 是一个"匿名类"
cpp
auto f = [](int a, int b) {
return a + b;
};
编译器大致生成:
cpp
class __lambda_1 {
public:
int operator()(int a, int b) const {
return a + b;
}
};
这解释了:
- 为什么 Lambda 可以像函数一样调用
- 为什么它可以捕获变量(成员变量)
八、Lambda 与 std::function
8.1 std::function 基本使用
cpp
#include <functional>
std::function<int(int, int)> f = [](int a, int b) {
return a + b;
};
8.2 对比模板 vs std::function
| 方案 | 优点 | 缺点 |
|---|---|---|
| 模板 | 零开销 | 接口复杂 |
| std::function | 统一接口 | 有性能损耗 |
九、Lambda 在 STL 中的经典应用
9.1 sort
cpp
std::sort(v.begin(), v.end(), [](int a, int b) {
return a > b;
});
9.2 find_if
cpp
auto it = std::find_if(v.begin(), v.end(), [](int x) {
return x % 2 == 0;
});
9.3 for_each
cpp
std::for_each(v.begin(), v.end(), [](int x) {
std::cout << x << " ";
});
十、Lambda 与多线程
cpp
#include <thread>
int main()
{
int x = 10;
std::thread t([&x]() {
x++;
});
t.join();
}
⚠️ 注意:
- 捕获引用时要注意生命周期
- 多线程下要考虑数据竞争
十一、C++14 / C++17 / C++20 中的 Lambda 演进
11.1 C++14:泛型 Lambda
cpp
auto add = [](auto a, auto b) {
return a + b;
};
11.2 C++17:constexpr Lambda
cpp
constexpr auto square = [](int x) {
return x * x;
};
11.3 C++20:模板 Lambda
cpp
auto f = []<typename T>(T a, T b) {
return a + b;
};
十二、Lambda 使用中的常见陷阱
- 悬空引用捕获
- 默认捕获导致的误用
- 过度使用 std::function
- 捕获 this 的生命周期问题
十三、工程实践建议
- 短小逻辑:Lambda
- 复杂逻辑:具名函数 / 类
- 性能敏感:模板 + Lambda
- 接口统一:std::function
十四、总结
Lambda 表达式的引入,极大地提升了 C++ 的:
- 表达力
- 局部性
- 抽象能力
它不是"语法糖",而是现代 C++ 的核心工具之一。
写好 Lambda,是迈入现代 C++ 的重要一步。
完