CPP学习笔记—lambda表达式
Lambda 表达式是 C++11 引入的一项革命性特性,它极大地改变了 C++ 的编程风格,尤其是在与标准模板库(STL)配合使用时。它允许我们在代码中需要一个函数的地方,直接定义一个匿名的、临时的函数对象。
1. 什么是 Lambda 表达式?
简单来说,Lambda 表达式就是一个可调用的代码单元,可以把它理解为一个匿名的内联函数。与普通函数不同,它可以在函数内部定义,并且可以“捕获”其所在作用域中的变量。
2. 为什么需要 Lambda 表达式?
在 C++11 之前,如果想向一个算法(如 std::sort
)传递一个简单的、一次性的比较逻辑,通常需要:
- 定义一个全局函数或静态成员函数:这会污染命名空间,并且逻辑与使用它的地方相隔甚远。
- 定义一个函数对象(Functor):需要编写一个完整的类,重载
operator()
。这非常繁琐。
Lambda 的优势:
- 代码局部性:将逻辑直接写在使用它的地方,代码更紧凑,可读性更高。
- 简洁性:避免了编写独立的函数或函数对象类的样板代码。
- 状态捕获:可以方便地访问和使用其定义作用域内的变量,这是普通函数难以做到的。
3. Lambda 表达式的完整语法
一个完整的 Lambda 表达式的语法结构如下:
1 | [capture_list] (parameters) mutable noexcept -> return_type { |
3.1. 捕获列表 []
(Capture Clause)
这是 Lambda 最重要和最强大的部分。它决定了 Lambda 函数体内部可以访问哪些外部变量,以及如何访问它们。
[]
:不捕获任何外部变量。1
[]() { std::cout << "Hello from Lambda!" << std::endl; }
[=]
:按值捕获所有外部变量。在 Lambda 内部,得到的是外部变量的一份拷贝。修改它们不会影响外部的原始变量。1
2
3
4
5
6int x = 10;
auto myLambda = [=]() {
// x 在这里是 10 的一份拷贝
std::cout << x << std::endl; // 合法
// x = 20; // 编译错误!因为拷贝是 const 的
};[&]
:按引用捕获所有外部变量。在 Lambda 内部,得到的是外部变量的引用。修改它们会直接影响外部的原始变量。1
2
3
4
5
6int x = 10;
auto myLambda = [&]() {
x = 20; // 合法,外部的 x 会被修改为 20
};
myLambda();
std::cout << x; // 输出 20警告:按引用捕获要非常小心悬挂引用的问题!如果 Lambda 的生命周期超过了它所引用的局部变量,调用这个 Lambda 将导致未定义行为。
[this]
:捕获当前对象的this
指针。这允许你在 Lambda 内部访问类的成员变量和成员函数。1
2
3
4
5
6
7
8
9
10
11
12
13class MyClass {
public:
void doWork() {
int factor = 2;
auto task = [this, factor](int value) {
// this->member_var_ 是通过 this 指针访问的
return value * factor * this->member_var_;
};
int result = task(5);
}
private:
int member_var_ = 10;
};指定捕获:可以精确控制捕获哪些变量以及如何捕获。
[x, &y]
:变量x
按值捕获,变量y
按引用捕获。[=, &y]
:除了y
按引用捕获,其他所有变量都按值捕获。[&, x]
:除了x
按值捕获,其他所有变量都按引用捕获。
3.2. 参数列表 ()
(Parameter List)
和普通函数的参数列表一样,用于接收传递给 Lambda 的参数。如果不需要参数,()
可以省略。
1 | auto add = [](int a, int b) { |
3.3. mutable
关键字
默认情况下,对于按值捕获的变量,Lambda 内部不能修改它们(它们是 const
的)。如果你希望在 Lambda 内部可以修改这些拷贝,就需要使用 mutable
关键字。
1 | int x = 10; |
输出:
1 | Inside lambda, x = 20 |
3.4. 异常说明 noexcept
和普通函数一样,你可以用 noexcept
来指明该 Lambda 不会抛出异常,这有助于编译器进行优化。
1 | auto f = []() noexcept { /* ... */ }; |
3.5. 返回类型 -> return_type
(Trailing Return Type)
- 自动推导:在大多数情况下,编译器可以根据
return
语句自动推导出 Lambda 的返回类型,所以-> return_type
是可选的。1
auto add = [](int a, int b) { return a + b; }; // 编译器推导出返回类型为 int
- 显式指定:如果 Lambda 函数体中有多个返回语句,且它们的类型不同,或者你希望强制返回一个特定类型(例如,
return 0;
本应推导为int
,但你希望返回long
),则必须显式指定返回类型。1
2
3
4
5
6
7auto f = [](double d) -> int {
if (d > 0) {
return static_cast<int>(d); // 返回 int
} else {
return 0; // 返回 int
}
};
3.6. 函数体 {}
(Function Body)
Lambda 的执行逻辑,和普通函数的函数体一样。
4. 高级用法
4.1. 泛型 Lambda (Generic Lambdas, C++14)
通过在参数列表中使用 auto
关键字,可以创建泛型 Lambda。这实际上是创建了一个函数模板。
1 | auto add = [](auto a, auto b) { |
4.2. 初始化捕获 / 广义捕获 (Init Capture, C++14)
这极大地增强了捕获列表的功能,允许你在捕获时进行初始化。语法是 [identifier = expression]
。
用途:
创建仅在 Lambda 内部可见的新变量:
1
2
3
4
5
6int x = 10;
auto myLambda = [y = x + 5]() {
// y 是在捕获列表中创建的,值为 15
// x 在这里是不可见的
std::cout << y << std::endl; // 输出 15
};移动捕获(Move Capture):对于只能移动不能拷贝的类型(如
std::unique_ptr
),这是捕获它们的唯一方式。1
2
3
4
5
6auto ptr = std::make_unique<int>(10);
auto myLambda = [p = std::move(ptr)]() {
std::cout << "Value from unique_ptr: " << *p << std::endl;
};
myLambda();
// 此时 ptr 已经是 nullptr 了
4.3. constexpr
Lambda (C++17)
如果一个 Lambda 满足 constexpr
函数的所有要求,你可以在其前面加上 constexpr
关键字,使其可以在编译时求值。
1 | constexpr auto add = [](int a, int b) { return a + b; }; |
5. 实际应用场景
5.1. 与 STL 算法结合
这是 Lambda 最常见的用途。
std::sort
1
2
3
4std::vector<int> v = {5, 1, 4, 2, 3};
std::sort(v.begin(), v.end(), [](int a, int b) {
return a > b; // 实现降序排序
});std::for_each
1
2
3
4
5std::vector<int> v = {1, 2, 3};
int sum = 0;
std::for_each(v.begin(), v.end(), [&sum](int n) {
sum += n;
}); // sum 最终为 6std::find_if
1
2
3
4
5
6
7std::vector<std::string> names = {"Alice", "Bob", "Charlie"};
auto it = std::find_if(names.begin(), names.end(), [](const std::string& name) {
return name.length() > 4;
});
if (it != names.end()) {
std::cout << "Found: " << *it << std::endl; // 输出 "Found: Alice"
}
5.2. 作为 std::function
的实现
std::function
是一个通用的可调用对象包装器。Lambda 可以被用来初始化 std::function
对象。
1 | std::function<int(int, int)> operation; |
5.3. 多线程编程
在创建线程时,Lambda 提供了一种非常方便的方式来定义线程要执行的任务。
1 |
|
6. 底层原理 (Lambda 是如何工作的)
在编译时,编译器会将每个 Lambda 表达式转换成一个唯一的、匿名的类类型,这个类被称为闭包类型(Closure Type)。
一个 Lambda 表达式 [...] (...) {...}
基本上等同于:
1 | class __UniqueLambdaName { |
当你定义一个 Lambda 时,编译器会创建这个类的一个实例,这个实例就是闭包对象(Closure Object)。这就是为什么 Lambda 可以存储状态(通过捕获的成员变量)并且可以被调用的原因。
7. 总结
特性 | 语法/关键字 | 描述 | 引入版本 |
---|---|---|---|
基本 Lambda | [](){} |
创建匿名函数对象 | C++11 |
值捕获 | [=] , [x] |
拷贝外部变量 | C++11 |
引用捕获 | [&] , [&x] |
引用外部变量 | C++11 |
修改值捕获 | mutable |
允许修改按值捕获的拷贝 | C++11 |
泛型 Lambda | [](auto p){} |
参数类型自动推导,类似模板 | C++14 |
初始化捕获 | [x=expr] |
在捕获时创建并初始化变量 | C++14 |
编译时求值 | constexpr |
允许 Lambda 在编译时执行 | C++17 |