自 C++11 标准引入以来,Lambda 表达式(也称为匿名函数)已经成为现代 C++ 编程中不可或缺的一部分。它们提供了一种简洁而强大的方式来定义内联函数,特别适用于需要临时函数对象的场景,如算法库的使用。这次整理,我尽可能全面深入地探讨 C++ 中的 Lambda 表达式,包括其语法、特性、用法和实现原理。
一、什么是 Lambda 表达式#
Lambda 表达式是一种 匿名函数,可以在代码中定义并立即使用,而无需为其命名。这使得代码更加简洁和可读,特别是在需要将函数作为参数传递的情况下。
Lambda 表达式的特点#
- 简洁性:无需定义额外的函数或函数对象类。
- 就近性:函数定义与使用地点接近,方便阅读和维护。
- 强大性:支持捕获外部变量,能够访问所在作用域的上下文。
基本语法#
1
2
3
|
[capture list](parameter list) -> return type {
function body
};
|
- capture list(捕获列表):指定哪些外部变量可在 Lambda 表达式中使用,以及如何捕获。
- parameter list(参数列表):与普通函数的参数列表类似,指定传递给 Lambda 的参数。
- return type(返回类型):可选,使用尾置返回类型(trailing return type)语法。
- function body(函数体):Lambda 表达式的执行代码。
1
2
3
|
[](const int& no) -> void {
std::cout << "编号 " << no << ": 你好,Lambda!\n";
};
|
二、Lambda 表达式的使用示例#
让我们通过一个完整的示例来理解 Lambda 表达式的实际应用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
#include <iostream>
#include <vector>
#include <algorithm>
void show(const int& no) {
std::cout << "普通函数:编号 " << n << "\n";
}
int main() {
std::vector<int> numbers = {1, 2, 3};
// 使用普通函数
std::for_each(numbers.begin(), numbers.end(), show);
// 使用函数对象(仿函数)
struct Functor {
void operator()(int n) const {
std::cout << "仿函数:编号 " << n << "\n";
}
};
std::for_each(numbers.begin(), numbers.end(), Functor());
// 使用 Lambda 表达式
std::for_each(numbers.begin(), numbers.end(), [](int n) {
std::cout << "Lambda 表达式:编号 " << n << "\n";
});
return 0;
}
|
输出:
1
2
3
4
5
6
7
8
9
|
普通函数:编号 1
普通函数:编号 2
普通函数:编号 3
仿函数:编号 1
仿函数:编号 2
仿函数:编号 3
Lambda 表达式:编号 1
Lambda 表达式:编号 2
Lambda 表达式:编号 3
|
三、Lambda 表达式的各个部分详解#
3.1 捕获列表(Capture List)#
捕获列表位于 []
中,用于指定 Lambda 表达式可以访问的外部变量。
捕获方式
- 值捕获(By Value):
[a, b]
,拷贝外部变量的值。
- 引用捕获(By Reference):
[&a, &b]
,引用外部变量。
- 隐式捕获:
- 按值捕获所有外部变量:
[=]
- 按引用捕获所有外部变量:
[&]
- 混合捕获:
[=, &a, &b]
或 [&, a, b]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
int x = 10;
int y = 20;
// 值捕获
auto lambda_by_value = [x, y]() {
// x 和 y 的值是在 Lambda 创建时拷贝的
std::cout << "值捕获:x = " << x << ", y = " << y << "\n";
};
// 引用捕获
auto lambda_by_ref = [&x, &y]() {
// x 和 y 是引用,指向原始变量
x += 10;
y += 10;
std::cout << "引用捕获:x = " << x << ", y = " << y << "\n";
};
lambda_by_value(); // 输出:x = 10, y = 20
lambda_by_ref(); // 输出:x = 20, y = 30
std::cout << "外部变量:x = " << x << ", y = " << y << "\n"; // 输出:x = 20, y = 30
|
注意:
- 值捕获的变量在 Lambda 创建时被拷贝,后续对原始变量的修改不会影响 Lambda 内部的值。
- 引用捕获的变量,Lambda 内部的修改会影响外部变量。
3.2 参数列表(Parameter List)#
Lambda 表达式的参数列表与普通函数类似,但有以下几点需要注意:
- 必须为每个参数命名:不能省略参数名。
- 不支持默认参数:无法为参数指定默认值。
- 不支持可变参数:无法使用
...
可变参数列表。
1
2
3
4
5
|
auto sum = [](int a, int b) {
return a + b;
};
std::cout << "Sum: " << sum(3, 4) << "\n"; // 输出:Sum: 7
|
3.3 返回类型(Return Type)#
Lambda 表达式可以显式指定返回类型,使用尾置返回类型语法 -> return_type
。如果省略,编译器会自动推导。
1
2
3
4
5
6
7
8
9
|
// 编译器自动推导返回类型
auto multiply = [](int a, int b) {
return a * b;
};
// 显式指定返回类型为 double
auto divide = [](int a, int b) -> double {
return static_cast<double>(a) / b;
};
|
建议:
- 对于复杂的返回类型或多种返回路径,显式指定返回类型以避免推导错误。
3.4 函数体(Function Body)#
函数体包含 Lambda 表达式要执行的代码,与普通函数的函数体类似。
1
2
3
4
5
|
auto greet = []() {
std::cout << "Hello, Lambda!\n";
};
greet(); // 输出:Hello, Lambda!
|
四、捕获列表的深入解析#
4.1 值捕获(By Value)#
- 特性:捕获时拷贝变量的值,Lambda 内部的修改不会影响外部变量。
- 限制:默认情况下,Lambda 内部不能修改值捕获的变量(除非使用
mutable
关键字)。
1
2
3
4
5
6
7
8
9
|
int count = 0;
auto increment = [count]() mutable {
// 需要 mutable 才能修改值捕获的变量
count++;
std::cout << "内部 count = " << count << "\n";
};
increment(); // 输出:内部 count = 1
std::cout << "外部 count = " << count << "\n"; // 输出:外部 count = 0
|
4.2 引用捕获(By Reference)#
- 特性:捕获外部变量的引用,Lambda 内部的修改会影响外部变量。
- 注意:需要确保外部变量在 Lambda 表达式执行时仍然存在。
1
2
3
4
5
6
7
8
|
int count = 0;
auto increment = [&count]() {
count++;
std::cout << "内部 count = " << count << "\n";
};
increment(); // 输出:内部 count = 1
std::cout << "外部 count = " << count << "\n"; // 输出:外部 count = 1
|
4.3 隐式捕获(Implicit Capture)#
- 按值捕获所有外部变量:
[=]
- 按引用捕获所有外部变量:
[&]
1
2
3
4
5
6
7
8
9
10
11
|
int a = 5, b = 10;
// 按值捕获所有变量
auto lambda_value = [=]() {
std::cout << "按值捕获:a = " << a << ", b = " << b << "\n";
};
// 按引用捕获所有变量
auto lambda_ref = [&]() {
std::cout << "按引用捕获:a = " << a << ", b = " << b << "\n";
};
|
4.4 混合捕获(Mixed Capture)#
- 格式:
[default_capture, variable_list]
- 规则:默认捕获方式和显式捕获方式不能对同一变量使用。
1
2
3
4
5
6
7
8
9
|
int x = 1, y = 2, z = 3;
// 默认按值捕获,显式按引用捕获 x
auto lambda_mixed = [=, &x]() {
x++;
std::cout << "x = " << x << ", y = " << y << ", z = " << z << "\n";
};
lambda_mixed(); // 输出:x = 2, y = 2, z = 3
|
4.5 修改值捕获的变量:mutable
关键字#
- 作用:允许在 Lambda 内部修改值捕获的变量。
- 限制:修改仅在 Lambda 内部有效,不会影响外部变量。
1
2
3
4
5
6
7
8
|
int value = 5;
auto modify = [value]() mutable {
value *= 2;
std::cout << "内部 value = " << value << "\n";
};
modify(); // 输出:内部 value = 10
std::cout << "外部 value = " << value << "\n"; // 输出:外部 value = 5
|
五、Lambda 表达式的高级特性#
5.1 异常说明和 noexcept
#
Lambda 表达式可以指定异常说明,以声明可能抛出的异常类型。
1
2
3
|
auto safe_divide = [](int a, int b) noexcept -> double {
return static_cast<double>(a) / b;
};
|
5.2 Lambda 表达式的类型和实现#
- 类型:每个 Lambda 表达式都有唯一的匿名类型,不能被复制或赋值。
- 实现原理:编译器将 Lambda 表达式转换为一个拥有
operator()
的匿名类。
1
2
3
4
5
6
7
8
9
10
11
|
auto lambda = [](int n) {
return n * n;
};
// 等价于
class __LambdaUniqueName__ {
public:
int operator()(int n) const {
return n * n;
}
};
|
六、Lambda 表达式的实际应用场景#
6.1 标准算法库的使用#
Lambda 表达式在使用 STL 算法时非常方便,例如 std::sort
、std::for_each
。
1
2
3
4
5
6
7
8
9
10
|
std::vector<int> numbers = {3, 1, 4, 1, 5};
std::sort(numbers.begin(), numbers.end(), [](int a, int b) {
return a > b; // 降序排序
});
for (int n : numbers) {
std::cout << n << " ";
}
// 输出:5 4 3 1 1
|
6.2 事件处理和回调函数#
在需要传递回调函数的场景,如 GUI 编程或异步操作。
1
2
3
4
5
6
7
8
9
10
11
|
void setCallback(std::function<void(int)> callback) {
// 模拟事件触发
callback(42);
}
int main() {
setCallback([](int eventId) {
std::cout << "事件触发,ID:" << eventId << "\n";
});
return 0;
}
|
七、注意事项和最佳实践#
- 避免捕获过多的变量:捕获列表应尽量精简,明确指定需要的变量。
- 显式指定返回类型:在可能有歧义或复杂返回类型时,显式指定返回类型。
- 谨慎使用引用捕获:确保引用的变量在 Lambda 执行时仍然有效。
- 理解可变性:使用
mutable
时,注意值捕获的变量修改仅在 Lambda 内部有效。