下面的代码:
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
}