Lambda senza stato come variabile locale statica

Oct 03 2020

Sto cercando di scrivere una classe wrapper basata su modelli attorno a un lambda senza stato. Qualcosa come questo:

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

Poiché ciò non è possibile prima che i lambda costruibili predefiniti arrivino con C ++ 20, ho usato questa tecnica per far funzionare la mia classe: Chiamare un lambda senza stato senza un'istanza (solo tipo)

Quindi la soluzione finale è simile a questa:

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;
}

Questo codice funziona su tutti i principali compilatori (clang, gcc, msvc). Ma con una scoperta sorprendente. L'inizializzazione (o la sua mancanza) dell'istanza locale statica di lambda in GetOpImpl () non ha alcun effetto. Funziona bene in ogni caso.

Qualcuno può spiegare come funziona? Sto invocando UB se utilizzo l'istanza locale statica di lambda senza inizializzarla?

Risposte

2 Klaus Oct 03 2020 at 05:54

In ogni caso, accedere a a nullptrnon è mai una buona idea in quanto è UB.

Ma possiamo vedere che le implementazioni tipiche generano codice che funziona semplicemente. Provo a spiegare perché:

Innanzitutto, non ha nulla a che fare con i lambda. È semplicemente l'uso non necessario di un costruttore di copia su una classe che non ha dati. Poiché non hai dati, il codice generato non accederà all'oggetto passato. Nel tuo caso, "copi" l'oggetto a cui TFuncOp *pFunc = 0punta il puntatore , che è un nullptr che andrà in crash se è necessario accedere all'oggetto. Poiché non ci sono dati a cui accedere, un'implementazione tipica non genererà alcun codice che accederà affatto a nullptr. Ma è ancora UB.

Lo stesso funziona con tutti gli altri tipi allo stesso modo e non ha niente di speciale con un 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();
}

E un lambda che non ha dati acquisiti, è una struttura vuota con estensione operator()().

Questa è tutta una risposta al motivo per cui sembra funzionare.