C++11
中有两种默认捕获方式:按值或者按引用。按值的默认捕获方式可能会忽悠你,按引用的默认捕获方式可能导致空悬引用。
按引用捕获会导致闭包包含涉及到局部变量的引用,或者涉及到定义lambda
式子的作用域内形参的引用,考虑如下代码:
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( // 危险,对divisor的指涉可能空悬!
[&](int value){ return value % divisor == 0;}
);
}
上述代码随时会出错,lambda
式指涉到局部变量diviisor
的引用,但该变量在addDivisorFilter
返回时即不存在。就算不这样做,换作以显示方式按引用捕获divisor
,问题依旧
filters.emplace_back( // 危险,对divisor的指涉可能空悬!
[&divisor](int value){ return value % divisor == 0;}
解决这个问题的一个办法就是使用按值捕获的方式:
filters.emplace_back(
[=](int value){ return value % divisor == 0;}
对于本例来说足够;但是问题有:按值捕获后一个指针后,在lambda
式子创建的闭包中持有的是这个指针的副本,但是你并无法阻止lambda
以外的代码针对该指针实施delete
操作所导致的指针副本空悬;
考虑如下代码:
class widget {
public:
void addFilter() const;
private:
int divisor;
};
void widget::addFilter() const
{
filters.emplace_back([=](int value){ return value % divisor == 0;});
}
捕获只能针对在创建lambda
表达式的作用域可见的非静态局部变量(包括形参)。在wdiget::addFilter
的函数体内,divisor
并非局部变量,而是widget
类的成员变量。它压根无法捕获。这么一来,如果默认捕获模式消除,代码久无法通过编译:
void widget::addFilter() const
{
filters.emplace_back([](int value){ return value % divisor == 0;}); // 错误!没有可捕获的divisor
}
如果试图显示捕获divisor
,这个捕获语句不能通过编译,因为divisor
既不是局部变量,也不是形参
void widget::addFilter() const
{
filters.emplace_back([divisor](int value){ return value % divisor == 0;}); // 错误!没有可捕获的divisor
}
考虑如下代码:
using FilterContainer = std::vector<std::function<bool(int)>>;
FilterContainer filters;
void doSomeWork() {
auto pw = std::make_unique<Widget>();
pw->addFilter();
// ... Widget被销毁,filter现在持有空悬指针
}
调用doSomeWork
创建了一个筛选函数,依赖于std::make_unique
创建的Widget
对象,该函数被到filters
中,不过当doSomeWork
执行结束后,Widget
对象即被管理着它的生命周期的std::unique_ptr
销毁。从那刻起,filter
中就含一个带有空悬指针的元素。这个问题可以通过將你想捕获的成员变量复制到局部变量中,然后捕获该局部变量解决:
void Widget::addFilter() const {
auto divisorCopy = divisor; // 复制成员变量
filter.emplace_back(
[divsorCopy](int value) // 捕获副本
{ return value % divisorCopy == 0}; // 使用副本
)
}