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

时间:2014-01-29 21:28:06

标签: c++ c++11 lambda language-lawyer

考虑一下:

#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返回后最终得到一个悬空引用,或者只要最初引用的对象仍然存在,捕获的引用是否可以保证工作?

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

2 个答案:

答案 0 :(得分:27)

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

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

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

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

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

  

一个lambda表达式,其最小的封闭范围是块范围(3.3.3)是一个局部lambda表达式;任何其他lambda-expression在其lambda-introducer中都不应该有capture-default或simple-capture。 本地lambda表达式的到达范围是一组封闭范围,包括   最里面的封闭函数及其参数。

...

  

所有这些隐式捕获的实体都应在lambda表达式的范围内声明。

...

  

[注意:如果通过引用隐式或显式捕获实体,则在实体的生命周期结束后调用相应lambda-expression的函数调用运算符可能会导致未定义的行为。 - 尾注]

我们期望发生的事情是xmake_function中使用,是指i中的main()(因为这是引用所做的),以及实体通过引用捕获i。由于该实体在lambda通话时仍然存在,所以一切都很好。

但是! “隐式捕获的实体”必须“在lambda表达式的范围内”,i中的main()不在达到范围内。 :(除非参数x计为“在达到范围内声明”,即使实体i本身不在达到范围内。

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

我希望看到标准澄清。

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


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

这意味着当make_function返回并且其堆栈框架消失时,所有访问本地和参数的能力也是如此,即使是那些是参考的也是如此。

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

  

未指定是否在闭包类型中为通过引用捕获的实体声明了其他未命名的非静态数据成员。

结论:标准不保证问题中的代码,并且有合理的lambdas实现会导致它破坏。假设它是不可携带的。

答案 1 :(得分:25)

保证代码有效。

在我们深入研究标准措辞之前:C ++委员会的意图是这个代码有效。然而,目前的措辞被认为是不够明确的(实际上,对标准后C ++ 14的错误修正打破了使其起作用的微妙安排),因此提出了CWG issue 2011以澄清重要的是,现在正在通过委员会。据我所知,没有任何实现可以解决这个问题。

我想澄清一些事情,因为Ben Voigt的答案包含一些造成一些混淆的事实错误:

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

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

    n在此范围内,但lambda的范围不包括它,因此无法捕获。换句话说,达到范围的λ是多远&#34; up&#34;它可以到达并捕获变量 - 它可以达到封闭(非lambda)函数及其参数,但它不能到达外部并捕获出现在外部的声明。

  3. 所以&#34;达到范围的概念&#34;与这个问题无关。被捕获的实体是make_function的参数x,它在lambda的范围内。

    好的,让我们看一下这个问题的标准措辞。 Per [expr.prim.lambda] / 17,只有 id-expression 引用由副本捕获的实体转换为lambda闭包类型的成员访问; id-expression 引用通过引用捕获的实体是单独存在的,并且仍然表示它们在封闭范围中表示的相同实体。

    这看起来很糟糕:参考x的生命周期已经结束,那么我们怎样才能参考呢?好吧,事实证明,几乎(见下文)无法在其生命周期之外引用引用(您可以看到它的声明,在这种情况下,它在范围内,因此可以使用,或者它是一个类成员,在这种情况下,类本身必须在其生命周期内,以使成员访问表达式有效)。因此,该标准直到最近才禁止在其生命周期之外使用参考。

    lambda措辞利用了这样一个事实,即在其生命周期之外使用引用不会受到惩罚,因此不需要为通过引用方式捕获的实体的访问提供任何明确的规则 - 它只是意味着你使用那个实体;如果它是引用,则名称表示其初始化程序。这就是如何保证直到最近(包括在C ++ 11和C ++ 14中)的工作。

    但是,在 之前,您不能在其生命周期之外提及引用;特别是,您可以从它自己的初始化程序中引用它,从引用之前的类成员的初始化程序引用它,或者如果它是名称空间范围变量,并且您从另一个在它之前初始化的全局访问它。引入CWG issue 2012来修复这种疏忽,但它无意中通过引用引用破坏了lambda捕获的规范。我们应该在C ++ 17发布之前修复这个回归;我已经提交了国家机构的评论,以确保它的优先顺序。