50 / 80 · C++14 · 约 9 分钟
Lambda 捕获:闭包也是有生命期的对象
Lambda 创建带调用运算符的闭包对象,捕获决定它保存值还是借用外部状态。按值、按引用与初始化捕获有不同的所有权后果;把回调存起来之前,应先证明它依赖的对象能活到调用结束。
捕获是闭包的数据成员设计
C++11 的 lambda 不是脱离对象模型的匿名代码段。编译器为每个 lambda 表达式产生独特的闭包类型,捕获的状态随闭包对象存储。[n] 在创建闭包时保存 n 的值,[&n] 则借用原对象;以后外部修改 n,只有借用版本必然观察到新值。
普通按值捕获的闭包调用运算符默认是 const,因此不能直接修改捕获的副本。加 mutable 后可以改变副本,但不会把改动同步到外部 n。示例让副本自增与原对象自增分别发生,证明 mutable 改变的是闭包自己的可修改性。
初始化捕获可以转移所有权
C++14 增加初始化捕获,例如 [p = std::move(owner)],适合把 unique_ptr 的所有权移入回调。闭包因此也可能成为只能移动、不能复制的类型。泛型 lambda 的 auto 参数同样从 C++14 开始,背后仍然是模板化调用运算符,不是运行期任意类型容器。
例子将一个整数的独占所有者放入 lambda,并在源指针为空后安全读取闭包拥有的整数。这比捕获局部 unique_ptr 的引用更适合需要离开当前作用域的任务。注意复制裸指针或 string_view 只复制借用信息,不会自动复制其指向的数据。
存储与异步调用需要额外检查
引用捕获不会延长对象生命期,返回捕获局部变量引用的 lambda 会留下悬空借用。成员函数中的 [this] 保存指针而非整个对象,默认按值捕获也不能据此保证对象活着;C++17 的 [*this] 才复制对象,但复制后的业务语义仍需审查。
立即执行的小算法通常可以清楚地借用局部数据;长期保存的回调应优先显式列出捕获,明确值语义、所有权或外部生命周期约束。调用包装器也有限制:C++20 的 std::function 要求目标可复制,不能直接接纳本例拥有 unique_ptr 的闭包。
容易答错的地方
- mutable 只允许修改按值捕获的副本,不会把副本变成外部变量的引用。
- [=] 不表示递归深拷贝;捕获 this、裸指针或视图时,底层对象仍可能先于回调销毁。
运行一个例子
最低标准 C++14 · 完整程序 · 下载 .cpp
#include <cassert>
#include <iostream>
#include <memory>
#include <utility>
int main() {
int n = 5;
auto own_copy = [n]() mutable { return ++n; };
auto borrow = [&n] { return ++n; };
const int a = own_copy();
assert(a == 6 && n == 5);
const int b = borrow();
assert(b == 6 && n == 6);
auto p = std::make_unique<int>(9);
auto owns_resource = [p = std::move(p)] { return *p; };
assert(!p && owns_resource() == 9);
std::cout << a << ' ' << b << ' ' << owns_resource() << '\n';
}
在本地编译
g++ -std=c++14 -Wall -Wextra -Wpedantic -pthread modern-lambda.cpp -o example && ./example预期结果
6 6 9
CHECK YOUR UNDERSTANDING
合上答案,试着解释。
不修改示例结构,再调用一次 own_copy(),它返回多少?如果在此之前令 n = 100,会改变该结果吗?
查看参考答案
返回 7。第一次调用已经把闭包内部副本从 5 改成 6,第二次继续自增。外部 n 改成 100 不影响副本。borrow() 则会访问外部 n 并将其变为 101,这正是两种捕获策略的区别。
继续查证
标准草案链接会随工作草案更新;本文版本标记对应示例最低要求,不表示草案中的所有新规则都适用于旧标准。