Lambda passed by reference runs when invoked in the constructor, but not when later stored in a data member

Lambda passed by reference runs when invoked in the constructor, but not when later stored in a data member

27

The following C++ code prints 11.1 then crashes. The lambda function seems to be called correctly inside the constructor, but then later, that same function no longer works! Why is this? Does the lambda have a limited lifespan?

#include <functional>
#include <iostream>

class LambdaStore
{
public:
    LambdaStore(const std::function<void(float)>& _fn)
    : fn(_fn)
    {
        fn(11.1f);    // works
    }

    void ExecuteStoredLambda()
    {
        fn(99.9f);    // crashes
    }

private:
    const std::function<void(float)>& fn;
};

int main()
{
    LambdaStore lambdaStore([](float a) { std::cout << a << 'n'; });

    lambdaStore.ExecuteStoredLambda();
}

Share

6

  • 26

    Reference to a temporary. Also, thanks for including a nice MCVE.

    – jxh

    2 days ago

  • 4

    In general: put reference inside a class? Prepare for pain. Don't do it.

    – HTNW

    2 days ago

  • I'd weaken @HTNW just a slight bit: Only do so if you can guarantee the live time of the referenced object exceeding the one of the referencing object and you don't need to ever copy/move any of these two objects – though typically you won't often discover this situation…

    – Aconcagua

    2 days ago

  • Man, you people have a lot to unlearn when you encounter real lambdas.

    – Kaz

    yesterday

  • 3

    @Kaz C++ lambdas are real lambdas, except everything in C++ also has a lifetime that has to be manually managed, and you have very fine-grained control over capture compared to lisp-like lambdas. Remember, you can treat C++ as a pure functional language if you first remap every statement to returning a new program!

    – Yakk – Adam Nevraumont

    17 hours ago

4 Answers
4

Reset to default

Highest score (default)

Trending (recent votes count more)

Date modified (newest first)

Date created (oldest first)

36

You’re not storing a lambda, you’re storing a reference to a std::function.

In fact, that std::function is being created as a temporary when the lambda is implicitly converted to std::function. That std::function temporary dies after the line where the constructor is invoked.

    LambdaStore(const std::function<void(float)>& _fn) // _fn refers to a temporary
    : fn(_fn)
    {
        fn(11.1f);    // works
    } // fn (and _fn) dies

But, even if you would change your class to use the lambda type directly through a template, the lambda itself would die too, but this is true with any C++ type, no matter the type. Consider with ints:

class LambdaStore
{
public:
    LambdaStore(const int& _i)
    : i(_i)
    {
        std::cout << i;    // works
    }

    void ExecuteStoredLambda()
    {
        std::cout << i;    // crashes
    }

private:
    const int& i;
};

void main()
{
    LambdaStore lambdaStore(1); // temporary int created here
    // temporary int gone

    lambdaStore.ExecuteStoredLambda();
}

Temporaries don’t get lifetime extension past the statement they are created for when they are bound to a function parameter.

They do get lifetime extension if it binds directly to a member reference, when using the braces only, though:

struct ref {
    const int& i
};

int main() {
  ref a{3};

  std::cout << a.i; // works

  ref b(3);

  std::cout << b.i; // crashes
}

The solution is obviously to store the std::function by value instead of by reference:

class LambdaStore
{
public:
    LambdaStore(const std::function<void(float)>& _fn)
    : fn(_fn)
    {
        fn(11.1f);    // works
    }

    void ExecuteStoredLambda()
    {
        fn(99.9f);    // will also work
    }

private:
    std::function<void(float)> fn; // no & here
};

Share

