本节摘要:Lambda 表达式在内存视角下毫无神秘——编译器为它生成一个匿名类,捕获列表变成成员变量,函数体变成 operator() 的实现。值捕获拷贝数据,引用捕获只存地址(悬垂风险自担),捕获 this 实际存的是指针。理解"闭包是对象",std::function 的开销、泛型 Lambda、递归 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 字节。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 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()。它不是新语言特性,而是把你本要手写的那个仿函数类,交给编译器代笔。
第三章通关:资源从裸奔到全副武装。最后一章把视野放大——标准库的容器、流、并发设施,全是本章原则的工业化成品。