8. 捕获列表详解:值捕获 [=]、引用捕获 [&]、混合捕获、默认捕获模式

好,咱们今天来聊聊 lambda 里最核心、也最容易踩坑的一个东西——捕获列表

说实话,我刚开始学 C++11 那会儿,觉得 lambda 就是个语法糖。写个 [] 完事,多简单。结果呢?项目一复杂,各种悬空引用、意外拷贝、性能崩盘……嗯,后来我才老老实实把捕获列表的每个细节啃了一遍。

今天我就把这块掰开揉碎了讲给你听。你想想看,捕获列表说白了就三件事:捕获什么、怎么捕获、什么时候用哪种

8.1 值捕获 [=]:把变量“拍个快照”

值捕获,就是用 [=] 把外部变量按值复制一份到 lambda 内部。说白了,lambda 拿到的是变量的副本,外面怎么改,跟它没关系。

int x = 42;
auto lambda = [=]() {
    // x 是拷贝进来的,只读(除非加 mutable)
    std::cout << x << std::endl;
};
x = 100;
lambda(); // 输出 42,不是 100

我在项目中遇到过一个问题:有个回调函数捕获了一个大对象,结果每次调用都拷贝一次,内存暴涨。后来才发现是 [=] 惹的祸。值捕获虽然安全,但有性能代价

关键点:值捕获的变量在 lambda 创建时就被“冻结”了。后续外部修改不影响内部副本。

我的习惯:如果捕获的是 int、double 这种小类型,用值捕获没问题。如果是大对象,我会三思。

8.2 引用捕获 [&]:拿个“遥控器”

引用捕获正好相反。你用 [&],lambda 内部操作的就是外部变量的本体。改它,外面也跟着变。

int x = 42;
auto lambda = [&]() {
    x += 10; // 直接修改外面的 x
};
lambda();
std::cout << x; // 输出 52

听起来很方便对吧?但这里有个大坑——悬空引用

我曾经在写异步任务时,用 [&] 捕获了一个局部变量。结果 lambda 执行时,那个变量早就析构了。程序直接崩溃,查了半天才定位到。引用捕获的生命周期必须比 lambda 长,这是铁律。

你想想看,引用捕获就像你拿着一个遥控器。遥控器本身没问题,但如果电视机被搬走了,你按啥都没用。

8.3 混合捕获:既要又要还要

有时候,你希望某些变量按值捕获(安全),某些按引用捕获(高效)。这时候就用混合捕获

int a = 1, b = 2, c = 3;
auto lambda = [=, &b]() {
    // a 和 c 是值捕获,b 是引用捕获
    // b += 10;  // 可以修改外面的 b
    // a += 1;   // 错误!值捕获默认 const
};

语法规则很简单:

  • 默认模式写在前面([=][&]
  • 例外项用 &varvar 写在后面
  • 不能重复捕获同一个变量
写法 含义
[=, &x] 除 x 按引用捕获,其余按值
[&, x] 除 x 按值捕获,其余按引用
[=, x] 错误!x 已经在默认值捕获中
[&, &x] 错误!x 已经在默认引用捕获中

我建议:混合捕获时,把默认模式写清楚,例外项单独列出来。代码可读性会好很多。别偷懒写 [&] 然后指望别人猜你捕获了啥。

8.4 默认捕获模式:省事但别滥用

[=][&] 就是默认捕获模式。它们会捕获 lambda 体内用到的所有外部变量。

听起来很方便对吧?但我在 code review 里经常看到这样的代码:

auto callback = [=]() {
    // 几十行代码,用了十几个外部变量
    // 鬼知道捕获了啥
};

这种写法有两个问题:

  • 性能隐患:你可能无意中捕获了一个大对象,自己都不知道
  • 可读性差:别人看代码,得一行行去数你到底用了哪些变量

我的原则:除非 lambda 体非常短(两三行),否则我倾向于显式列出每个捕获变量。比如 [x, &y][=] 清晰得多。

8.5 知识体系图

下面这张图帮你理清捕获列表的完整脉络:

捕获列表 值捕获 [=] 引用捕获 [&] 混合捕获 拷贝副本 只读(默认) 直接操作本体 悬空引用风险 默认+例外 显式列出 建议:优先显式捕获,避免默认模式 值捕获安全但拷贝开销大,引用捕获高效但需注意生命周期

8.6 几个实用建议

最后,我总结几条实战经验:

  1. 小对象用值捕获,大对象用引用捕获——但前提是你得保证生命周期没问题。
  2. 不要在默认捕获模式里混入 this——[=] 会隐式捕获 this,如果你在成员函数里用 lambda,小心 this 指针失效。
  3. mutable 时要谨慎——值捕获默认是 const,加 mutable 才能修改副本。但改了也没人知道,容易产生迷惑行为。
  4. C++14 支持初始化捕获——比如 [x = std::move(obj)],可以更精细地控制捕获方式。这个后面会细讲。

我个人习惯:写 lambda 时,先想清楚“这个 lambda 会在哪里执行?它的生命周期多长?”然后再决定用哪种捕获。别一上来就 [&] 完事,那是给自己埋雷。

好了,捕获列表这块就聊到这儿。记住一句话:捕获列表不是语法糖,是设计决策。选对了,代码干净又高效;选错了,调试到怀疑人生。

公众号:蓝海资料掘金营,微信 deep3321