How are lambda captures initialized in case of nested lambdas?

Viewed 659

Okay, so this is right off the bat from [expr.prim.lambda]p16 in n3337.pdf. Below code is given as an example:

int a = 1, b = 1, c = 1;
auto m1 = [a, &b, &c]() mutable
{
    auto m2 = [a, b, &c]() mutable
    {
        std::cout << a << b << c;     // Shouldn't this print 113 or 133?
        a = 4; b = 4; c = 4;
    };
    a = 3; b = 3; c = 3;
    m2();
};
a = 2; b = 2; c = 2;
m1();
std::cout << a << b << c;             // Okay, this prints 234

and that it shall generate below output:

123234

However, the way I have understood the text in [expr.prim.lambda] (which is somehow obviously flawed), I feel the output should be 113234, specifically the value of b printed in m2. Below is my understanding/explanation:

When std::cout << a << b << c; is executed inside m2, as per [expr.prim.lambda]p16 (emphasis mine):

If a lambda-expression m2 captures an entity and that entity is captured by an immediately enclosing lambda expression m1, then m2’s capture is transformed as follows:

if m1 captures the entity by copy, m2 captures the corresponding non-static data member of m1’s closure type;

Therefore, the a inside m2 shall capture the member generated to the corresponding a captured in the closure type m1. Since a in m1 captures by copy, and a in m2 also captures by copy, a's value in m2 should be 1.

The standard goes on to say (again, emphasis mine):

if m1 captures the entity by reference, m2 captures the same entity captured by m1.

I believe "same entity" here refers to the entity captured by m1 via reference, and when captured by m2 it shall be - a reference to the same entity if it's a capture by reference, or a copy of it if it's a capture by copy.

Therefore for b in m2 shall refer to the b defined outside both lambdas. The value of b in m2 then should be 1 as b is also captured by copy.

Where am I going wrong? More specifically, when is b inside m2 initialised?

2 Answers

First, note that whether a capture is by copy or by reference depends only on the lambda expression's own lambda-introducer (the initial [] part), per C++11 [expr.prim.lambda] paragraph 14 (or C++17 [expr.prim.lambda.capture] paragraph 10).

The pieces you quoted from C++11 [expr.prim.lambda]/16 (or the same in C++17 [expr.prim.lambda.capture]/13) change only what entity is captured, not the type of the capture. So in the example, the inner lambda used to initialize m2 captures the b from the original definition, by copy.

Then, note C++11 [expr.prim.lambda]/21:

When the lambda-expression is evaluated, the entities that are captured by copy are used to direct-initialize each corresponding non-static data member of the resulting closure object.

(C++17 [expr.prim.lambda.capture]/15 starts out the same, but additional wording is added for the init-capture syntax like [var=init].)

In the example, the inner lambda-expression for initializing m2 is evaluated, and the closure object's member for b is initialized, each time m1.operator() is invoked, not in the order the lambda-expression appears in the code. Since the lambda for m2 captures the original b by copy, it gets the value of that b at the time m1 is called. If m1 were called multiple times, that initial value for b could be different each time.

— if m1 captures the entity by reference, m2 captures the same entity captured by m1.

Yes, so b in m2's capture list captures not the reference itself (the capture of m1, that is), but the object that it points to.

But whether m2 captures b by value or by reference is determined solely by what's written in m2's capture list. There's no & before b, so b is captured by value.

when is b inside m2 initialised?

When control reaches auto m2 = ...;. At that point, the reference to b stored in m1 is examined, and the object it points to is copied into m2.


Here's an easier explanation.

  • When you capture a reference by value, you make a copy of the object that it points to.

  • When you capture a reference by reference, you make a reference to the object that it points to.

Here, "capturing a reference" applies equally well to capturing actual references, and to capturing reference-captures of enclosing lambdas.

Related