sdgfsdh
Estoy trabajando con la memoria de algunas lambdas en C++, pero estoy un poco desconcertado por su tamaño.
Aquí está mi código de prueba:
#include <iostream>
#include <string>
int main()
{
auto f = [](){ return 17; };
std::cout << f() << std::endl;
std::cout << &f << std::endl;
std::cout << sizeof(f) << std::endl;
}
La salida es:
17
0x7d90ba8f626f
1
Esto sugiere que el tamaño de mi lambda es 1.
-
¿Cómo es esto posible?
-
¿No debería ser la lambda, como mínimo, un indicador de su implementación?
Yakk – Adam Nevraumont
La lambda en cuestión en realidad tiene ningún estado.
Examinar:
struct lambda {
auto operator()() const { return 17; }
};
Y si tuviéramos lambda f;
, es una clase vacía. No solo es lo anterior 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 un 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 diferente o lambda!
auto f = [](){ return 17; };
f = [](){ return -42; };
lo anterior es ilegal. no hay lugar en f
Almacenar cual se va a llamar la función, esa información se almacena en el escribe 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 está almacenando la lambda directamente. En ambos casos, f = [](){ return -42; }
es legal, por lo que en estos casos, estamos almacenando cual función que 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 el tamaño de un puntero o más grande, como esperas). std::function
tiene un tamaño mínimo implícito en el estándar (tienen que poder almacenar llamadas “dentro de sí mismos” 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 esa lambda. Esto solo funciona para lambdas sin estado (aquellas con un vacío []
lista de capturas).
En el std::function<int()> f
caso, está creando una clase de borrado de tipo std::function<int()>
instancia 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 pasara una lambda más grande (con más estado), usaría la asignación de almacenamiento dinámico).
Como suposición, algo como esto es probablemente lo que crees que está pasando. Que una lambda es un objeto cuyo tipo se describe por su firma. En C++, se decidió hacer lambdas costo cero abstracciones sobre la implementación del objeto de función manual. Esto le permite pasar una lambda a una std
algoritmo (o similar) y hacer que su contenido sea completamente visible para el compilador cuando crea una instancia de la plantilla de 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 hecho a mano.
Ahora que entiendes que tu f
es de hecho apátrida, debería haber otra pregunta en tu 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 colocados sizeof(T)
aparte.
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 explota este hecho. Así es como funciona:
Como una lambda es equivalente a una clase con operator()
lambdas sobrecargados y sin estado (con un []
lista de captura) son todas clases vacías. Ellos tienen sizeof
de 1
. De hecho, si heredas de ellos (¡lo cual está permitido!), no ocuparán espacio siempre y cuando no cause 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
toy(T &&t):T(std::move
int state = 0;
};
template<class Lambda>
toy<Lambda> make_toy( Lambda const& l ) { return {l}; }
la 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 auto toy = make_toy(blah);
entonces hazlo sizeof(blah)
pero eso es solo ruido). sizeof([]{std::cout << "hello world!\n"; })
es todavía 1
(titulaciones similares).
Si creamos otro tipo de juguete:
template<class T>
struct toy2:T {
toy2(toy2 const&)=default;
toy2(T const&t):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
!
-
Nit: un puntero de función puede ser más pequeño que un vacío*. Dos ejemplos históricos: en primer lugar, máquinas dirigidas a palabras donde sizeof(void*)==sizeof(char*) > sizeof(struct*) == sizeof(int*). (void* y char* necesitan algunos bits adicionales para mantener el desplazamiento dentro de una palabra). En segundo lugar, el modelo de memoria 8086 donde void*/int* era segmento+desplazamiento y podía cubrir toda la memoria, pero las funciones se ajustaban a un solo segmento de 64K ( por lo que un puntero de función tenía solo 16 bits).
– Martin Bonner apoya a Mónica
28 de mayo de 2016 a las 6:22
-
@martin cierto. Extra
()
adicional.– Yakk – Adam Nevraumont
28 de mayo de 2016 a las 9:21
Sam Varshavchik
Una lambda no es un puntero de función.
Una lambda es una instancia de una clase. Su código es aproximadamente equivalente a:
class f_lambda {
public:
auto operator() { return 17; }
};
f_lambda f;
std::cout << f() << std::endl;
std::cout << &f << std::endl;
std::cout << sizeof(f) << std::endl;
La clase interna que representa una lambda no tiene miembros de clase, por lo tanto, su sizeof()
es 1 (no puede ser 0, por razones adecuadamente expuestas en otra parte).
Si su lambda fuera a capturar algunas variables, serán equivalentes a los miembros de la clase, y su sizeof()
indicará en consecuencia.
-
¿Podría vincular a “otro lugar”, lo que explica por qué el
sizeof()
no puede ser 0?– usuario1717828
27 de mayo de 2016 a las 14:18
ComicSansMS
Su compilador traduce más o menos la lambda al siguiente tipo de estructura:
struct _SomeInternalName {
int operator()() { return 17; }
};
int main()
{
_SomeInternalName f;
std::cout << f() << std::endl;
}
Dado que esa estructura no tiene miembros no estáticos, tiene el mismo tamaño que una estructura vacía, que es 1
.
Eso cambia tan pronto como agrega una lista de captura no vacía a su lambda:
int i = 42;
auto f = [i]() { return i; };
que se traducirá a
struct _SomeInternalName {
int i;
_SomeInternalName(int outer_i) : i(outer_i) {}
int operator()() { return i; }
};
int main()
{
int i = 42;
_SomeInternalName f(i);
std::cout << f() << std::endl;
}
Dado que la estructura generada ahora necesita almacenar un no estático int
miembro para la captura, su tamaño crecerá hasta sizeof(int)
. El tamaño seguirá creciendo a medida que capture más cosas.
(Tome la analogía de la estructura con un grano de sal. Si bien es una buena manera de razonar sobre cómo funcionan internamente las lambdas, esta no es una traducción literal de lo que hará el compilador)
leyendas2k
¿No debería ser la lambda, como mínimo, un indicador de su implementación?
No necesariamente. De acuerdo con el estándar, el tamaño de la clase única sin nombre es definido por la implementación. Extracto de [expr.prim.lambda]C++14 (énfasis mío):
El tipo de la expresión lambda (que también es el tipo del objeto de cierre) es un tipo de clase sin unión único y sin nombre, denominado tipo de cierre, cuyas propiedades se describen a continuación.
[ … ]
Una implementación puede definir el tipo de cierre de manera diferente a lo que se describe a continuación siempre que esto no altere el comportamiento observable del programa aparte de cambiar:
— el tamaño y/o la alineación del tipo de cierre,
— si el tipo de cierre es trivialmente copiable (Cláusula 9),
— si el tipo de cierre es una clase de diseño estándar (Cláusula 9), o
— si el tipo de cierre es una clase POD (Cláusula 9)
En su caso, para el compilador que usa, obtiene un tamaño de 1, lo que no significa que esté arreglado. Puede variar entre diferentes implementaciones del compilador.
george_ptr
De http://en.cppreference.com/w/cpp/language/lambda:
La expresión lambda construye un objeto temporal prvalue sin nombre de un tipo de clase único, sin unión, no agregada, sin nombre, conocido como tipo de cierreque se declara (a efectos de ADL) en el ámbito de bloque, ámbito de clase o ámbito de espacio de nombres más pequeño que contiene la expresión lambda.
Si la expresión lambda captura algo por copia (ya sea implícitamente con la cláusula de captura [=] o explícitamente con una captura que no incluya el carácter &, por ejemplo [a, b, c]), el tipo de cierre incluye miembros de datos no estáticos sin nombredeclaradas en orden no especificado, que contienen copias de todas las entidades que fueron así capturadas.
Para las entidades que son capturado por referencia (con la captura por defecto [&] o cuando se usa el carácter &, por ejemplo [&a, &b, &c]), es no especificado si se declaran miembros de datos adicionales en el tipo de cierre
De http://en.cppreference.com/w/cpp/language/sizeof
Cuando se aplica a un tipo de clase vacío, siempre devuelve 1.
se implementa como un objeto de función (un
struct
con unoperator()
)– george_ptr
27 de mayo de 2016 a las 11:02
Y una estructura vacía no puede tener el tamaño 0, por lo tanto, el resultado 1. Intente capturar algo y vea qué sucede con el tamaño.
– Mohamad Elghawi
27 de mayo de 2016 a las 11:03
¿Por qué una lambda debería ser un puntero? Es un objeto que tiene un operador de llamada.
– KerrekSB
27 mayo 2016 a las 11:37
Las lambdas en C++ existen en tiempo de compilación y las invocaciones están vinculadas (o incluso en línea) en tiempo de compilación o vinculación. Por lo tanto, no hay necesidad de un tiempo de ejecución puntero en el propio objeto. @KerrekSB No es una suposición antinatural esperar que una lambda contenga un puntero de función, ya que la mayoría de los lenguajes que implementan lambdas son más dinámicos que C++.
– Kyle Strand
27 mayo 2016 a las 18:44
@KerrekSB “lo que importa”, ¿en qué sentido? los razón un objeto de cierre puede estar vacío (en lugar de contener un puntero de función) es porque la función a llamar se conoce en tiempo de compilación/enlace. Esto es lo que el OP parece haber entendido mal. No veo cómo tus comentarios aclaran las cosas.
– Kyle Strand
27 mayo 2016 a las 20:50