15. Lambda 的捕获陷阱:悬空引用、生命周期问题、捕获 this 指针
Lambda 表达式用起来确实爽,但捕获这块儿,坑是真的多。
我见过不少项目,线上崩溃查了半天,最后定位到是 Lambda 捕获出了问题。说白了,Lambda 捕获的本质是把外部变量“带进”闭包。但怎么带、带的是什么、带进来之后还能不能用——这些细节一旦忽略,就是灾难。
15.1 捕获的本质:值还是引用?
先看个最简单的例子:
int x = 42;
auto lambda = [&]() { return x; };
x = 100;
std::cout << lambda(); // 输出 100
为什么是 100?因为 [&] 捕获的是引用。Lambda 内部存的是 x 的地址,不是副本。你想想看,如果外部 x 被销毁了,这个引用就悬空了。
反过来,值捕获就安全得多:
int x = 42;
auto lambda = [=]() { return x; };
x = 100;
std::cout << lambda(); // 输出 42
值捕获在 Lambda 创建时就把 x 的副本存进去了。之后外部怎么改,跟 Lambda 内部没关系。
15.2 悬空引用:最常见的坑
我在项目中遇到过最典型的场景——把 Lambda 存起来异步执行:
std::function<void()> task;
void createTask() {
int localVar = 42;
task = [&localVar]() {
std::cout << localVar << std::endl;
};
} // localVar 在这里销毁了
void runTask() {
task(); // 崩溃!localVar 已经没了
}
为什么会这样?createTask 执行完,localVar 的栈帧就释放了。Lambda 里存的引用指向了一块已经被回收的内存。访问它,就是未定义行为。
我曾经在一个网络库中排查过一个诡异的内存错乱问题。查了两天才发现,是某个回调 Lambda 捕获了一个栈上的临时对象引用。那个对象早就析构了,但 Lambda 还在队列里等着执行。嗯,从那以后,我给自己定了个规矩:
- Lambda 如果立即执行(比如传给
std::for_each),引用捕获没问题 - Lambda 如果被存储或延迟执行,一律值捕获,或者用
std::shared_ptr管理生命周期
15.3 生命周期问题:不仅仅是引用
值捕获就绝对安全吗?不一定。看这个:
std::unique_ptr<int> ptr = std::make_unique<int>(42);
auto lambda = [ptr = std::move(ptr)]() {
std::cout << *ptr << std::endl;
};
这里用了移动捕获(C++14 特性),把 unique_ptr 的所有权转移到了 Lambda 内部。Lambda 销毁时,unique_ptr 自动释放内存。没问题。
但如果你不小心捕获了原始指针:
int* raw = new int(42);
auto lambda = [raw]() {
std::cout << *raw << std::endl;
};
delete raw;
lambda(); // 悬空指针!
值捕获只复制了指针本身,没复制它指向的内存。你想想看,指针的值没变,但指向的内存已经被释放了。这跟引用捕获本质上是一个问题。
15.4 捕获 this 指针:隐蔽的陷阱
这是最容易被忽视的坑。看代码:
class Widget {
int value = 42;
public:
auto getLambda() {
return [this]() {
return value; // 等价于 this->value
};
}
};
void test() {
auto lambda = Widget().getLambda();
std::cout << lambda(); // 崩溃!Widget 对象已销毁
}
[this] 捕获的是当前对象的指针。如果对象被销毁了,这个 this 指针就悬空了。Lambda 里访问 value,实际上是在访问一块已经被释放的内存。
更隐蔽的是 [=] 默认捕获:
class Widget {
int value = 42;
public:
auto getLambda() {
return [=]() { // 你以为捕获了 value 的副本?
return value; // 实际上捕获了 this,访问的是 this->value
};
}
};
很多人以为 [=] 会把所有成员变量都复制一份。错了![=] 在成员函数中只捕获 this 指针。访问成员变量时,走的还是 this->value。
[*this],可以按值捕获当前对象的副本。这样 Lambda 内部操作的是副本,跟原对象无关。
class Widget {
int value = 42;
public:
auto getLambda() {
return [*this]() { // 捕获 this 对象的副本
return value; // 操作的是副本,安全!
};
}
};
我个人建议:如果 Lambda 的生命周期可能超过当前对象,优先用 [*this] 或者显式捕获需要的成员变量副本。
15.5 知识体系图
15.6 避坑指南
总结几条实战经验,都是我踩过的坑:
- 延迟执行的 Lambda,不要用引用捕获。 我曾经把一个引用捕获的 Lambda 塞进线程池,结果线程执行时变量早就没了。排查了一下午。
- 成员函数里用
[=]要小心。 它捕获的是this,不是成员变量的副本。如果 Lambda 生命周期超过对象,用[*this]或者显式捕获成员变量。 - 捕获智能指针时注意循环引用。 如果 Lambda 被
shared_ptr管理,Lambda 内部又捕获了同一个shared_ptr,就会形成循环引用,内存泄漏。 - 能用值捕获就别用引用捕获。 值捕获的语义清晰,不容易出错。性能损失通常可以忽略。
- 移动捕获是 C++14 的好东西。 对于只能移动的类型(如
unique_ptr、fstream),用移动捕获把所有权转给 Lambda。
Lambda 捕获的陷阱,说白了就是生命周期管理的问题。你想想看,C++ 里所有悬空指针、悬空引用的问题,本质上都是“对象已经死了,但还有人拿着它的地址”。Lambda 捕获只是把这个老问题包装了一层新皮而已。
嗯,记住一点:捕获不是复制生命期。你捕获了一个变量,不代表这个变量会一直活着。生命期管理,永远是程序员自己的责任。
公众号:蓝海资料掘金营,微信deep3321