0

This is follow-up question to Does a C++ lambda have a limited life?.

In my previous question, trying to use a lambda after it had gone out of scope caused a crash. While it shouldn't have been surprising, I think I was using an incorrect mental model of where the lambda was stored, which made me think that it would always exist, even if it was out of scope.

Perhaps it's because I often work with microcontrollers, where code executes from non-volatile FLASH memory, which is quite separate from data memory (e.g. the stack). But I imagined that creating a lambda was equivalent to explicitly writing a stand-alone function, but with a more convenient syntax. I.e:

void SaySomething()
{
    cout << "Hello" << endl;
}

int main()
{
    AcceptFunction([](){cout << "Hello" << endl;});    // I thought that this
    AcceptFunction(SaySomething);                    // was the same as this
}

What I imagined was that the lambda compiles to actual code which exists statically alongside other non-lambda code, and AcceptFunction is passed a pointer to that code. In which case, that code would always exist in memory, even if the lambda was out of scope.

But my program crashes, so this can't be true. So where is that code? Is it on the stack? And if so, would the behaviour be different on an architecture with FLASH memory where code cannot be run from the stack?

Update

Just to note that this question is about where the lambda is physically stored. I am not wishing the behaviour to be different, or for the lambda's destructors not to be called.

Konrad Rudolph
  • 530,221
  • 131
  • 937
  • 1,214
Rocketmagnet
  • 5,656
  • 8
  • 36
  • 47
  • So you would expect the destructors for objects captured by a lambda to never run? Lambdas aren't just code, they're code and values of data objects or references to data objects. – David Schwartz Aug 02 '23 at 09:48
  • @DavidSchwartz - Of course not. – Rocketmagnet Aug 02 '23 at 09:49
  • @463035818_is_not_an_ai - Thanks. Corrected. – Rocketmagnet Aug 02 '23 at 09:49
  • 1
    @Rocketmagnet Once you realize that there *must* be a way to destruct a lambda when it goes out of scope or all kinds of language features won't work, that should be the end of it. A lambda contains more than just code, it contains other dynamic information that *must* be cleaned up at the point the language says it must. – David Schwartz Aug 02 '23 at 09:50
  • A lambda is just an object, and works exactly like all other objects. – molbdnilo Aug 02 '23 at 09:54
  • 2
    btw the answer to your other question already states "but this is true with any C++ type, no matter the type". The same holds here. I think what you need is to get lambdas de-mystified. What came new with lambdas as language features is syntax. But long before c++11 you could define a local type and create an instance of it, just not as compact and not with that fancy syntax. – 463035818_is_not_an_ai Aug 02 '23 at 10:02

2 Answers2

10

Your mental model of what a lambda is is too complicated. Read here to see that a lambda expression does nothing but define an (unnamed) functor type and creates an instance of that type. Its not magic, but merely short hand for code you could write manually. Concerning lifetime and where it is stored there is no difference to a functor you can write yourself:

int main()
{
    struct Hello {
         void operator()() { std::cout << "Hello" << std::endl; }
    };
    AcceptFunction( Hello{} );
}

A class is defined, Hello{} constructs a temporary instance that is passed to AcceptFunction. The temporary gets destroyed at the end of the expression.

Where it is "physically stored" is not easy to answer, because the compiler might optimize away the instance altogether and simply inline std::cout << "Hello" << std::endl; inside AcceptFunction. However, this is still not fundamentally different for lambdas as for any other object. For example when you call foo(42) the 42 is not necessarily stored anywhere.

Jarod42
  • 203,559
  • 14
  • 181
  • 302
463035818_is_not_an_ai
  • 109,796
  • 11
  • 89
  • 185
3

A lambda is an object with an operator()(args...). So if you have an actual compilable version of your source code like this (yes without using using namespace std):

#include <iostream>

template<typename fn_t>
void AcceptFunction(fn_t fn)
{
    fn();
}

void SaySomething()
{
    std::cout << "Hello from function\n";
}

int main()
{
    AcceptFunction([]()
    {
          std::cout << "Hello from lambda\n";
    }); // I thought that this
  
    AcceptFunction(SaySomething); // was the same as this
  
    return 0;
}

Then this (kind of) code gets generated for your lambda:

class __lambda_16_20
{
public:
    constexpr void operator()() const
    {
        std::cout << "Hello from lambda\n";
    }

    using __ptr_type = void (*)();

    inline constexpr operator __ptr_type() const noexcept
    {
        return __invoke;
    }

private:
    static inline constexpr void __invoke()
    {
        __lambda_16_20{}();
    }
};

For more details on what code gets generated (including the code of the AcceptFunction template) see : https://cppinsights.io/s/eb6a9c96 (don't forget to press the "play" button to see the generated intermediate code)

Pepijn Kramer
  • 9,356
  • 2
  • 8
  • 19