Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

C++ Lambda Code Generation with Init Captures in C++ 14

I am trying to understand / clarify the code code that is generated when captures are passed to lambdas especially in generalized init captures added in C++14.

Give the following code samples listed below this is my current understanding of what the compiler will generate.

Case 1: capture by value / default capture by value

int x = 6;
auto lambda = [x]() { std::cout << x << std::endl; };

Would equate to:

class __some_compiler_generated_name {
public:
    __some_compiler_generated_name(int x) : __x{x}{}
    void operator()() const { std::cout << __x << std::endl;}
private:
    int __x;
};

So there are multiple copies, one to copy into the constructor parameter and one to copy into the member, which would be expensive for types like vector etc.

Case 2: capture by reference / default capture by reference

int x = 6;
auto lambda = [&x]() { std::cout << x << std::endl; };

Would equate to:

class __some_compiler_generated_name {
public:
    __some_compiler_generated_name(int& x) : x_{x}{}
    void operator()() const { std::cout << x << std::endl;}
private:
    int& x_;
};

The parameter is a reference and the member is a reference so no copies. Nice for types like vector etc.

Case 3:

Generalised init capture

auto lambda = [x = 33]() { std::cout << x << std::endl; };

My under standing is this is similar to Case 1 in the sense that it is copied into to the member.

My guess is that the compiler generates code similar to...

class __some_compiler_generated_name {
public:
    __some_compiler_generated_name() : __x{33}{}
    void operator()() const { std::cout << __x << std::endl;}
private:
    int __x;
};

Also if I have the following:

auto l = [p = std::move(unique_ptr_var)]() {
 // do something with unique_ptr_var
};

What would the constructor look like? Does it also move it into the member?

like image 264
Blair Davidson Avatar asked Oct 08 '19 12:10

Blair Davidson


3 Answers

There's less of a need to speculate, using cppinsights.io.

Case 1:
Code

#include <memory>

int main() {
    int x = 33;
    auto lambda = [x]() { std::cout << x << std::endl; };
}

Compiler generates

#include <iostream>

int main()
{
  int x = 6;

  class __lambda_5_16
  {
    int x;
    public: 
    inline void operator()() const
    {
      std::cout.operator<<(x).operator<<(std::endl);
    }

    // inline /*constexpr */ __lambda_5_16(const __lambda_5_16 &) = default;
    // inline /*constexpr */ __lambda_5_16(__lambda_5_16 &&) noexcept = default;
    public: __lambda_5_16(int _x)
    : x{_x}
    {}

  };

  __lambda_5_16 lambda = __lambda_5_16(__lambda_5_16{x});
}

Case 2:
Code

#include <iostream>
#include <memory>

int main() {
    int x = 33;
    auto lambda = [&x]() { std::cout << x << std::endl; };
}

Compiler generates

#include <iostream>

int main()
{
  int x = 6;

  class __lambda_5_16
  {
    int & x;
    public: 
    inline void operator()() const
    {
      std::cout.operator<<(x).operator<<(std::endl);
    }

    // inline /*constexpr */ __lambda_5_16(const __lambda_5_16 &) = default;
    // inline /*constexpr */ __lambda_5_16(__lambda_5_16 &&) noexcept = default;
    public: __lambda_5_16(int & _x)
    : x{_x}
    {}

  };

  __lambda_5_16 lambda = __lambda_5_16(__lambda_5_16{x});
}

Case 3:
Code

#include <iostream>

int main() {
    auto lambda = [x = 33]() { std::cout << x << std::endl; };
}

Compiler generates

#include <iostream>

int main()
{

  class __lambda_4_16
  {
    int x;
    public: 
    inline void operator()() const
    {
      std::cout.operator<<(x).operator<<(std::endl);
    }

    // inline /*constexpr */ __lambda_4_16(const __lambda_4_16 &) = default;
    // inline /*constexpr */ __lambda_4_16(__lambda_4_16 &&) noexcept = default;
    public: __lambda_4_16(int _x)
    : x{_x}
    {}

  };

  __lambda_4_16 lambda = __lambda_4_16(__lambda_4_16{33});
}

Case 4 (unofficially):
Code

#include <iostream>
#include <memory>

int main() {
    auto x = std::make_unique<int>(33);
    auto lambda = [x = std::move(x)]() { std::cout << *x << std::endl; };
}

Compiler generates

// EDITED output to minimize horizontal scrolling
#include <iostream>
#include <memory>

int main()
{
  std::unique_ptr<int, std::default_delete<int> > x = 
      std::unique_ptr<int, std::default_delete<int> >(std::make_unique<int>(33));

  class __lambda_6_16
  {
    std::unique_ptr<int, std::default_delete<int> > x;
    public: 
    inline void operator()() const
    {
      std::cout.operator<<(x.operator*()).operator<<(std::endl);
    }

    // inline __lambda_6_16(const __lambda_6_16 &) = delete;
    // inline __lambda_6_16(__lambda_6_16 &&) noexcept = default;
    public: __lambda_6_16(std::unique_ptr<int, std::default_delete<int> > _x)
    : x{_x}
    {}

  };

  __lambda_6_16 lambda = __lambda_6_16(__lambda_6_16{std::unique_ptr<int, 
                                                     std::default_delete<int> >
                                                         (std::move(x))});
}

And I believe this last piece of code answers your question. A move occurs, but not [technically] in the constructor.

Captures themselves aren't const, but you can see that the operator() function is. Naturally, if you need to modify the captures, you mark the lambda as mutable.

like image 71
sweenish Avatar answered Sep 28 '22 00:09

