La lambda en cuestión en realidad no tiene estado .
Examinar:
struct lambda {
auto operator()() const { return 17; }
};
Y si lo tuviéramos lambda f;
, es una clase vacía. No solo lo anterior es lambda
funcionalmente similar a su lambda, ¡es (básicamente) cómo se implementa su lambda! (También necesita una conversión implícita al operador de puntero de función, y el nombre lambda
se reemplazará con algún pseudo-guid generado por el compilador)
En C ++, los objetos no son punteros. Son cosas reales. Solo utilizan el espacio necesario para almacenar los datos en ellos. Un puntero a un objeto puede ser más grande que un objeto.
Si bien puede pensar en esa lambda como un puntero a una función, no lo es. ¡No puede reasignar el auto f = [](){ return 17; };
a una función o lambda diferente!
auto f = [](){ return 17; };
f = [](){ return -42; };
lo anterior es ilegal . No hay espacio f
para almacenar qué función se va a llamar; esa información se almacena en el tipo de f
, no en el valor de f
.
Si hiciste esto:
int(*f)() = [](){ return 17; };
o esto:
std::function<int()> f = [](){ return 17; };
ya no almacena la lambda directamente. En ambos casos, f = [](){ return -42; }
es legal, por lo que en estos casos, almacenamos qué función estamos invocando en el valor de f
. Y sizeof(f)
ya no es 1
, sino más bien sizeof(int(*)())
o más grande (básicamente, tener un tamaño de puntero o más grande, como se espera. std::function
Tiene un tamaño mínimo implícito en el estándar (tienen que ser capaces de almacenar "dentro de sí mismos" los invocables hasta un cierto tamaño) que es al menos tan grande como un puntero de función en la práctica).
En el int(*f)()
caso, está almacenando un puntero de función a una función que se comporta como si llamara a ese lambda. Esto solo funciona para lambdas sin estado (aquellas con una []
lista de captura vacía ).
En el std::function<int()> f
caso, está creando una std::function<int()>
instancia de clase de borrado de tipo que (en este caso) usa la ubicación nueva para almacenar una copia de la lambda de tamaño 1 en un búfer interno (y, si se pasó una lambda más grande (con más estados ), usaría la asignación de montón).
Como conjetura, algo como esto es probablemente lo que crees que está sucediendo. Que una lambda es un objeto cuyo tipo está descrito por su firma. En C ++, se decidió hacer abstracciones de costo cero de lambdas sobre la implementación del objeto de función manual. Esto le permite pasar una lambda a un std
algoritmo (o similar) y hacer que su contenido sea completamente visible para el compilador cuando crea una instancia de la plantilla del algoritmo. Si una lambda tuviera un tipo como std::function<void(int)>
, su contenido no sería completamente visible y un objeto de función hecho a mano podría ser más rápido.
El objetivo de la estandarización de C ++ es la programación de alto nivel con cero gastos generales sobre el código C elaborado a mano.
Ahora que comprende que, f
de hecho, es apátrida, debería haber otra pregunta en su cabeza: la lambda no tiene estado. ¿Por qué no tiene tamaño 0
?
Ahí está la respuesta corta.
Todos los objetos en C ++ deben tener un tamaño mínimo de 1 según el estándar, y dos objetos del mismo tipo no pueden tener la misma dirección. Estos están conectados, porque una matriz de tipo T
tendrá los elementos sizeof(T)
separados.
Ahora bien, como no tiene estado, a veces no puede ocupar espacio. Esto no puede suceder cuando está "solo", pero en algunos contextos puede suceder. std::tuple
y un código de biblioteca similar aprovecha este hecho. Así es como funciona:
Como una lambda es equivalente a una clase con operator()
sobrecarga, las lambdas sin estado (con una []
lista de captura) son todas clases vacías. Tienen sizeof
de 1
. De hecho, si hereda de ellos (¡lo cual está permitido!), No ocuparán espacio siempre que no provoque una colisión de direcciones del mismo tipo . (Esto se conoce como optimización de base vacía).
template<class T>
struct toy:T {
toy(toy const&)=default;
toy(toy &&)=default;
toy(T const&t):T(t) {}
toy(T &&t):T(std::move(t)) {}
int state = 0;
};
template<class Lambda>
toy<Lambda> make_toy( Lambda const& l ) { return {l}; }
el sizeof(make_toy( []{std::cout << "hello world!\n"; } ))
es sizeof(int)
(bueno, lo anterior es ilegal porque no puede crear un lambda en un contexto no evaluado: debe crear un nombre y auto toy = make_toy(blah);
luego hacerlo sizeof(blah)
, pero eso es solo ruido). sizeof([]{std::cout << "hello world!\n"; })
sigue siendo 1
(calificaciones similares).
Si creamos otro tipo de juguete:
template<class T>
struct toy2:T {
toy2(toy2 const&)=default;
toy2(T const&t):T(t), t2(t) {}
T t2;
};
template<class Lambda>
toy2<Lambda> make_toy2( Lambda const& l ) { return {l}; }
esto tiene dos copias de la lambda. Como no pueden compartir la misma dirección, ¡ sizeof(toy2(some_lambda))
es 2
!
struct
con anoperator()
)