Stateless lambdas as static local variable

Viewed 65

I am trying to write a templated wrapper class around a stateless lambda. Something like this:

template <class TFuncOp>
class Adapter
{
public:
    void Op()
    {
        TFuncOp func; // not possible before C++20
        func();
    }
};

Since this isn't possible before default constructible lambdas arrive with C++20, I used this technique to make my class work: Calling a stateless lambda without an instance (only type)

So the final solution looks like this:

template <class TFuncOp>
class Adapter
{
public:
    static TFuncOp GetOpImpl( TFuncOp *pFunc = 0 )
    {
        static TFuncOp func = *pFunc;
        return func;
    }

    void Op()
    {
        GetOpImpl()();
    }
};

template <class TFuncOp>
Adapter<TFuncOp> MakeAdapter(TFuncOp func )
{
    // Removing the line below has no effect.
    //Adapter<TFuncOp>::GetOpImpl( &func );
    return Adapter<TFuncOp>();
}

int main()
{
    auto adapter = MakeAdapter( [] { printf("Hello World !\n"); } );
    adapter.Op();
    return 0;
}

This code works on all major compilers (clang, gcc, msvc). But with one surprising discovery. Initialization (or lack thereof) of the static local instance of the lambda in GetOpImpl() has no effect. It works fine either way.

Can anyone explain how this works? Am I invoking UB if I use the static local instance of the lambda without initializing it?

1 Answers

In any case, accessing a nullptr is never a good idea as it is UB.

But we can see that typical implementations generate code which simply works. I try to explain why:

First, it has nothing to do with lambdas. It is simply the not needed using of a copy constructor on a class which has no data. As you have no data, the generated code will not access the passed object. In your case, you "copy" the object which the pointer TFuncOp *pFunc = 0 points to, which is a nullptr which will crash if the object must be accessed. As there is no data to access, a typical implementation will not genrate any code which will access the nullptr at all. But it is still UB.

The same works with all other types in the same way and has nothing special with a lambda!

struct Empty
{
    void Do() { std::cout << "This works the same way" << std::endl; }
    // int i; // << if you add some data, you get a seg fault
};

int main()
{
    Empty* ptr = nullptr;
    Empty empty = *ptr; // get seg fault here, because default copy constructor access the nullptr, but typically only if copy ctor needs to access!

    empty.Do();
}

And a lambda which has no captured data, is an empty structure with a operator()().

That all is a answer why it seems to work.

Related