Capturing a reference by reference in a C++11 lambda
Asked Answered
B

2

95

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 5 without invoking undefined behavior?

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

Bevan answered 29/1, 2014 at 21:28 Comment(4)
Note that another safe solution to capturing the location of x is: std::function<void()> make_function(int& x) { auto px = &x; return [=](){ std::cout << *px << std::endl; }; }Throes
Yes, that's worth mentioning. Thanks.Bevan
Just updated above comment to show that the parameter can stay a reference. Important thing is to close on a pointer, by value.Throes
Is there really any such thing as a "reference to a reference"? My understanding has always been that any reference is simply to the original instance and not the reference(s) it might have been created from. i.e. Even if there's a chain of references made from other references the interim references can go out of scope without affecting references created from them as long as the original item is still current.Lapierre
P
52

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


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

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

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.


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.

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.

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

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.

Pietra answered 29/8, 2016 at 21:18 Comment(4)
There are no factual errors in my answer, my mention of reaching scope is a proof that the captured entity is x not i, which you state without proof. We appear to agree on all points, including that "the arrangement that [would have] made it work" was broken in C++14. Glad to see a fix in C++17, you'll note that my answer said exactly "Definitely something I would like to see the Standard clarify."Throes
has the CWG2011 issue been vetted in c++17 ?Stand
In the last C++17 draft, the relevant part is now 8.1.5.2(11), p. 106.Antipas
A C++ beginner here: As per the exact wording in the question Is this program guaranteed to output 5 without invoking undefined behavior?, compiler optimization/instruction re-ordering may not guarantee output 5 even if the standard is correct. Since i = 5 appears no-op, compiler may ignore the assignment. Is this understanding correct?Dudek
T
35

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

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:

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

On first glance, it seems that should be 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 scope of a local lambda expression is the set of enclosing scopes up to and including the innermost enclosing function and its parameters.

...

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

...

[ 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 ]

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

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

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.

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.


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.

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

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.

