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

2021-12-23 00:00:00 lambda language-lawyer c++ 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();
}

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

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

我了解按值 ([=]) 捕获 x 的工作原理,但我不确定是否通过引用捕获来调用未定义的行为.可能是在 make_function 返回后我最终会得到一个悬空引用,还是只要原始引用的对象仍然存在,捕获的引用就可以保证工作?

I understand how it works if I capture x by 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_function returns, or is the captured reference guaranteed to work as long as the originally referenced object is still there?

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

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

推荐答案

保证代码有效.

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

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 2011 was raised to clarify matters, and is making its way through the committee now. As far as I know, no implementation gets this wrong.

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

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

  1. 范围"是 C++ 中的静态词法概念,它描述程序源代码的一个区域,在该区域中,非限定名称查找将特定名称与声明相关联.跟寿命没有关系.请参阅[basic.scope.declarative]/1.
  2. lambda 的到达范围"规则同样是一个句法属性,用于确定何时允许捕获.例如:

  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; };
      // ...

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

n is 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.

所以达到范围"的概念与这个问题无关.被捕获的实体是make_function的参数x,在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.

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

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.

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

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.

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

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).

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

However, it's not quite true 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 2012 was 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.

相关文章