sweenish


Case 1 [x](){}: The generated constructor will accept its argument by possibly const-qualified reference to avoid unnecessary copies:

__some_compiler_generated_name(const int& x) : x_{x}{}

Case 2 [x&](){}: Your assumptions here are correct, x is passed and stored by reference.


Case 3 [x = 33](){}: Again correct, x is initialized by value.


Case 4 [p = std::move(unique_ptr_var)]: The constructor will look like this:

    __some_compiler_generated_name(std::unique_ptr<SomeType>&& x) :
        x_{std::move(x)}{}

so yes, the unique_ptr_var is "moved into" the closure. See also Scott Meyer's Item 32 in Effective Modern C++ ("Use init capture to move objects into closures").

like image 37
lubgr Avatar answered Sep 28 '22 02:09

lubgr


This question cannot be fully answered in code. You might be able to write somewhat "equivalent" code, but the standard is not specified that way.

With that out of the way, let's dive into [expr.prim.lambda]. The first thing to note is that constructors are only mentioned in [expr.prim.lambda.closure]/13:

The closure type associated with a lambda-expression has no default constructor if the lambda-expression has a lambda-capture and a defaulted default constructor otherwise. It has a defaulted copy constructor and a defaulted move constructor ([class.copy.ctor]). It has a deleted copy assignment operator if the lambda-expression has a lambda-capture and defaulted copy and move assignment operators otherwise ([class.copy.assign]). [ Note: These special member functions are implicitly defined as usual, and might therefore be defined as deleted. — end note ]

So right off the bat, it should be clear that constructors are not formally how capturing objects is defined. You can get pretty close (see the cppinsights.io answer), but the details differ (note how the code in that answer for case 4 does not compile).


These are the main standard clauses needed to discuss case 1:

[expr.prim.lambda.capture]/10

[...]
For each entity captured by copy, an unnamed non-static data member is declared in the closure type. The declaration order of these members is unspecified. The type of such a data member is the referenced type if the entity is a reference to an object, an lvalue reference to the referenced function type if the entity is a reference to a function, or the type of the corresponding captured entity otherwise. A member of an anonymous union shall not be captured by copy.

[expr.prim.lambda.capture]/11

Every id-expression within the compound-statement of a lambda-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. [...]

[expr.prim.lambda.capture]/15

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, and the non-static data members corresponding to the init-captures are initialized as indicated by the corresponding initializer (which may be copy- or direct-initialization). [...]

Let's apply this to your case 1:

Case 1: capture by value / default capture by value

int x = 6;
auto lambda = [x]() { std::cout << x << std::endl; };

The closure type of this lambda will have an unnamed non-static data member (let's call it __x) of type int (since x is neither a reference nor a function), and accesses to x within the lambda body are transformed to accesses to __x. When we evaluate the lambda expression (i.e. when assigning to lambda), we direct-initialize __x with x.

In short, only one copy takes place. The constructor of the closure type is not involved, and it is not possible to express this in "normal" C++ (note that the closure type is not an aggregate type either).


Reference capture involve [expr.prim.lambda.capture]/12:

An entity is captured by reference if it is implicitly or explicitly captured but not captured by copy. It is unspecified whether additional unnamed non-static data members are declared in the closure type for entities captured by reference. [...]

There is another paragraph about reference capture of references but we're not doing that anywhere.

So, for case 2:

Case 2: capture by reference / default capture by reference

int x = 6;
auto lambda = [&x]() { std::cout << x << std::endl; };

We don't know whether a member is added to the closure type. x in the lambda body might just directly refer to the x outside. This is up to the compiler to figure out, and it will do this in some form of intermediate language (which differs from compiler to compiler), not a source transformation of the C++ code.


Init captures are detailed in [expr.prim.lambda.capture]/6:

An init-capture behaves as if it declares and explicitly captures a variable of the form auto init-capture ; whose declarative region is the lambda-expression's compound-statement, except that:

  • (6.1) if the capture is by copy (see below), the non-static data member declared for the capture and the variable are treated as two different ways of referring to the same object, which has the lifetime of the non-static data member, and no additional copy and destruction is performed, and
  • (6.2) if the capture is by reference, the variable's lifetime ends when the closure object's lifetime ends.

Given that, let's look at case 3:

Case 3: Generalised init capture

auto lambda = [x = 33]() { std::cout << x << std::endl; };

As stated, imagine this as a variable being created by auto x = 33; and explicitly captured by copy. This variable is only "visible" within the lambda body. As noted in [expr.prim.lambda.capture]/15 earlier, the initialization of the corresponding member of the closure type (__x for posterity) is by the given initializer upon evaluation of the lambda expression.

For the avoidance of doubt: This does not mean things are initialized twice here. The auto x = 33; is an "as if" to inherit the semantics of simple captures, and the described initialization is a modification to those semantics. Only one initialization happens.

This also covers case 4:

auto l = [p = std::move(unique_ptr_var)]() {
  // do something with unique_ptr_var
};

The closure type member is initialized by __p = std::move(unique_ptr_var) when the lambda expression is evaluated (i.e. when l is assigned to). Accesses to p in the lambda body are transformed into accesses to __p.


TL;DR: Only the minimal number of copies/initializations/moves are performed (as one would hope/expect). I would assume that lambdas are not specified in terms of a source transformation (unlike other syntactic sugar) exactly because expressing things in terms of constructors would necessitate superfluous operations.

I hope this settles the fears expressed in the question :)

like image 26
Max Langhof Avatar answered Sep 28 '22 02:09

Max Langhof