Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How are lambda captures initialized in case of nested lambdas?

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?

like image 581
Cheshar Avatar asked May 27 '20 15:05

Cheshar


People also ask

What are captures in lambda?

A capture clause of lambda definition is used to specify which variables are captured and whether they are captured by reference or by value. An empty capture closure [ ], indicates that no variables are used by lambda which means it can only access variables that are local to it.

Are lambda captures Const?

By default, variables are captured by const value . This means when the lambda is created, the lambda captures a constant copy of the outer scope variable, which means that the lambda is not allowed to modify them.

How are lambdas stored?

The Lambda service stores your function code in an internal S3 bucket that's private to your account. Each AWS account is allocated 75 GB of storage in each Region. Code storage includes the total storage used by both Lambda functions and layers.

What is capture in C++ lambda?

Capture clauseA lambda can introduce new variables in its body (in C++14), and it can also access, or capture, variables from the surrounding scope. A lambda begins with the capture clause. It specifies which variables are captured, and whether the capture is by value or by reference.


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.

like image 100
aschepler Avatar answered Oct 25 '22 11:10

aschepler


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

like image 20
HolyBlackCat Avatar answered Oct 25 '22 10:10

HolyBlackCat