我们知道C++11中,lambda有两种默认捕获方式:按值或者按引用。
其中按引用的默认捕获方式存在可能的空悬引用问题,例如在lambda按引用捕获了某局部变量的引用,一旦lambda表达式创建的闭包越过了该局部变量的使命周期,则闭包内的该引用将会空悬。
考虑如下代码,我们有一个元素为筛选器的vector容器,每个筛选器都接受一个int参数,返回值为bool类型,表示传入的值是否满足筛选条件:
using FilterContainer = std::vector<std::function<bool(int)>>;
FilterContainer filters;
void addDivisorFilter(int calc) {
auto divisor = calc;
filters.emplace_back( // 危险,对divisor的指涉可能空悬!
[&divisor](int value){ return value % divisor == 0;}
);
}
上述代码随时会出错,因为该lambda式指涉到了局部变量diviisor的引用,diviisor变量的声明周期仅仅是addDivisorFilter内,当addDivisorFilter()函数返回时,diviisor将被释放,而之后再使用该lambda表达式将会导致未定义的行为。
当然,如果你确定lambda创建的闭包的生命周期,一定小于它所引用的局部变量或形参,那按引用捕获不会产生任何问题。
如果要解决上述问题,一个办法就是使用按值捕获的方式:
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
类的成员变量,理论上它压根无法捕获,而之所以又可以编译通过,是因为lambda捕获了Widget对象的this指针,上面代码用到的divisor实际是this->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}; // 使用副本
)
}
C++14中捕获成员变量有一个更好的方法叫做广义lambda捕获,代码如下:
void Widget::addFilter()const {
filters.emplace_back([divisor = divisor](int value) {
return value % divisor == 0;
});
}