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

4

43

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 happening? 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();
}
Hodgkins answered 1/8, 2023 at 23:26 Comment(6)
Reference to a temporary. Also, thanks for including a nice MCVE.Bolen
In general: put reference inside a class? Prepare for pain. Don't do it.Roseanneroseate
I'd weaken @Roseanneroseate 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…Trill
Man, you people have a lot to unlearn when you encounter real lambdas.Memling
@Memling 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!Peritoneum
Frustratingly, this executes successfully in my environment, even under Valgrind. That's the problem with UB - it can lurk undetected. g++ -fanalyzer misses it, too.Mudlark
P
52

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
};
Permeance answered 1/8, 2023 at 23:35 Comment(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 :-)Alcaic
@Alcaic yeah it was easier to explain like thatPermeance
The language is getting more and more opaque to me. (1) I thought that binding to a const ref always extends the lifetime of temporaries (not only to member references)? (2) Only when using braces? Why? (Actually, I am surprised that ` ref b(3);` compiles at all though, at second glance.)Belisle
@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 :-)Alcaic
@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.Permeance
@GuillaumeRacicot: I'm curious as to whether a shared pointer to the temporary would solve this problem. But, even if it did, I think you'd have to copy the shared pointer (by copy, I mean up the ref-cont rather than copying the shared pointer object itself) within the constructor so you may as well just copy the std::function.Alcaic
@Alcaic the temporary will have to be copied into the dynamic allocation of the shared pointer. This is equivalent to store by value but slower. There's no free lunch. If the temporary itself is a temporary of a shared pointer, you'll have to copy the shared pointer to increment the refcount. Storing a reference to a shared pointer will make a reference to a dead shared pointer value, even if the shared pointer didn't freed the memory, the shared pointer itself will die. There's no difference here, and no free lunch.Permeance
@GuillaumeRacicot I see what you are saying, but it is still strange.Belisle
@Alcaic 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.Belisle
Doh! IT seems to obvious now you say it. Thanks for this answer. What puzzles me is: where is this function created in memory that it can just disappear and cause a crash? Is it on the stack?Hodgkins
@Hodgkins Consider void f(int n) { auto lambda = [&n]() { }; } – every function would need to create a new lambda as each of would require its own reference (the one established with previous call of f invalidated on returning from!). Though there will be only one single piece of code for the lambdas implicit operator(), which would be stored just as any other (member) function.Trill
@Hodgkins 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.Permeance
@Rocketmagnet: A lambda isn't just a function pointer, it's also a (potential) context to support captures. The std::function object holds that info along with an actual pointer to some machine code, and the std::function& is a reference to that object. So actually calling it involves double indirection in the asm.Madelina
In std::function<void (float)>::operator()(float) const: (tailcalled by ExecuteStoredLambda if we make that noinline godbolt.org/z/1EPPrr593), note the call [QWORD [rdi+24]] - a memory-indirect call, loading a new RIP from a member of the std::function object pointed to by the first arg. (Where that rdi arg was loaded by LambdaStore::ExecuteStoredLambda():, from the class member pointed to by its this pointer, which was the arg to that function.)Madelina
M
9

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. This lambda is a temporary object (and is not subject to lifetime extension), so it has a very limited lifespan.

Mouth answered 2/8, 2023 at 2:40 Comment(2)
Thread local objects have limited lifespans, they get destroyed when the thread is destroyed.Nettie
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.Nettie
S
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.

Sherillsherilyn answered 1/8, 2023 at 23:36 Comment(4)
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.Alcaic
@Alcaic C++ also has clearly defined ownership and lifetimes. You just have to know the rules..Sherillsherilyn
Jesper, it was more the "caught at compile time" that I was warming towards :-)Alcaic
@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.Barrick
A
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 &)

Allseed answered 1/8, 2023 at 23:34 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.