Throes answered 29/1, 2014 at 21:43 Comment(35)
"is likely to result in undefined behavior". Is this normal standard-speak? :) That sounds unexpectedly vague :PBevan
@MagnusHoff: Well, a lambda could have captured something and then end up not actually using it.Throes
Is it even specified what an access to an entity captured by reference means? For entities captures by copy, there's /17, but I can't something similar for references..Mundt
@dyp: I don't see any such thing in /17, maybe we're using different drafts. The /17 I see refers to nested lambdas.Throes
n3485, /17 "Every id-expression that is an odr-use of an entity captured by copy is transformed into an access to the corresponding unnamed data member of the closure type."Mundt
@dyp: Ok that's /18 in both drafts I'm using (n3690 and n3797)Throes
Yes, in the post-n3797 where I also looked there doesn't seem to be an analogous part about entities captured by reference either. (building the latest github version right now..)Mundt
To me, it seems the Standard even fails to define what "capture by reference" means. It only specifies when (under which circumstances) something is captured by reference.Mundt
@dyp: Look at /18 (or /17) again. "Every id-expression that is an odr-use (3.2) of an entity captured by copy is transformed into an access to the corresponding unnamed data member of the closure type." It is transformed to an access to a member of the closure type. Before this transformation, it was an access to the original entity. And the transformation isn't done for capture-by-reference. Those remain as access to the original entity in the reaching scope.Throes
Oops, sry. I shouldn't do this when I'm tired ;)Mundt
@dyp: So what we know is that inside the lambda, variable accesses which haven't been transformed are accesses to the original entity in the reaching scope. How the compiler accomplishes this "capture by reference" is left unrestricted. (/17 or /18 states this explicitly for accesses which are not odr-use, and also do not get transformed)Throes
@dyp: Please note that the fact that normal lookup rules are in effect (prior to the transformation on value-captures) allows lambdas to use names at namespace scope, static members, etc., without needing to capture them.Throes
That seems to imply that "capturing by reference" is just not capturing by copy. Therefore, the lifetime issues are not described in the section about lambda-expressions (except for special rules such as the nested-lambda rule). The lifetime of a reference is equal to its storage duration [basic.stc]/3, so we access an entity (but not an object?) outside its lifetime, correct?Mundt
@dyp: It's really hard to say; the implementation has tremendous freedom in how to implement this case. A high-quality implementation will do exactly the alternate code shown in my comment and answer, a poor-quality implementation might needlessly introduce a second layer of indirection which actually holds the address of the hidden pointer used to implement the reference parameter (this makes more sense if you think about storing EBP for the stack frame of the function where the lambda is created and then finding all captured-by-reference variables from that).Throes
The effect of the extra indirection is that the saved EBP points to a removed stack frame, and that would make this case fail. Solution: just use the code in my answer.Throes
I guess it's not so much a matter of "implementation quality" then, as optimizing for speed (which avoids the extra indirection) vs optimizing for space (which stores only a copy of EBP).Throes
The quotations are missing citations. Anyway: "Unless the parameter x counts as "declared within the reaching scope" even though the entity i itself is outside the reaching scope." n3337 5.1.2/10 describes 'entity' as 'variable or this' and the captured variable is indeed x, not i. Such a capture looks legal to me.Ontina
@bames: The capture is legal but what you capture is a dangling reference, as x no longer exists.Throes
Hm... is there a difference between capturing the reference implicitly ([&]{...}) or explicitly ([&x]{...})? I see that some of your quotations are specifically about implicitly captured entities.Bevan
@BenVoigt Certainly the variable with a reference type is 'captured by reference' but that term isn't defined such that we can reason about what happens when a variable which is captured by reference goes out of scope. C++11 5.1.2/22 is non-normative and I think somewhat confused, referring to the 'lifetime' of a variable, rather than a scope. My conclusion is that the result is undefined behavior simply because the standard fails to define the behavior.Ontina
@barnes53: That is anything but obvious. Nowhere else in C++ is it possible to make a reference (or pointer) to a reference. If you pass a reference to a by-reference parameter, the parameter is bound to the target object, not the reference. If you say &x, you do not get the address of the reference. What is inconsistent, and therefore surprising is that &x can bind to the reference itself and not its target, when that &x appears inside a lambda capture.Throes
In no other case does the lifetime of a reference matter. Sure, references which are member variables can die and become unusable -- but that's because the parent object died and is no longer usable with member access operators . and ->. In a lambda, the reference can be directly accessed without going through a parent object (neither a function nor a call frame is an object in C++ terminology), and so the lifetime of the reference variable itself becomes important.Throes
@dyp: Guess I couldn't make up my mind between [=] and [px]. Either will work.Throes
I do not see your reference-to-reference in the quoted standard, but indeed it should be clarified. The problem seems to be related to creation of a function returning a reference, which can indeed lead to a reference being the outermost entity of the reaching scope. But in that case I would follow the same rule as for lambda enclosing lambdas: "if m1 [external lambda] captures the entity by reference m2 [internal lambda] captures the same entity captured by m1". In the example context the equivalent would be the lambda refering to the same entity as the enclosing function.Antechoir
@kriss: I think you did not read the final section, about practical lambda implementation using EBP (register name varies by architecture, purpose doesn't)Throes
@BenVoigt: I've read it. It merely suggest that the above case may be undefined behavior with some implementations. But it's still not explicitly said. As this use case seems simple enough making it an UB does not look such a good idea. Also the suggested EBP implementation wouldn't be a problem if the function is inlined (it may even allow further optimisations). And if using the most common implementation of reference using a hidden pointer you just have to copy that hidden pointer to have a capture by reference. I see nowhere any hint of reference-to-reference.Antechoir
@kriss: I'm sorry you disagree with the specification. It is very explicit that (1) the captured entity is the reference, not the referent and (2) use of the lambda after the lifetime of the captured entities has ended is not defined. How is "entity captured by reference, and the entity is itself a reference" not a reference to reference? If what you want is a copy of the pointer, there's a syntax for that, which has been part of my answer for the last six months.Throes
(1) It is indeed explicit that the captured entity is the reference, but that does not mean that you take a reference to it (something that is meaningless), but it just means that you take it. In other words that the lambda capture points to the same storage as the initial reference whatever it can be. It's exactly the same thing that instantiation of a local class containing some explicit reference member with x. Lambdas are something syntaxically new but not semantically new, you can do that from ages using a quite verbose syntax.Antechoir
@kriss: No, it's not exactly the same. The compiler can access variables as a group consisting of the call frame of the function where the lambda was created, and your functor created by hand cannot. While a different compiler may choose to create an entirely new reference within the lambda's state object. Both implementations are legal, and using the lambda after the lifetime of captured entities has ended is not.Throes
@kriss: You can disagree whether "reference-to-reference" is a good shorthand for "an entity that has reference type has been implicitly or explicitly captured by reference"... but giving it some other name does not change its semantics.Throes
(2) It is indeed an UB, but it's not stated that way in the standard, the phrasing means that the opposite is defined... if there was a reaching scope containing our variable it would be defined. Indeed we don't have one here, but it's undefined "by default" not explicitly. In many other parts of the standard it says "such behavior is undefined", not here. It would be ridiculous here to leave it undefined explcitely to make room for implementations, because UB is basically useless for everybody. In other words it looks like a minor bug in the standard, not a feature.Antechoir
@kriss: "To make room for implementations" is the reason for most cases of UB. It's obvious that this is and should be undefined behavior for all parameters and locals which aren't of reference type, so I can't see why you think the absence of an exception for reference types is either a bug in the Standard or making lambdas useless. If your lambda is going to live longer than the scope where it is created, then capture by value, not by reference. Simple as that.Throes
making lambda useless is much too strong. It merely affects functions defined at to level returning a lambda and containing reference parameters. If this is not well defined behavior nobody should do that, ever. And a lambda capturing by reference a local variable of the function returning the lambda is obviously not a good idea (the variable would never be defined at call time). The only usefull use case is when the top level function provide a reference parameter that can be captured by lambda.Antechoir
Another interpretation could be that instead of reference-by-reference that particular case implies a dynamic binding of the external reference. It would be the only place where we have that, but at least it's useful and semantic is consistent with a local functor.Antechoir
@kriss: Well, there should be an additional workaround in C++14: You can capture arbitrary expressions... capture by reference of *&parameter_reference should force the lambda state class to contain a reference completely independent of the stack frame.Throes

© 2022 - 2024 — McMap. All rights reserved.