条款31:避免使用默认捕获模式
默认捕获容易引发潜在悬挂指针或捕获不明确,安全性和代码可读性差,应显式指定捕获。
条款31:避免使用默认捕获模式
条款31:避免使用默认捕获模式
背景概述
C++11 提供两种默认捕获模式:
[&]
:按引用捕获所有使用到的局部变量。[=]
:按值捕获所有使用到的局部变量。
虽然简洁,但这两种模式都存在 隐含风险,应尽量避免。
按引用默认捕获 [&]
的风险
问题:悬空引用(Dangling Reference)
如果 lambda 捕获了局部变量的引用,而 lambda 的闭包生命周期超过了变量本身,就会造成悬空引用。
示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void addDivisorFilter() {
// 计算第一个参与值,可能是某种配置或动态输入
auto calc1 = computeSomeValue1();
// 计算第二个参与值
auto calc2 = computeSomeValue2();
// 使用两个计算结果生成一个除数(用于过滤判断)
auto divisor = computeDivisor(calc1, calc2);
// 向过滤器容器中添加一个 lambda,用于判断一个整数是否为 divisor 的倍数
filters.emplace_back(
[&](int value) {
// 捕获了 divisor 的引用,但 divisor 是局部变量
// 当 lambda 被 emplace_back 到 filters 后,它会长时间存在
// 而 divisor 的生命周期在本函数返回时结束,因此 lambda 中的引用将悬空
return value % divisor == 0;
}
);
// 本函数返回后 divisor 被销毁,filters 中闭包中的引用变为悬空引用
// 后续使用 filters 容器中的 lambda 将导致未定义行为
}
建议
- 使用显式捕获
[&divisor]
更安全,容易识别依赖。 - 最好不要使用
[&]
,除非 lambda 不会逃出当前作用域。
按值默认捕获 [=]
的误导性
问题 1:未捕获到成员变量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Widget {
public:
void addFilter() const {
filters.emplace_back(
[=](int value) {
// 看似捕获了 divisor,但实际并没有!
// [=] 表示按值捕获所有使用到的局部变量和形参
// 但 divisor 是 Widget 的成员变量,不属于局部变量,也不是形参
// 所以并不会被捕获,真正被捕获的是 this 指针(等效于 [this])
// 实际上等价于:
// [this](int value) { return value % this->divisor == 0; }
return value % divisor == 0;
}
);
// 闭包中保存的是 Widget 的 this 指针副本
// 如果 Widget 实例在 lambda 被调用前已销毁(如通过 unique_ptr 销毁),
// 就会导致闭包内访问悬空的 this 指针,产生未定义行为
}
private:
int divisor; // Widget 的数据成员,用作过滤逻辑的除数
};
这里其实捕获的是 this
指针(即 [=]
等效于 [this]
),不是变量本身。
问题 2:悬空指针
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void doSomeWork() {
// 使用智能指针创建 Widget 实例
auto pw = std::make_unique<Widget>();
// 向 filters 添加一个 lambda(通过 Widget::addFilter)
// Widget::addFilter 中的 lambda 使用了 [=] 捕获,
// 实际捕获的是 this 指针 —— 即指向 Widget 的指针
pw->addFilter();
// filters 现在持有一个闭包,该闭包依赖 Widget 的 this 指针
// 但此时 pw 是局部变量,生命周期只到 doSomeWork 函数结束
}
// 函数结束后,pw 被销毁,Widget 对象也随之析构
// 此时 filters 中的 lambda 仍然保存着 Widget 的 this 指针(现在已悬空)
// 任何对 filters 中 lambda 的调用,都会访问悬空的 this 指针,导致未定义行为
问题 3:误导“闭包是独立的”
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void addDivisorFilter() {
// 使用 static 变量意味着 divisor 拥有静态存储周期
// 它在首次调用该函数时初始化,后续调用时保持其值不变(除非被修改)
static auto divisor = computeDivisor(...);
// 添加一个过滤器 lambda 到 filters 容器中
filters.emplace_back(
[=](int value) {
// 虽然写了 [=](默认按值捕获),但其实并没有捕获任何东西
// 因为 divisor 是 static 变量,而捕获只适用于非 static 的局部变量和形参
// 所以这个 lambda 实际上是直接引用了静态变量 divisor,而不是捕获副本
return value % divisor == 0;
}
);
// 修改 static divisor 的值
++divisor;
// 所有之前添加到 filters 的 lambda 都会使用这个新的 divisor 值
// 因为它们引用的是同一个 static divisor,而不是独立副本
// 这违反了 [=] 所暗示的“按值捕获”语义,会误导读者以为 lambda 是独立的
}
虽然用了 [=]
,但 lambda 实际引用的是静态变量,所有闭包共享。
推荐做法
显式按值捕获成员变量副本(C++11)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void Widget::addFilter() const {
// 将成员变量 divisor 拷贝到一个局部变量中
// 这是关键操作,因为成员变量无法被 lambda 捕获
// 复制一份,确保闭包中不会访问到 Widget 对象本身(避免捕获 this)
auto divisorCopy = divisor;
// 向 filters 容器中添加一个 lambda(函数对象)
filters.emplace_back(
[divisorCopy](int value) {
// 显式按值捕获局部变量 divisorCopy(而不是成员变量)
// lambda 是纯函数行为:接收一个 int,判断它是否能被 divisorCopy 整除
return value % divisorCopy == 0;
}
);
}
- lambda 不依赖 idget 的 this 指针,也就避免了 Widget 被销毁后闭包悬空的问题
- divisorCopy 是局部变量,生命周期在闭包创建时结束,之后的副本保存在闭包中
- 每个添加到 filters 的 lambda 都是独立的,互不影响
使用 C++14 的通用 lambda 捕获
1
2
3
4
5
6
7
8
9
10
11
12
13
14
void Widget::addFilter() const {
// 向 filters 容器添加一个 lambda(类型为 std::function<bool(int)>)
filters.emplace_back(
[divisor = divisor](int value) {
// C++14 通用 lambda 捕获语法:
// - 左边的 `divisor` 是捕获到的闭包内部变量名
// - 右边的 `divisor` 是当前作用域中的表达式,这里是 Widget 的成员变量
// - 本质上相当于:auto divisor = this->divisor;
// 捕获的是成员变量的副本,不会捕获 this 指针,也不会悬空
return value % divisor == 0;
}
);
}
- 相较于 C++11 写法(先复制成员变量,再显式捕获局部变量),这里更简洁
- 每个 lambda 都是独立的,互不影响
- 没有捕获 this,因此 Widget 即使析构,闭包也安全
本文由作者按照 CC BY 4.0 进行授权