C++ lambda表达式
·
C++ 中的 Lambda 表达式(Lambda Expression)是 C++11 引入的一项重大特性,它允许你在代码中直接定义匿名函数对象。这使得代码更加简洁,特别是在需要传递短小的函数逻辑给算法(如 std::sort, std::for_each)或作为回调函数时。
1. 基本语法结构
Lambda 表达式的标准语法如下:
[捕获列表] (参数列表) mutable(可选) -> 返回类型(可选) { 函数体 }
[捕获列表]:指定如何从外部作用域捕获变量(这是 Lambda 最强大的功能)。(参数列表):与普通函数一样,定义输入参数。如果无参数,可省略括号[]。mutable:默认情况下,按值捕获的变量在 Lambda 内是const的。加上mutable后,允许修改按值捕获的副本。-> 返回类型:尾随返回类型。通常编译器可以自动推导,但在复杂逻辑或无返回值(需显式写void)时可省略或显式指定。{ 函数体 }:具体的执行逻辑。
2. 捕获列表(Capture Clause)详解
捕获列表决定了 Lambda 内部如何访问外部变量。
| 语法 | 含义 |
|---|---|
[] |
不捕获任何外部变量。 |
[x, y] |
按值捕获 x 和 y。Lambda 内部拥有它们的副本,修改不影响外部。 |
[&x, &y] |
按引用捕获 x 和 y。Lambda 内部直接操作外部变量,修改会影响外部。 |
[=] |
按值捕获当前作用域内的所有自动变量。 |
[&] |
按引用捕获当前作用域内的所有自动变量。 |
[&, x] |
默认按引用捕获所有,但 x 按值捕获。 |
[=, &y] |
默认按值捕获所有,但 y 按引用捕获。 |
[this] |
捕获当前对象的 this 指针(用于成员函数中)。 |
[*this] |
(C++17) 按值捕获当前对象的所有成员。 |
重要注意事项:悬空引用
如果使用 [&] 或 [&var] 按引用捕获局部变量,必须确保 Lambda 的生命周期不超过被捕获变量的生命周期。如果 Lambda 被存储并在外部变量销毁后执行,会导致未定义行为(崩溃)。
3. 代码示例
示例 1:简单的按值与按引用捕获
#include <iostream>
#include <vector>
#include <algorithm>
int main() {
int factor = 10;
std::vector<int> nums = {1, 2, 3, 4, 5};
// 按值捕获 factor:Lambda 内部使用的是 factor 的副本
auto by_value = [factor](int n) { return n * factor; };
// 按引用捕获 factor:如果 factor 改变,Lambda 结果也会变
auto by_ref = [&factor](int n) { return n * factor; };
std::cout << "初始 factor: " << factor << std::endl;
std::cout << "按值计算 (5 * 10): " << by_value(5) << std::endl;
factor = 20; // 修改外部变量
std::cout << "修改后 factor: " << factor << std::endl;
std::cout << "按值计算 (5 * 10): " << by_value(5) << std::endl; // 仍然是 50
std::cout << "按引用计算 (5 * 20): " << by_ref(5) << std::endl; // 变成 100
return 0;
}
示例 2:配合 STL 算法使用
这是 Lambda 最常见的场景,替代了以前需要编写独立函数或函数对象(Functor)的做法。
#include <iostream>
#include <vector>
#include <algorithm>
int main() {
std::vector<int> vec = {5, 2, 9, 1, 5, 6};
int threshold = 4;
// 排序:降序
std::sort(vec.begin(), vec.end(), [](int a, int b) {
return a > b;
});
// 查找第一个大于 threshold 的元素
auto it = std::find_if(vec.begin(), vec.end(), [threshold](int n) {
return n > threshold;
});
if (it != vec.end()) {
std::cout << "找到第一个大于 " << threshold << " 的数: " << *it << std::endl;
}
// 遍历并修改:将所有偶数加倍 (需要 mutable 因为按值捕获的副本默认是 const,但这里没捕获外部变量,只是修改参数n的副本无效,若要修改容器需引用)
// 正确做法:直接修改容器元素,通常用引用参数
std::for_each(vec.begin(), vec.end(), [](int &n) {
if (n % 2 == 0) n *= 2;
});
return 0;
}
示例 3:mutable 关键字的使用
当你按值捕获变量,却需要在 Lambda 内部修改这个副本时,必须使用 mutable。
#include <iostream>
int main() {
int count = 0;
// 错误示范:没有 mutable,无法修改按值捕获的 count_copy
// auto lambda_err = [count]() { count++; };
// 正确示范
auto lambda_ok = [count]() mutable {
count++;
std::cout << "内部副本计数: " << count << std::endl;
};
lambda_ok(); // 输出 1
lambda_ok(); // 输出 2 (状态保持在 Lambda 对象内部)
std::cout << "外部原始计数: " << count << std::endl; // 输出 0,外部未受影响
return 0;
}
示例 4:泛型 Lambda (C++14 及以上)
在 C++14 中,参数列表可以使用 auto,使得 Lambda 成为模板函数。
#include <iostream>
int main() {
// 泛型 Lambda,编译器会自动推导 T
auto add = [](auto a, auto b) {
return a + b;
};
std::cout << add(1, 2) << std::endl; // int
std::cout << add(1.5, 2.5) << std::endl; // double
std::cout << add(std::string("Hi"), " Mom") << std::endl; // string
return 0;
}
4. Lambda 的本质
在底层,Lambda 表达式实际上是语法糖。编译器会将 Lambda 转换为一个唯一的匿名类(闭包对象),该类重载了 operator()。
例如:
int x = 10;
auto f = [x](int y) { return x + y; };
大致等价于编译器生成的如下代码(简化版):
class __lambda_unique_name__ {
int x; // 捕获的成员变量
public:
__lambda_unique_name__(int val) : x(val) {} // 构造函数
int operator()(int y) const { // 重载调用运算符
return x + y;
}
};
// 使用时:
__lambda_unique_name__ f(x);
f(5);
正因为如此,你可以使用 decltype 获取 Lambda 的类型,或者将 Lambda 存储在 std::function 中(虽然会有轻微的性能开销,但提供了多态性和可存储性)。
#include <functional>
#include <iostream>
int main() {
auto lam = [](int i) { return i * 2; };
// 存储到 std::function
std::function<int(int)> func = lam;
std::cout << func(10) << std::endl; // 输出 20
return 0;
}
5. 最佳实践总结
- 优先使用自动推导:除非必要(如递归或需要特定返回类型),否则让编译器推导返回类型。
- 小心捕获列表:
- 如果在多线程环境或异步回调中使用,尽量避免
[&],防止悬空引用。 - 明确列出需要的变量(如
[x, &y])通常比隐式捕获([=],[&])更安全且可读性更好。
- 如果在多线程环境或异步回调中使用,尽量避免
- 性能考量:
- Lambda 本身几乎没有运行时开销(通常会被内联优化)。
- 但如果将其存入
std::function,会引入动态分配和虚函数调用的开销。在对性能极度敏感的场景(如高频交易、游戏循环热点),尽量直接使用 Lambda 对象或模板传递,避免std::function。
- 可读性:如果 Lambda 逻辑超过 3-5 行,或者逻辑非常复杂,建议将其提取为独立的命名函数,以保持代码清晰。
更多推荐
所有评论(0)