C++ / a working model

50 / 80   ·   C++14   ·   约 9 分钟

Lambda 捕获:闭包也是有生命期的对象

先记住这句话

Lambda 创建带调用运算符的闭包对象,捕获决定它保存值还是借用外部状态。按值、按引用与初始化捕获有不同的所有权后果;把回调存起来之前,应先证明它依赖的对象能活到调用结束。

本篇内容
  1. 捕获是闭包的数据成员设计
  2. 初始化捕获可以转移所有权
  3. 存储与异步调用需要额外检查
  4. 运行示例
  5. 动手练习

捕获是闭包的数据成员设计

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,这正是两种捕获策略的区别。

继续查证

标准草案链接会随工作草案更新;本文版本标记对应示例最低要求,不表示草案中的所有新规则都适用于旧标准。

回到目录