对于lambda表达式,避免使用默认捕获模式
个人看法 英文原著中使用的是“avoid default capture modes”,所以我在文中翻译为“避免使用默认捕获模式”,但是我认为把“默认捕获模式”称为“隐式捕获模式”更好,因为作者所指的“默认捕获模式”是指在捕获语句中只出现等号或者引用符号(即“[=]”或“[&]”),而不出现捕获的变量名,但为了符合英文,还是把“default capture”翻译为“默认捕获”。
C++11中有两种默认捕获模式:引用捕获或值捕获。默认的引用捕获模式可能会导致悬挂引用,默认的值捕获模式诱骗你——让你认为你可以免疫刚说的问题(事实上没有免疫),然后它又骗你——让你认为你的闭包是独立的(事实上它们可能不是独立的)。
那就是本条款的总纲。如果你是工程师,你会想要更具体的内容,所以让我们从默认捕获模式的危害开始说起吧。
引用捕获会导致闭包包含一个局部变量的引用或者一个形参的引用(在定义lamda的作用域)。如果一个由lambda创建的闭包的生命期超过了局部变量或者形参的生命期,那么闭包的引用将会空悬。例如,我们有一个容器,它的元素是过滤函数,这种过滤函数接受一个int,返回bool表示传入的值是否可以满足过滤条件:
using FilterContainer = // 关于using,看条款9
std::vector<std::function<bool(int)>>; // 关于std::function,看条款2
FilterContainer filters; // 含有过滤函数的容器
我们可以通过添加一个过滤器,过滤掉5的倍数,像这样:
filters.emplace_back( // 关于emplace_back, 看条款42
[](int value) { return value % 5 == 0; }
);
但是,我们可能需要在运行期间计算被除数,而不是直接把硬编码5写到lambda中,所以添加过滤器的代码可能是这样的:
void addDivisorFilter()
{
auto calc1 = computeSomeValue1();
auto calc2 = computeSomeValue2();
auto divisor = computeDivisor(calc1, calc2);
filters.emplace_back(
[&](int value) { return value % divisor == 0; } // 危险!对divisor的引用会空悬
);
}
这代码有个定时炸弹。lambda引用了局部变量divisor
, 但是局部变量的生命期在addDivisorFilter
返回时终止,也就是在filters.emplace_back
返回之后,所以添加到容器的函数本质上就像是一到达容器就死亡了。使用那个过滤器会产生未定义行为,这实际上是在创建过滤器的时候就决定好的了。
现在呢,如果显式引用捕获divisor
,会存在着同样的问题:
filters.emplace(
[&divisor](int value) // 危险!对divisor的引用依然会空悬
{ return value % divisor == 0; }
);
不过使用显示捕获,很容易就可以看出lambda的活性依赖于divisor
的生命期。而且,写出名字“divisor”会提醒我们,要确保divisor
的生命期至少和lambda闭包一样长。比起用