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

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/21443023/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-27 23:39:14  来源:igfitidea点击:

Capturing a reference by reference in a C++11 lambda

c++c++11lambdalanguage-lawyer

提问by Magnus Hoff

Consider this:

考虑一下:

#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();
}

Is this program guaranteed to output 5without invoking undefined behavior?

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

I understand how it works if I capture xby value ([=]), but I am not sure if I am invoking undefined behavior by capturing it by reference. Could it be that I will end up with a dangling reference after make_functionreturns, or is the captured reference guaranteed to work as long as the originally referenced object is still there?

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

Looking for definitive standards-based answers here :) It works well enough in practice so far;)

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

采纳答案by Richard Smith

The code is guaranteed to work.

代码保证有效。

Before we delve into the standards wording: it's the C++ committee's intent that this code works. However, the wording as it stands was believed to be insufficiently clear on this (and indeed, bugfixes made to the standard post-C++14 broke the delicate arrangement that made it work), so CWG issue 2011was raised to clarify matters, and is making its way through the committee now. As far as I know, no implementation gets this wrong.

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



I'd like to clarify a couple of things, because Ben Voigt's answer contains some factual errors that are creating some confusion:

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

  1. "Scope" is a static, lexical notion in C++, that describes a region of the program source code in which unqualified name lookup associates a particular name with a declaration. It has nothing to do with lifetime. See [basic.scope.declarative]/1.
  2. The "reaching scope" rules for lambdas are, likewise, a syntactic property that determine when capture is permitted. For example:

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

    nis in scope here, but the reaching scope of the lambda does not include it, so it cannot be captured. Put another way, the reaching scope of the lambda is how far "up" it can reach and capture variables -- it can reach up to the enclosing (non-lambda) function and its parameters, but it can't reach outside that and capture declarations that appear outside.

  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 的到达范围是它可以“向上”到达和捕获变量的范围——它可以到达封闭的(非 lambda)函数及其参数,但它不能到达外部并且捕获出现在外面的声明。

So the notion of "reaching scope" is irrelevant to this question. The entity being captured is make_function's parameter x, which is within the reaching scope of the lambda.

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



OK, so let's look at the standard's wording on this issue. Per [expr.prim.lambda]/17, only id-expressions referring to entities captured by copy are transformed into a member access on the lambda closure type; id-expressions referring to entities captured by reference are left alone, and still denote the same entity they would have denoted in the enclosing scope.

好的,让我们看看标准在这个问题上的措辞。根据 [expr.prim.lambda]/17,只有引用复制捕获的实体的id-expression被转换为 lambda 闭包类型的成员访问;引用通过引用捕获的实体的id-expressions 被保留下来,并且仍然表示它们在封闭范围中表示的相同实体。

This immediately seems bad: the reference x's lifetime has ended, so how can we refer to it? Well, it turns out that there is almost (see below) no way to refer to a reference outside its lifetime (you can either see a declaration of it, in which case it's in scope and thus presumably OK to use, or it's a class member, in which case the class itself must be within its lifetime for the member access expression to be valid). As a result, the standard did not have any prohibitions on using a reference outside its lifetime until very recently.

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

The lambda wording took advantage of the fact that there is no penalty for using a reference outside its lifetime, and so didn't need to give any explicit rules for what access to an entity captured by reference means -- it just means you use that entity; if it's a reference, the name denotes its initializer. And that's how this was guaranteed to work up until very recently (including in C++11 and C++14).

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

However, it's not quitetrue that you can't mention a reference outside its lifetime; in particular, you can reference it from within its own initializer, from the initializer of a class member earlier than the reference, or if it is a namespace-scope variable and you access it from another global that is initialized before it is. CWG issue 2012was introduced to fix that oversight, but it inadvertantly broke the specification for lambda capture by reference of references. We should get this regression fixed before C++17 ships; I've filed a National Body comment to make sure it's suitably prioritized.

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

回答by Ben Voigt

TL;DR: The code in the question is not guaranteed by the Standard, and there are reasonable implementations of lambdas which cause it to break. Assume it is non-portable and instead use

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

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

Beginning in C++14, you can do away with explicit use of a pointer using an initialized capture, which forces a new reference variable to be created for the lambda, instead of reusing the one in the enclosing scope:

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

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


On first glance, it seems that shouldbe safe, but the wording of the Standard causes a bit of a problem:

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

A lambda-expression whose smallest enclosing scope is a block scope (3.3.3) is a local lambda expression; any other lambda-expression shall not have a capture-default or simple-capture in its lambda-introducer. The reaching scopeof a local lambda expression is the set of enclosing scopes up to and including the innermost enclosing function and its parameters.

最小封闭范围是块范围 (3.3.3) 的 lambda 表达式是本地 lambda 表达式;任何其他 lambda 表达式在其 lambda 引入器中不应具有捕获默认或简单捕获。局部 lambda 表达式到达范围是一组封闭范围,直到并包括最里面的封闭函数及其参数。

...

...

All such implicitly captured entities shall be declared within the reaching scope of the lambda expression.

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

...

...

[ Note: If an entity is implicitly or explicitly captured by reference, invoking the function call operator of the corresponding lambda-expression after the lifetime of the entity has ended is likely to result in undefined behavior. — end note ]

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

What we expect to happen is that x, as used inside make_function, refers to iin main()(since that is what references do), and the entity iis captured by reference. Since that entity still lives at the time of the lambda call, everything is good.

我们期望发生的是x,正如在 内使用的那样make_function,引用iin main()(因为这就是引用的作用),并且实体i是通过引用捕获的。由于该实体在 lambda 调用时仍然存在,所以一切都很好。

But! "implicitly captured entities" must be "within the reaching scope of the lambda expression", and iin main()is not in the reaching scope. :( Unless the parameter xcounts as "declared within the reaching scope" even though the entity iitself is outside the reaching scope.

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

What this sounds like is that, unlike any other place in C++, a reference-to-reference is created, and the lifetime of a reference has meaning.

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

Definitely something I would like to see the Standard clarify.

绝对是我希望标准澄清的事情。

In the meantime, the variant shown in the TL;DR section is definitely safe because the pointer is captured by value (stored inside the lambda object itself), and it is a valid pointer to an object which lasts through the call of the lambda. I would also expect that capturing by reference actually ends up storing a pointer anyway, so there should be no runtime penalty for doing this.

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



On closer inspection, we also imagine that it could break. Remember that on x86, in the final machine code, both local variables and function parameters are accessed using EBP-relative addressing. Parameters have a positive offset, while locals are negative. (Other architectures have different register names but many work in the same way.) Anyway, this means that capture-by-reference can be implemented by capturing only the value of EBP. Then locals and parameters alike can again be found via relative addressing. And in fact I believe I've heard of lambda implementations (in languages which had lambdas long before C++) doing exactly this: capturing the "stack frame" where the lambda was defined.

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

What this implies is that when make_functionreturns and its stack frame goes away, so does all ability to access locals AND parameters, even those which are references.

这意味着当make_function返回及其堆栈帧消失时,所有访问局部变量和参数的能力也会消失,即使是那些引用。

And the Standard contains the following rule, likely specifically to enable this approach:

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

It is unspecified whether additional unnamed non-static data members are declared in the closure type for entities captured by reference.

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

Conclusion: The code in the question is not guaranteed by the Standard, and there are reasonable implementations of lambdas which cause it to break. Assume it is non-portable.

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