Lambdas sans état comme variable locale statique
J'essaie d'écrire une classe wrapper basée sur un modèle autour d'un lambda sans état. Quelque chose comme ça:
template <class TFuncOp>
class Adapter
{
public:
void Op()
{
TFuncOp func; // not possible before C++20
func();
}
};
Comme ce n'est pas possible avant l'arrivée des lambdas constructibles par défaut avec C ++ 20, j'ai utilisé cette technique pour faire fonctionner ma classe: appeler un lambda sans état sans instance (type uniquement)
La solution finale ressemble donc à ceci:
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;
}
Ce code fonctionne sur tous les principaux compilateurs (clang, gcc, msvc). Mais avec une découverte surprenante. L'initialisation (ou son absence) de l'instance locale statique du lambda dans GetOpImpl () n'a aucun effet. Cela fonctionne bien de toute façon.
Quelqu'un peut-il expliquer comment cela fonctionne? Est-ce que j'appelle UB si j'utilise l'instance locale statique du lambda sans l'initialiser?
Réponses
Dans tous les cas, accéder à a nullptrn'est jamais une bonne idée car c'est UB.
Mais nous pouvons voir que les implémentations typiques génèrent du code qui fonctionne simplement. J'essaye d'expliquer pourquoi:
Premièrement, cela n'a rien à voir avec les lambdas. C'est simplement l'utilisation non nécessaire d'un constructeur de copie sur une classe qui n'a pas de données. Comme vous n'avez aucune donnée, le code généré n'accédera pas à l'objet passé. Dans votre cas, vous "copiez" l'objet vers lequel TFuncOp *pFunc = 0pointe le pointeur , qui est un nullptr qui plantera si l'objet doit être accédé. Comme il n'y a pas de données à accéder, une implémentation typique ne génèrera aucun code qui accèdera à nullptr du tout. Mais c'est toujours UB.
La même chose fonctionne avec tous les autres types de la même manière et n'a rien de spécial avec 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();
}
Et un lambda qui n'a pas de données capturées est une structure vide avec un operator()().
Tout cela est une réponse pourquoi cela semble fonctionner.