¿Por qué una lambda tiene un tamaño de 1 byte?

89

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

Puede ejecutarlo aquí: http://fiddle.jyt.io/github/b13f682d1237eb69ebdc60728bb52598

El resultado final es:

17
0x7d90ba8f626f
1

Esto sugiere que el tamaño de mi lambda es 1.

  • ¿Cómo es esto posible?

  • ¿No debería la lambda ser, como mínimo, un puntero a su implementación?

sdgfsdh
fuente
17
se implementa como un objeto de función (a structcon an operator())
george_ptr
14
Y una estructura vacía no puede ser de tamaño 0, por lo tanto, el resultado 1. Intente capturar algo y vea qué sucede con el tamaño.
Mohamad Elghawi
2
¿Por qué una lambda debería ser un puntero? Es un objeto que tiene un operador de llamada.
Kerrek SB
7
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 enlace. Por lo tanto, no es necesario un puntero de tiempo de ejecución en el propio objeto. @KerrekSB No es una suposición poco natural 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
2
@KerrekSB "lo que importa" - ¿en qué sentido? La razón por la que 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 el momento de la compilación / enlace. Esto es lo que el OP parece haber entendido mal. No veo cómo tus comentarios aclaran las cosas.
Kyle Strand

Respuestas:

107

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 lambdafuncionalmente 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 lambdase 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 fpara 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::functionTiene 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()> fcaso, 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 stdalgoritmo (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, fde 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 Ttendrá 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::tupley 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 sizeofde 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!

Yakk - Adam Nevraumont
fuente
6
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 con direcciones de 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 podría cubrir toda la memoria, pero las funciones se ajustan dentro de un solo segmento de 64K ( por lo que un puntero de función tenía solo 16 bits).
Martin Bonner apoya a Monica
1
@martin cierto. Extra ()agregado.
Yakk - Adam Nevraumont
50

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, sizeof()es 1 (no puede ser 0, por razones que se exponen adecuadamente en otra parte ).

Si su lambda capturara algunas variables, serán equivalentes a los miembros de la clase y su sizeof()indicará en consecuencia.

Sam Varshavchik
fuente
3
¿Podría vincular a "otro lugar", lo que explica por qué sizeof()no puede ser 0?
user1717828
26

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á en

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 intmiembro no estático para la captura, su tamaño aumentará a sizeof(int). El tamaño seguirá creciendo a medida que capture más cosas.

(Por favor, tome la analogía de la estructura con un grano de sal. Si bien es una buena forma de razonar sobre cómo funcionan las lambdas internamente, esta no es una traducción literal de lo que hará el compilador)

ComicSansMS
fuente
12

¿No debería la lambda ser, en mínimo, un puntero a su implementación?

No necesariamente. De acuerdo con el estándar, el tamaño de la clase única sin nombre está definido por la implementación . Extracto de [expr.prim.lambda] , C ++ 14 (énfasis mío):

El tipo de expresión lambda (que también es el tipo de objeto de cierre) es un tipo de clase de no unión sin nombre único, llamado 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 más que cambiando :

- el tamaño y / o alineación del tipo de cierre ,

- si el tipo de cierre se puede copiar trivialmente (cláusula 9),

- si el tipo de cierre es una clase de disposición 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 sea fijo. Puede variar entre diferentes implementaciones de compiladores.

legends2k
fuente
¿Estás seguro de que se aplica este bit? Una lambda sin un grupo de captura no es realmente un "cierre". (¿El estándar se refiere a las lambdas de grupo de captura vacía como "cierres" de todos modos?)
Kyle Strand
1
Sí lo hace. Esto es lo que dice el estándar: " La evaluación de una expresión lambda da como resultado un valor temporal prvalue. Este temporal se llama objeto de cierre ", capturando o no, es un objeto de cierre, solo que uno estará vacío de upvalues.
legends2k
No voté en contra, pero posiblemente el votante en contra no cree que esta respuesta sea valiosa porque no explica por qué es posible (desde una perspectiva teórica, no desde una perspectiva de estándares) implementar lambdas sin incluir un puntero en tiempo de ejecución al función llamada-operador. (Vea mi discusión con KerrekSB bajo la pregunta.)
Kyle Strand
7

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 no agregado sin unión sin nombre único, conocido como tipo de cierre , que se declara (a los 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 incluye el carácter &, por ejemplo, [a, b, c]), el tipo de cierre incluye datos no estáticos sin nombre miembros , declarados en orden no especificado, que tienen copias de todas las entidades que fueron capturadas.

Para las entidades que se capturan por referencia (con la captura predeterminada [&] o cuando se usa el carácter &, por ejemplo, [& a, & b, & c]), no se especifica 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.

george_ptr
fuente