Lambdas sin estado como variable local estática
Estoy tratando de escribir una clase contenedora con plantilla alrededor de una lambda sin estado. Algo como esto:
template <class TFuncOp>
class Adapter
{
public:
void Op()
{
TFuncOp func; // not possible before C++20
func();
}
};
Dado que esto no es posible antes de que lleguen las lambdas construibles predeterminadas con C ++ 20, utilicé esta técnica para hacer que mi clase funcione: Llamar a una lambda sin estado sin una instancia (solo tipo)
Entonces, la solución final se ve así:
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;
}
Este código funciona en todos los compiladores principales (clang, gcc, msvc). Pero con un descubrimiento sorprendente. La inicialización (o la falta de ella) de la instancia local estática de lambda en GetOpImpl () no tiene ningún efecto. Funciona bien de cualquier manera.
¿Alguien puede explicar cómo funciona esto? ¿Estoy invocando UB si uso la instancia local estática de lambda sin inicializarla?
Respuestas
En cualquier caso, acceder a a nullptrnunca es una buena idea ya que es UB.
Pero podemos ver que las implementaciones típicas generan código que simplemente funciona. Intento explicar por qué:
Primero, no tiene nada que ver con las lambdas. Es simplemente el uso innecesario de un constructor de copia en una clase que no tiene datos. Como no tiene datos, el código generado no accederá al objeto pasado. En su caso, "copia" el objeto al que TFuncOp *pFunc = 0apunta el puntero , que es un nullptr que se bloqueará si se debe acceder al objeto. Como no hay datos a los que acceder, una implementación típica no generará ningún código que acceda al nullptr en absoluto. Pero sigue siendo UB.
¡Lo mismo funciona con todos los demás tipos de la misma manera y no tiene nada especial con una 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();
}
Y una lambda que no tiene datos capturados, es una estructura vacía con un operator()().
Todo eso es una respuesta por qué parece funcionar.