通过引用在C ++ 11 lambda中捕获参考

考虑这个:

#include <functional> #include <iostream> std::function<void()> make_function(int& x) { return [&]{ std::cout << x << std::endl; }; } int main() { int i = 3; auto f = make_function(i); i = 5; f(); } 

这个程序保证输出5而不会调用未定义的行为?

我知道它是如何工作的,如果我通过值捕获x[=] ),但我不确定是否通过引用捕获它来调用未定义的行为。 难道是在make_function返回之后,我最终会得到一个make_function引用,或者只要最初引用的对象仍然存在,被捕获的引用就能保证工作?

在这里寻找明确的基于标准的答案:) 到目前为止 ,它在实践中运作良好;)

代码是保证工作。

在我们深入研究标准的措辞之前,C ++委员会的意图是这个代码的工作。 然而,现在的措辞在这方面被认为是不够明确的(实际上,对标准后C ++ 14的错误修正打破了使其工作的微妙安排),所以提出了2011年CWG问题来澄清问题,现在正在通过委员会。 据我所知,没有实现得到这个错误。


我想澄清几件事情,因为本·福伊特的答案包含了一些事实错误,这些错误造成了一些混淆:

  1. “范围”是C ++中的一个静态的词汇概念,它描述了程序源代码的一个区域,其中非限定名称查找将特定名称与声明关联。 这与生命无关。 见[basic.scope.declarative] / 1 。
  2. lambdas的“达到范围”规则同样也是一个确定何时允许捕获的语法属性。 例如:

     void f(int n) { struct A { void g() { // reaching scope of lambda starts here [&] { int k = n; }; // ... 

    n在这里的范围,但lambda的到达范围不包括它,所以它不能被捕获。 换句话说,lambda的到达范围是它能够到达并捕获variables多远 – 它可以达到封闭(非lambda)函数及其参数,但是它不能达到这个范围,捕获出现在外面的声明。

所以“达成范围”的概念与这个问题无关。 被捕获的实体是make_function的参数x ,它在lambda范围内。


好的,我们来看看标准在这个问题上的措词。 根据[expr.prim.lambda] / 17,只有指向被拷贝捕获的实体的id-expression被转换成lambda闭包types的成员访问; 引用被引用捕获的实体的id-expression是独立的,并且仍然表示它们将在封闭范围中表示的相同的实体。

这立刻显得很糟糕:参考文献x的一生已经结束了,那么我们该如何参考呢? 那么,事实certificate,几乎(见下文)没有办法在其生命周期之外引用一个引用(你可以看到它的声明,在这种情况下,它是在范围内,因此大概可以使用,或者它是一个类成员,在这种情况下,类本身必须在其生命周期内,以使成员访问expression式有效)。 因此,该标准直到最近才被禁止在其生命周期之外使用。

拉姆达的措辞利用了这样一个事实,即在其生命周期之外使用一个引用没有惩罚,因此不需要明确规定通过引用手段捕获的实体的访问权限 – 这只是意味着你使用实体; 如果它是一个引用,名字表示它的初始化。 这就是如何保证直到最近(包括在C ++ 11和C ++ 14)。

然而,你不能在一生之外提及一个参考, 特别是可以从它自己的初始化程序中,从引用之前的类成员的初始化程序中引用它,或者如果它是一个命名空间范围variables,并且从另一个在之前初始化的全局中访问它。 CWG的问题2012年被引入来解决这个监督,但它无意中打破了通过参考引用lambda捕获的规范。 我们应该在C ++ 17发布前修正这个回归。 我已经提交了国家机构的评论,以确保其适当的优先顺序。

TL; DR:问题中的代码不能被标准保证,并且有合理的lambda函数实现,这会导致它的中断。 假设它是不可移植的,而不是使用

 std::function<void()> make_function(int& x) { const auto px = &x; return [/* = */ px]{ std::cout << *px << std::endl; }; } 

从C ++ 14开始,可以使用初始化捕获来显式使用指针,这会强制为lambda创build一个新的引用variables,而不是重新使用封闭范围中的引用variables:

 std::function<void()> make_function(int& x) { return [&x = x]{ std::cout << x << std::endl; }; } 

乍看之下,似乎应该是安全的,但标准的措辞会引起一些问题:

其最小封闭范围是块范围(3.3.3)的lambdaexpression式是一个本地lambdaexpression式; 任何其他的lambdaexpression式在lambda-introducer中都不应该有capture-default或者simple-capture。 本地lambdaexpression式的扩展范围是包含最内部封闭函数及其参数的封闭范围的集合。

所有这些隐式捕获的实体都应该在lambdaexpression式的范围内进行声明。

[注意:如果实体被引用隐式或明确地捕获,那么在实体生命周期结束之后调用相应lambdaexpression式的函数调用操作符可能会导致未定义的行为。 – 结束注意]

我们期望发生的是,在make_function使用的x指的是main() i (因为这是引用的内容),而实体i是通过引用捕获的。 由于该实体在lambda呼叫时仍然存在,所以一切都很好。

但! “隐式捕获的实体”必须“在lambdaexpression式的范围内”,并且main() i不在触发范围内。 :(除非参数x计入“在范围内声明”,即使实体i本身在范围之外。

这听起来像是, 不同于C ++中的任何其他地方,创build了引用参考,并且引用的生命周期有意义。

当然,我希望看到标准的澄清。

与此同时,TL,DR部分中显示的变体绝对安全,因为指针是通过值(存储在lambda对象本身内部)捕获的,并且是通过lambda调用持续的对象的有效指针。 我也希望通过引用捕获实际上结束了存储一个指针,所以不应该有这样做的运行时间的惩罚。


仔细观察,我们也可以想象它会突破。 请记住,在x86中,在最终的机器代码中,使用EBP相对寻址访问局部variables和函数参数。 参数有一个正的偏移量,而当地人是负的。 (其他体系结构有不同的寄存器名称,但许多工作方式相同。)无论如何,这意味着可以通过仅捕获EBP的值来实现通过引用进行捕获。 然后可以通过相对寻址再次find当地人和参数。 实际上,我相信我听说过lambda实现(在C ++之前就已经有lambdas的语言了),正是这样做的:捕获定义了lambda的“栈帧”。

这意味着当make_function返回并且其堆栈框架消失时,所有访问make_function和参数的能力,甚至是那些引用。

该标准包含以下规则,可能专门用于实现此方法:

未指定是否在引用捕获的实体的封闭types中声明了额外的未命名的非静态数据成员。

结论:标准中没有保证问题中的代码,并且存在导致其中断的lambda的合理实现。 假设它是不可移植的。