一、语法
[capture](parameters) -> return_type {
function_body
};
1) 各部分的含义如下:
- [capture]:捕获列表,用于指定 lambda 表达式可以使用的外部变量。
- (parameters):参数列表,和普通函数类似,定义lambda表达式的参数, 参数类型可以是引用、指针、值等。
- -> return_type(可选):指定返回类型,如果可以自动推导则可以省略。
- {}:函数体,定义 lambda 表达式的逻辑。
2) 捕获方式
- 空捕获([]): 在lambda内部无法使用其外层上下文中的任何局部名字。
- 捕获所有变量的副本 ([=]):通过值隐式捕获, 自动捕获所有在lambda体中使用的外部变量的副本。
- 捕获所有变量的引用 ([&]):通过引用隐式捕获, 自动捕获所有在lambda体中使用的外部变量的引用。
- 按值捕获 ([x]):显式捕获;将外部变量x按值传递给lambda表达式,即生成lambda时会复制该变量的当前值。修改lambda中的变量不会影响外部变量。其中x是一个捕获列表,可以是一个外部变量,也可以是多个外部变量。
- 按引用捕获 ([&x]):显式捕获;将外部变量x的引用传递给lambda表达式,即lambda中对变量的修改会影响外部的变量。其中x是一个捕获列表,可以是一个外部变量,也可以是多个外部变量。
- [=, &x] :x是一个捕获列表,可以是一个外部变量,也可以是多个外部变量。对于名字没有出现在捕获列表中的局部变量,通过值隐式捕获。捕获列表中不允许包含this。列出的名字必须以&为前缀。捕获列表中的变量名通过引用的方式捕获。([=, this]是一个错误,因为=已经通过复制隐式捕获this,而[&, this]中的&则表示引用捕获,并且不隐式捕获this)
- [&, x] :x是一个捕获列表,可以是一个外部变量,也可以是多个外部变量。对于名字没有出现在捕获列表中的局部变量,通过引用隐式捕获。捕获列表中可以出现this。列出的名字不能以&为前缀。捕获列表中的变量名通过值的方式捕获。
auto func = [=, this](){}; //waring: explicit by-copy capture of “this” redunant with by-copy capture default [enabled by default] ==> 显式复制捕获“this”与默认隐式复制捕获重叠(默认启用)
3) 可变性(mutable关键字)
默认情况下,lambda表达式按值捕获的变量是不可修改的。如果需要修改捕获的变量,可以使用 mutable 关键字。注意:mutable影响的是捕获的变量,通过引用传递的参数,哪怕没有mutable关键字也是可以修改的。(通常情况下,人们不希望修改函数对象(闭包)的状态,因此默认设置为不可修改。换句话说,生成的函数对象的operator()()是一个const成员函数。只有在极少数情况下,如果我们确实希望修改状态(注意,不是修改通过引用捕获的变量的状态),则可以把1ambda声明成mutable的。)
int x = 10;
//值捕获不能修改
auto modify = [x]() {
x += 5; //error: assignment of read-only variable 'x'
return x;
};
auto modify = [x]() mutable {
x += 5;
return x;
};
//引用捕获可以修改
auto modify2 = [&x]() {
x += 5;
return x;
};
int result = modify(); // result=15, x=10
int result2 = modify2(); // result=15, y=15
4) 调用与返回
lambda传递参数的规则与向函数传递参数是一样的, 从lambda返回结果也是如此。实际上,除了关于捕获的规则之外,lambda的大多数规则都是从函数和类借鉴而来的。然而,有两点需要注意:
- 如果一条lambda表达式不接受任何参数,则其参数列表可被忽略。因此,lambda表达式的最简形式是 []{}。
- lambda表达式的返回类型能由lambda表达式本身推断得到,然而函数无法做到这一点。
如果在lambda的主体部分不包含return语句,则该lambda的返回类型是 void。如果lambda的主体部分只包含一条return语句,则该lambda的返回类型是该return 表达式的类型。其他情况下,我们必须显式地提供一个返回类型。
[&]{ f(y); } // return type is void
auto z1 = [=](int x){ return x+y; } //return type is double
auto z2 = [=, y]{ if (y) return 1; else return 2; } // error: body too complicated for return type deduction
auto z3 =[y]() { return 1 : 2; } // return type is int
auto z4 = [=, y]()−>int { if (y) return 1; else return 2; } // OK: explicit return type
二、示例
//不捕获任何外部变量
auto add = [](int a, int b) {
return a + b;
};
int result = add(2, 3);
//捕获所有外部变量的引用
int x = 10, y = 20;
auto increment = [&]() {
x += 5;
y += 5;
};
increment(); // x = 15, y = 25
//如果返回类型无法自动推导,可以显式指定
auto divide = [](int a, int b) -> double {
return static_cast<double>(a) / b;
};
double result = divide(10, 3); // result = 3.33333
//值捕获 lambda 表达式内部持有的是值的副本,不受外部变量变化的影响。
int value = 42;
auto func= [value]() { std::cout << value << std::endl;};
func(); // 输出42
value = 100;
func(); // 仍然输出42
三、lambda的用途
1) 结合STL算法使用
//Lambda表达式通常与STL算法结合使用,如 std::sort、std::for_each 等
std::vector<int> vec = {1, 2, 3, 4, 5};
std::for_each(vec.begin(), vec.end(), [](int &n) { n *= 2; });
// vec 变为 {2, 4, 6, 8, 10}
2) 回调函数
- Lambda表达式可以用于回调函数,简化了传递临时逻辑的过程。
3) 并发编程
//在多线程或并发编程中,lambda表达式可以方便地传递临时任务到线程或任务队列中
std::thread t([]() {
std::cout << "Hello from thread!" << std::endl;
});
t.join();
四、注意事项
1) lambda的生命周期
lambda的生命周期可能比它的调用者更长。当我们把lambda传递给另外一个线程或者被调用者把 lambda 存在别处以供后续使用时,这种情况就会发生。例如:
void setup(Menu& m)
{
// ...
Point p1, p2, p3;
// compute positions of p1, p2, and p3
m.add("draw triangle",[&]{ m.draw(p1,p2,p3); }); // probable disaster
// ...
}
假如 add() 负责把一个(名字,动作)对添加到菜单中,并且 draw() 操作是有效的,则上述程序无异于埋下了一颗定时炸弹:setup()调用完成之后 --> 也许要到好几分钟之后 --> 用户点了draw triangle按钮,此时lambda将会试图访问一个早已不存在的局部变量。如果在某些程序中lambda需要向通过引用捕获的变量写入内容,情况就更糟糕了。
因此,如果我们发现 lambda 的生命周期可能比它的调用者更长,就必须确保所有局部信息(如果有的话)都被拷贝到闭包对象中,并且这些值应该通过return机制或者适当的实参返回。对于setup()的例子来说,很容易做到这一点:
m.add("draw triangle", [=]{ m.draw(p1,p2,p3); });
2) lambda关于捕获this的陷阱
- lambda表达式可以捕获对象的this指针,但当对象生命周期结束,继续通过lambda访问会导致野指针问题。C++17引入了捕获this的副本来解决这个问题。
class Timer { int interval; function<void()> callback; public: Timer(int ms) : interval(ms) {} void setTimeout() { // ⚠️ 危险:这里使用[this]捕获可能导致野指针 auto task = [this]() { callback(); // 💥 如果Timer对象已销毁, 这里会崩溃! }; scheduler.schedule(interval, task); } }; /** * 优化后代码: * lambda捕获的对象副本与lambda对象具有相同的生命周期。 * 被捕获的副本是作为lambda对象的一个成员存在的。只要 lambda 对象还活着,这个副本就会一直存在。 * 当 lambda 对象最终被销毁时,这个副本也会跟着被销毁。 */ class Timer { // ... 其他代码不变 ... void setTimeout() { // 🔑 使用[*this]进行值捕获, 创建Timer对象的完整副本 // 🛡️ 这样即使原Timer对象被销毁, lambda 也能安全运行 auto task = [*this]() mutable { // ✨ 在Timer副本上调用callback,完全安全 // 💫 mutable关键字允许修改捕获对象的副本 callback(); }; // 📅 将任务提交给调度器 // 🔄 调度器会持有task直到执行完成 scheduler.schedule(interval, task); } };
- 在多线程环境中,lambda表达式可能会在不同的线程中执行,而this指针指向的对象可能在其他线程中被销毁。这种情况下,即使对象没有被立即销毁,也可能因为并发访问而导致数据竞争或竞态条件。
/**
* Worker对象可能在其他线程中被销毁,而lambda表达式仍然在尝试访问它,导致未定义行为。
* 为了避免这种情况,我们需要确保在lambda表达式执行之前,对象不会被销毁;
* 并且在多线程环境中采取适当的同步措施。
*/
class Worker {
public:
void startTask() {
auto lambda = [this]() {
// 可能在其他线程中执行
processTask();
};
thread_pool.submit(lambda);
}
void processTask() {
// 处理任务
}
};
- 当类中有特殊的成员,比如智能指针或者互斥量
class ResourceManager { // 🔒 独占式智能指针,不支持复制 unique_ptr<Resource> resource; // 🔐 互斥锁对象,也不支持复制 mutex mtx; void processAsync() { // ⚠️ 以下代码存在严重问题: auto task = [*this]() { // 💥 这里会尝试复制整个对象! // ❌ 错误1: mtx是副本,不同线程会获取不同的锁,失去了互斥作用 lock_guard<mutex> lock(mtx); // ❌ 错误2: unique_ptr不支持复制,编译会失败 resource->process(); }; // 📤 提交任务到线程池 threadPool.submit(task); } }; // ✅ 正确的实现方式: class ResourceManager { // 👥 改用支持共享的智能指针 shared_ptr<Resource> resource; // 🔐 使用静态互斥锁确保真正的线程安全 static mutex& getMutex() { static mutex mtx; return mtx; } void processAsync() { // 📦 只捕获需要的资源 auto res = resource; // 👍 shared_ptr支持复制 auto task = [res]() { // ✨ 显式捕获所需资源 // ✅ 所有线程使用同一个互斥锁 lock_guard<mutex> lock(ResourceManager::getMutex()); // 🚀 安全地访问共享资源 res->process(); }; // 📤 提交到线程池 threadPool.submit(task); } };
- 使用 [*this] 捕获时要注意事项
- 确保类的所有成员都是可复制的;
- 对于不可复制的成员(如 mutex), 考虑使用静态成员或其他替代方案;
- 对于独占型智能指针(unique_ptr),考虑改用 shared_ptr;
- 如果只需要部分成员,最好显式捕获这些成员而不是整个对象;
- 注意捕获对象的大小,避免不必要的性能开销";