在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表达式是自包含的;