文章

条款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 进行授权