通过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返回后悬挂参考,还是只要最初引用的对象仍在那里,就可以保证可用的捕获的参考。

在此处寻找基于标准的答案:)在实践中效果很好;)

保证代码可以正常工作。

在我们深入研究标准措辞之前:C 委员会的意图是该代码有效。但是,据信这样的措辞对此并不清楚(实际上,对标准后C 14制作的错误将其制作的漏洞打破了使它起作用的微妙安排),因此提出了CWG问题,以澄清事项,澄清问题,现在正在委员会通过。据我所知,没有实施错误。


我想澄清几件事,因为本·沃格特(Ben Voigt)的答案包含一些事实错误,这些错误引起了一些混乱:

  1. "范围"是C 中静态的词汇概念,它描述了程序源代码的一个区域,其中无限制的名称查找将特定名称与声明相关联。它与一生无关。请参阅[BASIC.SCOPE.DECLARATION]/1。
  2. 同样,lambdas的"达到范围"规则是确定允许捕获的句法属性。例如:

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

    n在这里范围内,但是Lambda的到达范围不包括它,因此无法捕获它。换句话说,lambda的到达范围是它可以到达并捕获变量的距离 - 它可以达到封闭(非lambda)函数及其参数,但它无法到达外部,并且捕获出现在外面的声明。

因此,"达到范围"的概念与这个问题无关。被捕获的实体是make_function的参数x,它在lambda的到达范围内。


好吧,让我们看一下这个问题的标准措辞。per [expr.prim.lambda]/17,仅 id-expression s引用复制捕获的实体被转换为lambda关闭类型上的成员访问; id-expression s引用通过参考捕获的实体是单独的,并且仍然表示它们在封闭范围中表示的相同实体。

这立即看起来很糟糕:参考x的寿命已经结束,那么我们如何提及它?好吧,事实证明,几乎没有(见下文)参考其一生之外的参考成员,在这种情况下,课程本身必须在其寿命内,才能有效成员访问表达式)。结果,该标准直到最近才使用其寿命以外的参考文献。

lambda措辞利用了以下事实:在其寿命之外使用参考不会受到惩罚,因此不需要给出任何明确的规则,即通过参考用途捕获的实体的访问 - 这只是意味着您使用该实体;如果是参考,则该名称表示其初始化器。这就是保证直到最近才能工作的方式(包括在C 11和C 14中)。

但是,您不能在其寿命之外提及参考,这不是完全不是。特别是,您可以从其自身的初始化器中,从参考提前的类成员的初始化器中引用它,或者如果它是命名空间 - 距离变量,并且可以从另一个在其之前初始化的全局访问它。引入了CWG问题,以解决该监督,但它通过参考参考文献无意间打破了Lambda捕获的规范。我们应该在C 17艘船之前修复此回归;我已经提出了国家机构的评论,以确保其适当的优先级。

tl; dr; 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表达式都不得在其lambda-Indroducer中具有捕获默认或简单捕获。到达本地lambda表达式的范围是封闭范围的集合,直到并包括 最内置的封闭功能及其参数。

...

所有此类隐式捕获的实体均应在Lambda表达式的到达范围内声明。

...

[注意:如果实体是通过参考隐式或明确捕获的,则在实体生命周期结束后调用相应Lambda-Expression的函数调用操作员可能会导致不确定的行为。 - 终点注]

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

但是!"隐式捕获的实体"必须是"在lambda表达式的到达范围内",并且main()中的i不在范围内。:(除非参数 x算作"在范围内声明",即使实体i本身不在触及范围之外。

这听起来是,与C 中的任何其他位置不同,创建了参考对参考,并且参考的寿命具有意义。

绝对希望看到标准澄清。

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


经过仔细检查,我们还可以想象它可能会破裂。请记住,在X86上,在最终的机器代码中,使用EBP相关地址访问本地变量和功能参数。参数具有正偏移,而当地人为负。(其他体系结构具有不同的寄存器名称,但许多以相同的方式工作。)无论如何,这意味着只能通过捕获EBP的值来逐个捕获。然后,可以通过相对地址再次找到当地人和参数。实际上,我相信我已经听说过lambda实施(用lambdas的语言很久以前C )做到了这一点:捕获定义Lambda的"堆栈框架"。

这意味着当make_function返回并且其堆栈框架消失时,所有访问当地人和参数的能力也将消失。

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

是否未指定是否在通过参考捕获的实体的闭合类型中声明其他未命名的非静态数据成员。

结论:该问题中的代码不能由标准保证,并且有合理的lambdas实现使其破裂。假设它是不可存放的。

最新更新