1. Lambda表达式基础回顾
在深入探讨捕获列表的陷阱之前,我们先快速回顾下C++11引入的lambda表达式基本语法。一个完整的lambda表达式通常由以下几部分组成:
cpp复制[capture-list](parameters) mutable -> return-type {
// 函数体
}
其中capture-list(捕获列表)决定了哪些外部变量能在lambda体内使用,以及如何捕获它们。常见的捕获方式包括:
[]:不捕获任何外部变量[=]:以值拷贝方式捕获所有外部变量[&]:以引用方式捕获所有外部变量[var]:仅以值拷贝方式捕获特定变量var[&var]:仅以引用方式捕获特定变量var
关键理解:捕获行为发生在lambda表达式定义的位置,而非调用位置。这是后续线程安全问题的根源。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 引用捕获的线程安全隐患
2.1 问题复现场景
考虑以下典型的多线程代码示例:
cpp复制#include <iostream>
#include <thread>
#include <vector>
void start_threads() {
std::vector<std::thread> workers;
for (int i = 0; i < 5; ++i) {
workers.emplace_back([&]() { // 危险!捕获了i的引用
std::cout << "Thread " << i << " is running\n";
});
}
for (auto& worker : workers) {
worker.join();
}
}
int main() {
start_threads();
return 0;
}
这段代码看似合理,但实际上存在严重的未定义行为。问题出在lambda通过[&]捕获了循环变量i的引用,而i在for循环中不断变化。
2.2 问题本质分析
当线程真正开始执行时,for循环可能已经结束,此时:
- 变量i已经超出其生命周期(当start_threads()函数返回时)
- 即使i仍在作用域内,其值可能已被修改为循环结束后的值(在这个例子中是5)
这导致多个线程可能访问:
- 已经销毁的变量(悬垂引用)
- 或者访问到相同的最终值(而非各自期望的i值)
2.3 编译器视角的解释
从编译器实现角度看:
- lambda表达式被编译为一个匿名类
- 捕获的变量成为该类的成员变量
- 对于
[&]捕获,这些成员是引用类型
因此上述代码等价于:
cpp复制class __lambda_1 {
int& i; // 引用成员
public:
void operator()() const {
std::cout << "Thread " << i << " is running\n";
}
};
// 使用时:
__lambda_1 lambda{i}; // 存储的是i的引用
3. 安全使用lambda的解决方案
3.1 显式值捕获特定变量
最直接的修复方式是改为值捕获:
cpp复制workers.emplace_back([i]() { // 安全:创建i的副本
std::cout << "Thread " << i << " is running\n";
});
C++14后还可以使用广义lambda捕获:
cpp复制
