29

How is it possible that this example works? It prints 6:

#include <iostream> #include <functional> using namespace std; void scopeIt(std::function<int()> &fun) { int val = 6; fun = [=](){return val;}; //<-- this } int main() { std::function<int()> fun; scopeIt(fun); cout << fun(); return 0; } 

Where is the value 6 stored after scopeIt is done being called? If I replace the [=] with a [&], it prints 0 instead of 6.

3
  • 1
    Just a hunch, which is why this isn't posted as an answer: You capture the variable by value, so it's being copied to a datamember of a functor that the compiler generates behind the scenes (at least this is how Scott Meyers explains the implementation of lambda's). If you capture by reference, the member of this functor would refer to something that doesn't exist anymore. Could be UB, I don't know. Commented Jan 15, 2015 at 0:00
  • Some peculiarities about lambda's, as pointed out by Scott Meyers: youtu.be/48kP_Ssg2eY (fast-forward to 16:42) Commented Jan 15, 2015 at 0:32
  • @JorenHeit The link is broken. Commented Nov 11, 2022 at 12:27

4 Answers 4

22

It is stored within the closure, which - in your code - is then stored within std::function<int()> &fun.

A lambda generates what's equivalent to an instance of a compiler generated class.

This code:

[=](){return val;} 

Generates what's effectively equivalent to this... this would be the "closure":

struct UNNAMED_TYPE { UNNAMED_TYPE(int val) : val(val) {} const int val; // Above, your [=] "equals/copy" syntax means "find what variables // are needed by the lambda and copy them into this object" int operator() () const { return val; } // Above, here is the code you provided } (val); // ^^^ note that this DECLARED type is being INSTANTIATED (constructed) too!! 
Sign up to request clarification or add additional context in comments.

8 Comments

So with a slightly better processor, I could make my own lambda macro? That's why I love C++.
Thanks man, i'll accept this because you were first and it explains everything.
@BWG Colombo also mentions why [&] produced strange results, which I skipped over. :)
I think a harder question than how the closure is stored is where it is stored. It looks like cannot be stored on the runtime stack, because the closure is built inside scopeIt but has to survive return from it. So it should be stored in dynamic storage, allocated during the assignment to fun. But then this storage must be freed at the end of main; however that might be separately compiled from scopeIt so cannot know about the structure of UNNAMED_TYPE. How does std::function manage to destroy its target? Is there some polymorphism going on behind the scenes?
@MarcvanLeeuwen you might want to read about type erasure, which is essentially what is going on behind the scenes in std::function. IOW, yes, std::function holds a dynamically allocated object with a virtual destructor that contains. The dynamic allocation of that object can be customized via one of the constructors taking a std::allocator_arg_t: en.cppreference.com/w/cpp/utility/functional/function/function
|
20

Lambdas in C++ are really just "anonymous" struct functors. So when you write this:

int val = 6; fun = [=](){return val;}; 

What the compiler is translating that into is this:

int val = 6; struct __anonymous_struct_line_8 { int val; __anonymous_struct_line_8(int v) : val(v) {} int operator() () const { return val; // returns this->val } }; fun = __anonymous_struct_line_8(val); 

Then, std::function stores that functor via type erasure.

When you use [&] instead of [=], it changes the struct to:

struct __anonymous_struct_line_8 { int& val; // Notice this is a reference now! ... 

So now the object stores a reference to the function's val object, which becomes a dangling (invalid) reference after the function exits (and you get undefined behavior).

1 Comment

Unlike capturing by copy, implementations don't even need to declare data members when capturing by reference ([expr.prim.lambda]/16) as they don't need to offer any guarantees for its lifetime.
8

The so-called closure type (which is the class type of the lambda expression) has members for each captured entity. Those members are objects for capture by value, and references for capture by reference. They are initialized with the captured entities and live independently within the closure object (the particular object of closure type that this lambda designates).

The unnamed member that corresponds to the value capture of val is initialized with val and accessed from the inside of the closure types operator(), which is fine. The closure object may easily have been copied or moved multiple times until that happens, and that's fine too - closure types have implicitly defined move and copy constructors just as normal classes do.
However, when capturing by reference, the lvalue-to-rvalue conversion that is implicitly performed when calling fun in main induces undefined behavior as the object which the reference member referred to has already been destroyed - i.e. we are using a dangling reference.

Comments

3

The value of a lambda expression is an object of class type, and

For each entity captured by copy, an unnamed non-static data member is declared in the closure type.

([expr.prim.lambda]/14 in C++11)

That is, the object created by the lambda

[=](){return val;} 

actually contains a non-static member of int type, whose value is 6, and this object is copied into the std::function object.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.