What is the lifetime of a C++ lambda expression?
Asked Answered
P

4

79

(I have read What is the lifetime of lambda-derived implicit functors in C++? already and it does not answer this question.)

I understand that C++ lambda syntax is just sugar for making an instance of an anonymous class with a call operator and some state, and I understand the lifetime requirements of that state (decided by whether you capture by value of by reference.) But what is the lifetime of the lambda object itself? In the following example, is the std::function instance returned going to be useful?

std::function<int(int)> meta_add(int x) {
    auto add = [x](int y) { return x + y; };
    return add;
}

If it is, how does it work? This seems a bit too much magic to me - I can only imagine it working by std::function copying my whole instance, which could be very heavy depending on what I captured - in the past I've used std::function primarily with bare function pointers, and copying those is quick. It also seems problematic in light of std::function's type erasure.

Prime answered 29/10, 2011 at 20:44 Comment(0)
P
74

The lifetime is exactly what it would be if you replaced your lambda with a hand-rolled functor:

struct lambda {
   lambda(int x) : x(x) { }
   int operator ()(int y) { return x + y; }

private:
   int x;
};

std::function<int(int)> meta_add(int x) {
   lambda add(x);
   return add;
}

The object will be created, local to the meta_add function, then moved [in its entirty, including the value of x] into the return value, then the local instance will go out of scope and be destroyed as normal. But the object returned from the function will remain valid for as long as the std::function object that holds it does. How long that is obviously depends on the calling context.

Periwig answered 29/10, 2011 at 20:54 Comment(4)
The thing is, I was not aware this actually worked with named classes either, and it confuses me to no end that it does.Prime
If you're not familiar with how function objects worked prior to C++11 you should take a look at those, because lambda's are almost nothing but syntax sugar over function objects. Once you understand that it becomes obvious that lambda's have the same value semantics as function objects, and so their lifetime is the same.Ordain
Normal (allocated-on-the-stack) lifetime, but with the return value optimization?Courland
Wouldn't add be copied on return, not moved? Would a real lambda be moved? I don't know any reason why it can't be, but maybe that's not actually how it works??Kan
M
18

It seems you're more confused about std::function than lambdas.

std::function uses a technique called type-erasure. Here's a quick fly by.

class Base
{
  virtual ~Base() {}
  virtual int call( float ) =0;
};

template< typename T>
class Eraser : public Base
{
public:
   Eraser( T t ) : m_t(t) { }
   virtual int call( float f ) override { return m_t(f); }
private:
   T m_t;
};

class Erased
{
public:
   template<typename T>
   Erased( T t ) : m_erased( new Eraser<T>(t) ) { }

   int do_call( float f )
   {
      return m_erased->call( f );
   }
private:
   Base* m_erased;
};

Why would you want to erase the type? Isn't the type we want just int (*)(float)?

What the type erasure allows is Erased can now store any value that is callable like int(float).

int boring( float f);
short interesting( double d );
struct Powerful
{
   int operator() ( float );
};

Erased e_boring( &boring );
Erased e_interesting( &interesting );
Erased e_powerful( Powerful() );
Erased e_useful( []( float f ) { return 42; } );
Mosier answered 31/10, 2011 at 16:40 Comment(5)
In retrospect, I was confused about std::function, because I didn't know it kept ownership of much of anything. I had assumed "wrapping" an instance into a std::function was not valid after that instance left scope. The reason lambdas brought the confusion to a head is because std::function is basically the only way to pass them around (if I had a named type, I'd just return an instance of the named type, and that works rather obviously), and then I had no idea where the instance went.Prime
This is just example code some details are missing. It leaks memory, and it's missing calls to std::move, std::forward. Also std::function generally uses a small object optimization to avoid using the heap when T is small.Mosier
Excuse me, I'm trying to learn what type erasure is, and in your example in class Erased you have m_erased.call( f ). If m_erased is a member pointer how can you do m_erased.call(f)? And I tried to change the dot to an arrow and I think it's trying to access the Base pure virtual function. Is this because it's just an example? I've been staring at it for ten minutes and I think I'm going mad. ThanksFou
@TitoneMaurice: Yep, that should definitely be an ->. Why do you think it will call the base virtual function? Remember that a virtual function is overloaded even if the derived class omits the virtual keywordWinker
I believe name "call parameter type converter" would be more appropriate than type-eraser...Ecumenicity
E
14

