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 和闭包的关系:
8.5 总结几个要点
- 闭包不是 Lambda——Lambda 是表达式,闭包是它的运行时产物
- 生命周期要心里有数——栈上闭包随作用域结束,堆上闭包由智能指针管理
- 引用捕获逃逸是大坑——我吃过亏,希望你不用再吃
- 拷贝/移动语义自动推导——捕获了什么类型,闭包就有什么语义
一个小建议:如果你不确定闭包的生命周期,优先用值捕获 + std::move。这样既安全,又不会因为拷贝大对象而影响性能。
嗯,关于闭包的核心内容就这些。理解清楚这几个概念,写 Lambda 的时候就能更自信了。
公众号:蓝海资料掘金营,微信deep3321