8. Lambda与闭包:闭包的概念、Lambda对象的生命周期、拷贝与移动

好,咱们今天聊点实在的——Lambda 和闭包。

很多 C++ 开发者写 Lambda 写得飞起,但一问「闭包是什么」,就有点含糊了。我当年也是这样。记得刚接触 C++11 那会儿,看到 [&] 捕获就觉得很爽,直到有一次在异步回调里踩了个大坑……嗯,从那以后我才真正去理解闭包的本质。

8.1 闭包到底是什么?

说白了,闭包就是「Lambda 表达式 + 它捕获的变量」打包在一起的一个对象。

你写一个 Lambda:

int factor = 3;
auto multiply = [factor](int x) { return x * factor; };

这个 multiply 就是一个闭包对象。它内部保存了 factor 的副本(或者引用),并且可以像函数一样被调用。

我习惯把闭包理解成一个「带状态的函数对象」。编译器其实会把它翻译成一个匿名类,捕获的变量就是类的成员变量。你想想看,这不就是仿函数(functor)的语法糖吗?

核心概念:Lambda 表达式是语法,闭包是运行时对象。每次执行 Lambda 表达式,都会生成一个新的闭包对象。

8.2 Lambda 对象的生命周期

这个问题很关键。闭包对象的生命周期,完全取决于你把它存在哪里。

8.2.1 栈上的闭包

最常见的情况:

void demo() {
    int local = 42;
    auto lambda = [local]() { return local; };
    // lambda 的生命周期和普通局部变量一样
    // 离开作用域就销毁
}

这个没什么好说的,和 int、string 一样,栈上分配,作用域结束就析构。

8.2.2 闭包逃逸——小心悬空引用

这里我要重点说一下。我曾经在项目里写过这样的代码:

std::function<int()> getCallback() {
    int value = 100;
    return [&value]() { return value; };  // 危险!
}

为什么危险?value 是局部变量,函数返回后就销毁了。但闭包还持有它的引用。调用这个闭包时,就是典型的悬空引用——未定义行为。

避坑指南:我曾经在一个网络库的异步回调里用过引用捕获,结果程序偶尔崩溃,查了两天才定位到。从那以后,我给自己定了个规矩:如果闭包要逃逸出当前作用域,一律用值捕获或者移动捕获

8.2.3 堆上的闭包

闭包也可以放在堆上,比如通过 std::function 或者 std::shared_ptr

auto sp = std::make_shared<std::function<int()>>(
    [x = 10]() { return x; }
);

这种情况下,闭包的生命周期由智能指针管理。只要还有引用,闭包就活着。

8.3 拷贝与移动

闭包对象支持拷贝和移动吗?答案是:取决于捕获的变量。

8.3.1 可拷贝的闭包

如果所有捕获的变量都是可拷贝的,那么闭包也是可拷贝的:

int a = 1;
auto lambda1 = [a]() { return a; };
auto lambda2 = lambda1;  // OK,拷贝构造

8.3.2 不可拷贝的闭包

如果捕获了 std::unique_ptr 这样的 move-only 类型,闭包本身也是 move-only:

auto up = std::make_unique<int>(42);
auto lambda = [up = std::move(up)]() { return *up; };
// auto lambda2 = lambda;  // 编译错误!不可拷贝
auto lambda3 = std::move(lambda);  // OK,移动构造

我记得有一次在代码审查里看到有人试图拷贝一个捕获了 unique_ptr 的 Lambda,编译不过还一脸懵。其实编译器已经说得很清楚了——「已删除的拷贝构造函数」。

8.3.3 闭包作为函数参数

传参时要注意:

void process(std::function<void()> f) {
    f();
}

auto big_lambda = [large_data = std::vector<int>(1000000)]() {
    // 处理大数据
};

// 传值:会拷贝闭包,包括 large_data
process(big_lambda);

// 传移动:避免拷贝
process(std::move(big_lambda));

我的习惯:如果闭包捕获了较大的数据,我一般用 std::move 传递,或者用 std::ref 包装引用。避免不必要的拷贝开销。

8.4 知识体系图

下面这张图帮你理清 Lambda 和闭包的关系:

Lambda 与闭包知识体系 Lambda 表达式 编译期语法结构 闭包对象 运行时实例 匿名函数对象 编译器生成类 生命周期 栈上闭包 作用域结束即销毁 逃逸闭包 小心悬空引用! 堆上闭包 智能指针管理 拷贝与移动 可拷贝闭包 所有捕获可拷贝 仅可移动闭包 捕获了 unique_ptr 等 ⚠ 引用捕获逃逸 = 未定义行为 核心原则:闭包是带状态的函数对象,生命周期由存储位置决定

8.5 总结几个要点

  • 闭包不是 Lambda——Lambda 是表达式,闭包是它的运行时产物
  • 生命周期要心里有数——栈上闭包随作用域结束,堆上闭包由智能指针管理
  • 引用捕获逃逸是大坑——我吃过亏,希望你不用再吃
  • 拷贝/移动语义自动推导——捕获了什么类型,闭包就有什么语义

一个小建议:如果你不确定闭包的生命周期,优先用值捕获 + std::move。这样既安全,又不会因为拷贝大对象而影响性能。

嗯,关于闭包的核心内容就这些。理解清楚这几个概念,写 Lambda 的时候就能更自信了。


公众号:蓝海资料掘金营,微信deep3321