14

  • Good answer. I would probably suggest that fn/_fn "dies" at the closing parenthesis of the constructor call rather than the closing brace of the constructor itself but, since not a lot can happen between those two, it's probably irrelevant 🙂

    – paxdiablo

    2 days ago

  • 2

    @Peter, re your Monica-moniker, I think it's probably safe to say that Monica has "left the building", so to speak. There's going to eventually be a whole generation of SO users who don't have the faintest idea who she is 🙂

    – paxdiablo

    2 days ago

  • 2

    @Peter-ReinstateMonica The lifetime of temporary is never extended when bound to a function parameter. That never changed. The temporary will live until the end of the statement, ie until ;. When using () to build a aggregate, the compiler synthesize a constructor function to make it more compatible to types that had constructors before C++20 so behaviour is kept. The syntax of thing(prvalue) never extended lifetimes. Whereas type{prvalue} always did.

    – Guillaume Racicot

    2 days ago

  • 19

    @paxdiablo I would hope that such users go to my profile and educate themselves. I do not consider myself unforgiving, generally, but that incident was unforgivable. I mean, I have exactly zero connection to Monica, I find religion ludicrous, and I still ended up supporting the mod of the Jewish discussion group. I'm still angry thinking about SE's behavior. And whether Monica is around or not: SE Inc. could simply publicly say "we'd welcome you back any time if you ever wish to return". Wouldn't even take an acknowledgement of guilt. But no.

    – Peter – Reinstate Monica

    2 days ago

  • 1

    @Rocketmagnet lambdas (without capture) is emplemented as an empty struct with a operator() defined in it. So it does use a byte on the stack. If you have captures, well, those are implemented as data member of that hidden struct, and those can take more space on the stack. std::function on the other hand will put small and captureless lambdas on the stack, otherwise on the heap.

    – Guillaume Racicot

    yesterday

7

The lambda function

This might be where your understanding went astray. Lambdas are objects with member functions; they are not themselves functions. Their definitions look like function bodies, but that is really the definition of the call operator, operator(), of the object.

A semi-corrected version of your assessment of the scenario:

The lambda object seems to call its operator correctly inside the constructor, but then later, that same object no longer works!

Why only "semi-"corrected? Because inside LambdaStore, you do not access the lambda object directly. Instead, you access it through (a reference to) a std::function object. A more correct version:

The std::function object seems to call its operator correctly inside the constructor, but then later, that same object no longer works!

Maybe this would be clearer if I take the notion of "lambda" out of the picture? Your main function is basically a syntactic shortcut for the following.

struct Functor {
    void operator()(float a) {
        cout << a << endl;
    }
};

int main()
{
    LambdaStore lambdaStore(Functor{});

    lambdaStore.ExecuteStoredLambda();
}

In this version, it should be easier to see that you create a temporary object as the argument to the LambdaStore constructor. (Actually, you create two temporaries — the explicit Functor object and an implicit std::function<void(float)> object.) Then you might note that you store a reference that becomes dangling as soon as the constructor finishes…

Does the lambda have a limited lifespan?

Yes, all objects with non-static (and non-thread) storage duration have limited lifespans in C++.

Share

2

  • Thread local objects have limited lifespans, they get destroyed when the thread is destroyed.

    – Daniel

    3 hours ago

  • Static objects, although specified by c++ to survive until the program terminates, in most operating systems can also have limited lifetimes. If a static object resides in a dynamically loaded module, it will not survive the unloading of the module. Other operating systems ignore module unloading calls, and will not free the memory or call the destructor.

    – Daniel

    3 hours ago

6

Yes, a temporary variable (including a lambda) has a limited life span and a reference is not keeping it alive. You may want to store a copy instead. Your problem would be the same with any other temporary variable (like a int) that you store a reference to. The referenced variable must outlive the reference if it’s to be valid for the lifetime of the reference.

Share

4

  • 1

    And this is why I'm starting to warm to Rust, it has clearly defined and controlled ownership, and violations are caught at compile time.

    – paxdiablo

    2 days ago

  • 1

    @paxdiablo C++ also has clearly defined ownership and lifetimes. You just have to know the rules..

    – Jesper Juhl

    2 days ago

  • 6

    Jesper, it was more the "caught at compile time" that I was warming towards 🙂

    – paxdiablo

    2 days ago

  • @Jesper I've been using C++ on and off since the 90s, but never as a full-time project language, but I didn't catch this one. Even compiling with -Wall -Wextra didn't report issue with that code despite being analyzable that a reference outlives it's value.

    – penguin359

    yesterday

5

In the constructor you are taking a reference to a function; and that is what is being stored. Because the function being passed into the constructor is an inline function, the reference to it is no longer valid by the time the ExecuteStoredLambda() is called. To make it work, pass in a non-inlined function, or better, change the fn member to be an object instance rather than a reference. ie
const std::function<void(float)> fn; (no &)

Share

Your Answer

Draft saved
Draft discarded

Post as a guest

Required, but never shown


By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged

or ask your own question.

Leave a Reply

Your email address will not be published. Required fields are marked *