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.

Community
  • 1
  • 1

4 Answers4

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.

Dennis Zickefoose
  • 10,791
  • 3
  • 29
  • 38
  • 1
    The thing is, I was not aware this actually worked with named classes either, and it confuses me to no end that it does. –  Oct 29 '11 at 20:56
  • 4
    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. – bames53 Oct 31 '11 at 18:56
  • Normal (allocated-on-the-stack) lifetime, but with the return value optimization? –  Mar 13 '12 at 21:42
  • 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?? – allyourcode Dec 20 '13 at 03:47
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; } );
Eric
  • 95,302
  • 53
  • 242
  • 374
deft_code
  • 57,255
  • 29
  • 141
  • 224
  • 3
    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. –  Oct 31 '11 at 17:24
  • 2
    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. – deft_code Oct 31 '11 at 17:26
  • 1
    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. Thanks – Zebrafish Nov 26 '16 at 05:32
  • 1
    @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` keyword – Eric Jan 06 '17 at 22:55
  • I believe name "call parameter type converter" would be more appropriate than type-eraser... – mip May 19 '22 at 22:56
14

This is:

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

Is equivalent to: (Or can be considered too)

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 it can be correctly copied out of a function.

quamrana
  • 37,849
  • 12
  • 53
  • 71
Martin York
  • 257,169
  • 86
  • 333
  • 562
  • 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. –  Oct 29 '11 at 20:54
  • 2
    @Joe: You basically described your run of the mill type erasure, and that's exactly how it works. – Dennis Zickefoose Oct 29 '11 at 20:56
  • 1
    @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. – Nicol Bolas Oct 29 '11 at 20:57
  • 1
    @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. –  Oct 29 '11 at 21:00
  • 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. – ildjarn Oct 30 '11 at 23:22
  • 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. – allyourcode Dec 20 '13 at 03:42
  • 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. – allyourcode Dec 20 '13 at 03:45
  • @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. – Martin York Dec 20 '13 at 07:17
  • @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. – Martin York Dec 20 '13 at 07:20
  • @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. – allyourcode Dec 25 '13 at 19:36
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.

Daniel Trebbien
  • 38,421
  • 18
  • 121
  • 193
  • Actually, the `std::function` object that is returned by the function holds a _moved_ instance of the lambda function object -- no copies are performed. – ildjarn Oct 30 '11 at 23:21
  • 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)? – Daniel Trebbien Nov 01 '11 at 19:40
  • 3
    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. – ildjarn Nov 01 '11 at 19:43