Skip to content

Latest commit

 

History

History
167 lines (126 loc) · 4.06 KB

File metadata and controls

167 lines (126 loc) · 4.06 KB

条款31:避免默认捕获模式

下面的代码:

using FilterContainer =
    std::vector<std::function<bool(int)>>;

FilterContainer filters;
...

void addDivisorFilter()
{
    auto calc1 = computeSomeValue1();
    auto calc2 = computeSomeValue2();

    auto divisor = computeDivisor(calc1, calc2);

    filters.emplace_back(
        [&](int value) { return value % divisor == 0; }
    );
}

lambda表达式按引用捕获的 divisor 会发生引用空悬,函数调用结束后 divisor 生命期结束,lambda表达式被保存进容器中。

如果你知道闭包会被立即使用(如STL算法)并且不会被复制,那么引用生命期会长于闭包,不存在风险。例如,我们的筛选器lambda表达式仅作用于 std::all_of 实参:

template<typename C>
void workWithContainer(const C& container)
{
    auto calc1 = computeSomeValue1();
    auto calc2 = computeSomeValue2();

    auto divisor = computeDivisor(calc1, calc2);

    using ContElemT = typename C::value_type;

    using std::begin;
    using std::end;

    if (std::all_of(
        begin(container), end(container),
        [&](const ContElemT& value)
        { return value % divisor == 0; }
    )) {
        ...
    } else {
        ...
    }
}

从长远观点来看,显式列出lambda表达式所以来的局部变量或形参是更好的软件工程实践。

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

filters.emplace_back(
    [=](int value) { return value % divisor == 0; }
);

但是,如果按值捕获了一个指针,但是指针在lambda表达式外被 delete ,同样也会发生指针空悬的危险行为。

假设 Widget 类可以向筛选器容器中添加条目:

class Widget {
public:
    void addFilter() const;
private:
    int divisor;
};

Widget::addFilter 可能如下定义:

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

但这是十分危险的代码。捕获只能针对在创建lambda表达式的作用域内可见的非静态局部变量(包括形参)。上面的代码隐式按值捕获了裸指针 this ,通过它访问到了 divisor 数据成员。

lambda闭包的存活与它含有其 this 指针副本的 Widget 对象的生命期是保定在一起的。考虑下面的代码:

using FilterContainer =
    std::vector<std::function<bool(int)>>;

FilterContainer filters;

void doSomeWork() 
{
    auto pw = std::make_unique<Widget>();

    pw->addFilter();
    ...
}   // Widget被销毁,filter持有空悬指针

解决这一问题可以通过捕获你想要的成员变量复制到局部变量中,然后捕获该局部变量:

void Widget::addFiler() const 
{
    auto divisorCopy = divisor;

    filters.emplace_back(
        [divisorCopy](int value) 
        { return value % divisorCopy == 0; }
    );
}

不过,既然采用这种方法,那么按值默认捕获也能成功运作:

void Widget::addFiler() const 
{
    auto divisorCopy = divisor;

    filters.emplace_back(
        [=](int value) 
        { return value % divisorCopy == 0; }
    );
}

在C++14中,捕获成员变量的一种更好的方法是使用广义lambda捕获:

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

使用默认捕获的另一个缺点是,在于它似乎表明闭包是自洽的,与闭包外的数据变化绝缘。然而,这条结论是不正确的。lambda表达式不会捕获静态变量,一旦静态变量发生修改,lambda表达式内的静态变量也会发生修改:

void addDivisorFilter()
{
    static auto calc1 = computeSomeValue1();
    static auto calc2 = computeSomeValue2();

    static auto divisor = 
        computeDivisor(calc1, calc2);

    filters.emplace_back(
        [=](int value)  // 不会捕获任何东西
        { return value % divisor == 0; }
    );

    ++divisor;  // 意外修改了divisor
}