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

Mag*_*off 59 c++ lambda language-lawyer c++11

考虑一下:

#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();
}
Run Code Online (Sandbox Code Playgroud)

是否保证在5不调用未定义行为的情况下输出该程序?

我理解如果我x通过value([=])捕获它是如何工作的,但我不确定我是否通过引用捕获它来调用未定义的行为.可能是我在make_function返回后最终会有一个悬空引用,或者只要原始引用的对象仍然存在,捕获的引用是否可以保证工作?

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

Ben*_*igt 29

TL; DR:标准不保证问题中的代码,并且有合理的lambdas实现会导致它破坏.假设它是非便携式的而是使用

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

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

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

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

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

...

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

...

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

我们希望发生的是x,作为内部使用make_function,是指imain()(因为这是引用做什么),与实体i通过引用捕捉.由于该实体仍然生活在lambda召唤时,一切都很好.

但!"隐式地捕获实体"必须是"lambda表达式的到达范围内的",并且imain()没有在到达范围.:(除非参数x计为"在达到范围内声明",即使实体i本身在达到范围之外.

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

绝对是我希望看到标准澄清的东西.

同时,TL; DR部分中显示的变体肯定是安全的,因为指针是通过值捕获的(存储在lambda对象本身内),并且它是指向持续通过lambda调用的对象的有效指针.我还希望通过引用捕获实际上最终会存储指针,因此执行此操作不应该有运行时惩罚.


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

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

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

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

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

  • "很可能导致未定义的行为".这是正常的标准吗?:)听起来意外模糊:P (3认同)
  • @dyp:再看看/ 18(或/ 17)."每个id-expression都是由副本捕获的实体的odr-use(3.2),它被转换为对闭包类型的相应未命名数据成员的访问." 它***转换为对闭包类型成员的访问.在此转换之前,它是对原始实体的访问.并且**转换不是为了参考捕获**.这些仍然是在达到范围内对原始实体的访问. (3认同)

Ric*_*ith 28

代码保证有效.

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


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

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

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

    n在这里的范围,但lambda的范围不包括它,因此无法捕获.换句话说,lambda的达到范围是它可以到达并捕获变量的"向上"多远 - 它可以达到封闭(非lambda)函数及其参数,但它无法到达外部和捕获出现在外面的声明.

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


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

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

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

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

  • 我的回答中没有任何事实错误,我提到达到范围是证明捕获的实体是“x”而不是“i”,您在没有证据的情况下声明了这一点。我们似乎同意所有观点,包括“[本来]使它起作用的安排”在 C++14 中被破坏了。很高兴看到 C++17 中的修复,您会注意到我的回答准确地说“绝对是我希望标准澄清的内容。” (2认同)
  • 在[最新的C ++ 17草案](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/n4659.pdf)中,相关部分现在为8.1.5.2(11 ),第 106。 (2认同)