This:

[x](int y) { return x + y; };

is equivalent to (or can be considered to be):

struct MyLambda
{
    MyLambda(int x): x(x) {}
    int operator()(int y) const { return x + y; }
private:
    int x;
};

So your object is returning an object that looks just like that. Which has a well defined copy constructor. So it seems very reasonable that it can be correctly copied out of a function.

Ecklund answered 29/10, 2011 at 20:52 Comment(10)
To copy it out of a function would require std::function to know it's type after the std::function has been instantiated. How can it do this? The only trick that comes to mind is a pointer to an instance of a templated class with a virtual function that knows the exact type of the lambda. That seems pretty nasty, and I don't even know if it would really work.Prime
@Joe: You basically described your run of the mill type erasure, and that's exactly how it works.Periwig
@JoeWreschnig: You did not ask about how std::function works; you asked about how lambdas work. std::function is not the same thing; it's just a way to wrap a generic callable in an object.Ygerne
@NicolBolas: Well, I returned through a std::function for a reason, because that's the step that I didn't understand. As Dennis said, this also works for named classes, which I was not aware of - for about the past year (after I started using std::function but before I started using lambdas) I always assumed it wouldn't work.Prime
The lambda will be moved into the std::function<> instance, not copied, so having a well-defined copy constructor is irrelevant -- the move constructor is what's relevant.Cleat
Um, if that were the case, then shouldn't I be able to do (new MyLamba(x))(y) ? I don't think that's possible.Kan
Also, what about move instead of copy? Seems like if the lambda copy captures many values, the difference between move and copy would be significant.Kan
@allyourcode: Comment one. You could do (new MyLambda(x))->operator()(y). What you normally do is MyLambda(x)(y). The trouble with new is that you create a pointer not an object. Pointers don't have methods so you need to de-reference the pointer to a object type.Ecklund
@allyourcode: In most case copy elidtion (sp) would make copy insignificant. But to really answer that you need to be much more specific about the situation that you are considering because it can be an issue.Ecklund
@LokiAstari Ah yes. Not sure why I used new. But that wasn't my point: that constructor does not seem to exist. Why I try to use it, my compiler tells me there are only two constructors, and neither of them take x.Kan
O
6

In the code that you posted:

std::function<int(int)> meta_add(int x) {
    auto add = [x](int y) { return x + y; };
    return add;
}

The std::function<int(int)> object that is returned by the function actually holds a moved instance of the lambda function object that was assigned to local variable add.

When you define a C++11 lambda that captures by-value or by-reference, the C++ compiler automatically generates a unique functional type, an instance of which is constructed when the lambda is called or assigned to a variable. To illustrate, your C++ compiler might generate the following class type for the lambda defined by [x](int y) { return x + y; }:

class __lambda_373s27a
{
    int x;

public:
    __lambda_373s27a(int x_)
        : x(x_)
    {
    }

    int operator()(int y) const {
        return x + y;
    }
};

Then, the meta_add function is essentially equivalent to:

std::function<int(int)> meta_add(int x) {
    __lambda_373s27a add = __lambda_373s27a(x);
    return add;
}

EDIT: By the way, I am not sure if you know this, but this is an example of function currying in C++11.

Onesided answered 29/10, 2011 at 21:9 Comment(3)
Actually, the std::function<int(int)> object that is returned by the function holds a moved instance of the lambda function object -- no copies are performed.Cleat
ildjarn: Wouldn't the meta_add(int) function need to return std::move(add) in order to invoke the move constructor of the functional type (__lambda_373s27a in this case)?Onesided
No, return statements are allowed to implicitly treat the returned value as an rvalue, making it implicitly movable and obviating the need for an explicit return std::move(...); (which prevents RVO/NRVO, actually making return std::move(...); an anti-pattern). So because add is treated as an rvalue in the return statement, the lambda is consequently moved into the std::function<> constructor argument.Cleat

© 2022 - 2024 — McMap. All rights reserved.