3.7 Lambda与闭包对象


3.7 Lambda 与闭包对象

本节摘要:Lambda 表达式在内存视角下毫无神秘——编译器为它生成一个匿名类,捕获列表变成成员变量,函数体变成 operator() 的实现。值捕获拷贝数据,引用捕获只存地址(悬垂风险自担),捕获 this 实际存的是指针。理解"闭包是对象",std::function 的开销、泛型 Lambda、递归 Lambda 的写法全部顺理成章。

学习目标

阅读完本节,你应当能够:

  1. 写出 Lambda 的等价匿名类形式,说出闭包对象的内存构成;
  2. 区分值捕获、引用捕获、初始化捕获的生存期后果;
  3. 按场景在 auto 与 std::function 之间选择存储方式;
  4. 使用泛型 Lambda 与捕获 this 的安全姿势。

一、拆开看:Lambda 就是个类

#include <iostream> int main() { int factor = 3; auto f = [factor](int x) { return x * factor; }; std::cout << f(10); // 30 }

编译器眼里,这段代码近似于:

class __匿名 { public: __匿名(int f) : factor_(f) {} int operator()(int x) const { return x * factor_; } private: int factor_; // 捕获列表原样变成成员变量 }; int factor = 3; __匿名 f = __匿名{factor}; // 捕获发生在闭包构造时

闭包 = 这个匿名类型的对象。捕获列表即成员清单,调用即 operator()。这个视角一次解决好几个疑问:

  • sizeof(f) 是多少?就是捕获数据的大小——捕获一个 int 闭包就是 4 字节,什么都没捕获的空闭包为 1 字节。
  • 为什么没捕获的 Lambda 可以转成函数指针(int(*)(int)),捕获了的不能?因为空闭包无状态,函数指针放不下"成员变量",带状态的闭包必须以对象形式存在。
  • mutable 干什么用?operator() 默认 const,改不了值捕获的成员;mutable 去掉 const,允许修改闭包自己的副本(外部原件依然不动)。

二、三种捕获,三种生存期

值捕获 [x]:构造闭包时拷贝一份,闭包与原件此后互不相干。安全但有拷贝成本,适合小型数据。

引用捕获 [&x]:只存地址,零拷贝但闭包的寿命不能盖过被引用者:

auto make_bad() { int local = 42; return [&local](int x) { return x + local; }; // local 随栈帧消亡 } // 返回的闭包持有悬垂引用 调用即未定义行为 auto make_good() { int local = 42; return [local](int x) { return x + local; }; // 拷贝进闭包 安全 }

逃出作用域的闭包(存进容器、回调注册、跨线程投递)一律禁止引用捕获,这是 Lambda 第一军规。函数内就地使用(std::sort 的比较器用完即弃)则引用捕获完全合宜。

初始化捕获 [v = std::move(big)](C++14):捕获时执行任意初始化表达式,让"移动进闭包"成为可能——大对象(字符串、容器、unique_ptr)进闭包的标准姿势:

auto task = [buf = std::move(big_vector)]() { process(buf); // big_vector 已被搬空 所有权在闭包里 };

这也回答了"unique_ptr 怎么进闭包":它是只移类型,[p = std::move(uptr)] 是唯一通路。[=, &big][&, small] 这类混合捕获按需可用,但显式列出每个名字始终是可读性最好的写法。

捕获 this 的陷阱单独划重点:[=][this] 在成员函数里捕获的是this 指针,不是对象副本。对象析构后闭包再调用,访问的就是亡灵:

class Session { public: void register_callback() { handler_ = [this] { on_event(); }; // 存的是 this 指针 } void on_event(); std::function<void()> handler_; }; // Session 销毁后 handler_ 若仍被调用 on_event 访问已亡对象

C++17 的安全替代是拷贝所需成员([id = id_]),或与智能指针配合:[self = shared_from_this()] 让闭包持有股份,对象活到回调结束(3.3 节的 shared 语义在此落地)。

三、auto 与 std::function:存闭包的两种货架

auto a = [](int x) { return x * 2; }; // 闭包原生存放 零开销 #include <functional> std::function<int(int)> b = a; // 类型擦除容器

auto 保存闭包的具体匿名类型,调用完全内联、零成本;std::function 是统一的"可调用物货架",能装 Lambda、函数指针、仿函数对象,代价是一次可能的堆分配(闭包超过小缓冲时)加一次间接调用加阻止内联。分工:局部使用、模板参数(std::sort 的比较器)用 auto 或模板;需要存储异构可调用物(回调注册表、事件系统)才用 function。C++23 的 std::move_only_function 补上了"装只移闭包"的空档。

四、泛型、递归与就地组合

泛型 Lambda(C++14 起 auto 参数)免写模板:

auto twice = [](auto&& f, auto&& v) { // 万能引用承接任意可调用物 return f(f(std::forward<decltype(v)>(v))); };

捕获时立即执行(C++14 的另一个便利):[n = compute()] 在构造闭包那一刻求值,而不是每次调用。

递归 Lambda 直接写不行(名字还没定就引用自己),出路是 std::function 显式声明类型,或用辅助函数 y_combinator 思路;日常更干脆的做法是写个普通函数或函数模板——Lambda 的定位是"就地的小段行为",超过二十行或需要递归时,命名函数更清晰。

与 STL 算法搭配是 Lambda 的主场,第 4 章将大量出现:

#include <vector> #include <algorithm> std::vector<int> v{3, 1, 4, 1, 5}; std::sort(v.begin(), v.end(), [](int a, int b) { return a > b; }); // 降序 auto it = std::find_if(v.begin(), v.end(), [](int x) { return x % 2 == 0; });

💡 关键直觉:把 Lambda 读成"构造一个带随身数据的小对象",一切行为都能推导:捕获是构造参数,值捕获是按值成员,引用捕获是按引用成员,调用是 operator()。它不是新语言特性,而是把你本要手写的那个仿函数类,交给编译器代笔。

本节要点回顾

  • 闭包即对象:捕获列表变成员、函数体变 operator(),sizeof 可算、行为可推理。
  • 引用捕获的军规:闭包逃出作用域必悬垂;就地使用才允许 &。
  • 初始化捕获是搬家的门:move 大对象与 unique_ptr 进闭包的唯一正道。
  • this 捕获存的是指针:回调用 shared_from_this 或拷贝成员保命。
  • auto 优先 function 兜底:内联与统一货架各司其职,别为统一多付每次调用的税。

第三章通关:资源从裸奔到全副武装。最后一章把视野放大——标准库的容器、流、并发设施,全是本章原则的工业化成品。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U