Item 31: Avoid default capture modes

本文探讨了C++11中lambda表达式默认捕获模式的隐患,特别是按值捕获导致的悬挂问题,以及如何处理局部变量、this和静态变量的正确捕获。通过实例分析和解决方案,揭示了按值捕获背后的机制和注意事项。
摘要由CSDN通过智能技术生成

在C++ 11中,lambda表达式的捕获模式默认有两种:by-reference 和 by-value。默认的按引用捕获会导致悬挂引用的问题。虽然默认的按值捕获给人一种安全的感觉,但是也会存在悬挂方面的相关问题。

假设一个lambda表达式捕获了局部变量的引用或者它的形参是一个引用,当这个lambda表达式创建的闭包的生命周期超过局部变量或传参的声明周期,则闭包内的引用就会变成空悬的了。假设我们有一个过滤函数的容器,每个过滤函数接受一个int并返回一个bool值(以表示示传入的值是否满足过滤条件):

using FilterContainer =                         // see Item 9 for
    std::vector<std::function<bool(int)>>;      // "using", Item 2
                                                // for std::function

FilterContainer filters;                        // filtering funcs

我们为其添加一个筛选5的倍数的函数:

filters.emplace_back(                           // see Item 42 for
    [](int value) { return value % 5 == 0; }    // info on
);

我们不想把”5“硬编码进lambda表达式,想在运行时计算出除数。这样的话,代码可能如下:

void addDivisorFilter()
{
    auto calc1 = computeSomeValue1();
    auto calc2 = computeSomeValue2();
    
    auto divisor = computeDivisor(calc1, calc2);
    
    filters.emplace_back(                                   // danger!
        [&](int value) { return value % divisor == 0; }     // ref to
    );                                                      // divisor
}                                                           // will
                                                            // dangle!

这段代码随时可能会有问题。divisor的生命周期,随着addDivisorFilter的返回就结束了。就算换作以显示方式按引用捕获divisor,也会造成同样的问题:

filters.emplace_back(
    [&divisor](int value)                   // danger! ref to
    { return value % divisor == 0; }        // divisor will
); 

解决这个问题的一种方法是对divisor采用默认按值捕获的方式:

filters.emplace_back(                                   // now
    [=](int value) { return value % divisor == 0; }     // divisor
);                                                      // can't
                                                        // dangle

但是,这不可能解决所有问题。当遇到指针的场景时,就不行。也许你会说,我们不用那些个原始指针,都用智能指针。但还是有例外的情况,就是this指针的问题。假设有一个Widget类,代码如下:

class Widget {
public:// ctors, etc.
    void addFilter() const;         // add an entry to filters
private:        
    int divisor;                    // used in Widget's filter
};

Widget::addFilter的实现可能如下:

void Widget::addFilter() const
{
    filters.emplace_back(
        [=](int value) { return value % divisor == 0; }
    );
}

这段代码乍看上去没什么问题。但试问,按值捕获的默认模式会确保divisor被拷贝到该lambda表达式创建的闭包里吗?
捕获只能针对在创建lambda式的作用域中可见的非静态局部变量(包括形参)。在Widget::addFilter函数体内,divisor不是一个局部变量,它是Widget类的成员变量。它根本无法被捕获(后面会提到,其实捕获的是this指针)。如果把默认的捕获模式去掉,代码根本就无法通过编译:

void Widget::addFilter() const
{
    filters.emplace_back(                                   // error!
        [](int value) { return value % divisor == 0; }      // divisor
    );                                                      // not
}                                                           // available

而且,如果试图显示的捕获divisor(无论是按值,还是按引用),也都无法通过编译。还是那个原因,divisor不是局部变量或形参:

void Widget::addFilter() const
{
    filters.emplace_back(
        [divisor](int value)                    // error! no local
        { return value % divisor == 0; }        // divisor to capture
    );
}

那默认按值捕获([=])的模式中为什么能正常使用divisor呢?答案是:this。
每一个非静态成员函数都持有一个this指针,在需要访问类的成员变量时都会用到这个指针。所以对于按值捕获的模式中,从编译器的角度看,代码类似于下:

void Widget::addFilter() const
{
    auto currentObjectPtr = this;
    
    filters.emplace_back(
        [currentObjectPtr](int value)
        { return value % currentObjectPtr->divisor == 0; }
    );
}

所以,闭包中的divisor的声明周期是与this所指的对象的生命周期一致的。我们进一步考虑如下情形:

using FilterContainer = // as before
    std::vector<std::function<bool(int)>>;
    
FilterContainer filters; // as before

void doSomeWork()
{
    auto pw =                           // create Widget; see
        std::make_unique<Widget>();     // Item 21 for
                                        // std::make_unique
                                        
    pw->addFilter();                    // add filter that uses
                                        // Widget::divisor}                                       // destroy Widget; filters
                                        // now holds dangling pointer!

当doSomeWork执行完,pw销毁,其管理的Widget对象也被销毁。从这一刻起,filters中就包含了一个带有悬挂指针的元素。
这个特殊的问题可以通过以下方法来解决:先创建想要捕获的数据成员的本地副本,然后捕获副本:

void Widget::addFilter() const
{
    auto divisorCopy = divisor;                 // copy data member
        
    filters.emplace_back(   
        [divisorCopy](int value)                // capture the copy
        { return value % divisorCopy == 0; }    // use the copy
    );
}

老实说,如果你采用这种方法,默认按值捕获也可以正常工作:

void Widget::addFilter() const
{
    auto divisorCopy = divisor;                 // copy data member
        
    filters.emplace_back(   
        [=](int value)                          // capture the copy
        { return value % divisorCopy == 0; }    // use the copy
    );
}

但是上面这种写法吧,还是把this捕获过来了,还是有可能有其他风险的。
在c++ 14中,更好的捕获数据成员的方法是使用广义的lambda捕获(see Item 32):

void Widget::addFilter() const
{
    filters.emplace_back(                  // C++14:
        [divisor = divisor](int value)     // copy divisor to closure
        { return value % divisor == 0; }   // use the copy
    );
}

对于广义lambda捕获而言,没有默认捕获模式一说。但在C++ 14中,避免使用默认的捕获模式依然使用。

默认按值捕获模式的另外一个缺点是,它似乎表明闭包是自包含的,与闭包外的数据变更是隔绝的。一般来说,这是不对的,因为lambdas可能不仅依赖于局部变量和形参,而且还依赖于静态存储器对象(static storage duration)。这样的对象在全局或命名空间作用域中定义,或者在类、函数或文件中声明为静态。这样的对象可以在lambda中使用,但是它们不能被捕获。使用了默认值捕获模式,会给人一种错觉,认为它们可以被捕获。假设有如下代码:

void addDivisorFilter()
{
    static auto calc1 = computeSomeValue1(); // now static
    static auto calc2 = computeSomeValue2(); // now static
    
    static auto divisor =                    // now static
        computeDivisor(calc1, calc2);    
             
    filters.emplace_back(    
        [=](int value)                       // captures nothing!
        { return value % divisor == 0; }     // refers to above static
    );   
         
    ++divisor;                               // modify divisor
}

如果addDivisorFilter被多次调用,那么每个闭包看到的divisor都是不同的。

Things to Remember

  • 按引用的默认捕获会导致悬挂引用的问题;
  • 按值的默认捕获也会受悬挂指针的影响(尤其是this),并会误导认为认为lambda表达式是自包含的;
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值