Esta pregunta no se puede responder completamente en código. Es posible que pueda escribir código algo "equivalente", pero el estándar no se especifica de esa manera.
Con eso fuera del camino, profundicemos [expr.prim.lambda]
. Lo primero a tener en cuenta es que los constructores solo se mencionan en [expr.prim.lambda.closure]/13
:
El tipo de cierre asociado con una expresión lambda no tiene un constructor predeterminado si la expresión lambda tiene una captura lambda y un constructor predeterminado predeterminado. Tiene un constructor de copia predeterminado y un constructor de movimiento predeterminado ([class.copy.ctor]). Tiene un operador de asignación de copia eliminado si la expresión lambda tiene una captura lambda y los operadores de asignación de copia y movimiento predeterminados de lo contrario ([class.copy.assign]). [ Nota: Estas funciones miembro especiales se definen implícitamente como de costumbre y, por lo tanto, pueden definirse como eliminadas. - nota final ]
Entonces, desde el principio, debe quedar claro que los constructores no están formalmente como se define la captura de objetos. Puede acercarse bastante (vea la respuesta de cppinsights.io), pero los detalles difieren (observe cómo el código en esa respuesta para el caso 4 no se compila).
Estas son las principales cláusulas estándar necesarias para analizar el caso 1:
[expr.prim.lambda.capture]/10
[...]
Para cada entidad capturada por copia, se declara un miembro de datos no estático sin nombre en el tipo de cierre. El orden de declaración de estos miembros no está especificado. El tipo de dicho miembro de datos es el tipo referenciado si la entidad es una referencia a un objeto, una referencia de valor al tipo de función referenciada si la entidad es una referencia a una función, o el tipo de la entidad capturada correspondiente de lo contrario. Un miembro de una unión anónima no será capturado por copia.
[expr.prim.lambda.capture]/11
Cada expresión de identificación dentro de la declaración compuesta de una expresión lambda que es un uso odr de una entidad capturada por copia se transforma en un acceso al miembro de datos correspondiente sin nombre del tipo de cierre. [...]
[expr.prim.lambda.capture]/15
Cuando se evalúa la expresión lambda, las entidades que se capturan mediante copia se utilizan para inicializar directamente cada miembro de datos no estático correspondiente del objeto de cierre resultante, y los miembros de datos no estáticos correspondientes a las capturas de inicio se inicializan como indicado por el inicializador correspondiente (que puede ser una copia o inicialización directa). [...]
Apliquemos esto a su caso 1:
Caso 1: captura por valor / captura predeterminada por valor
int x = 6;
auto lambda = [x]() { std::cout << x << std::endl; };
El tipo de cierre de este lambda tendrá un miembro de datos no estático sin nombre (llamémoslo __x
) de tipo int
(ya x
que no es una referencia ni una función), y los accesos x
dentro del cuerpo lambda se transforman en accesos __x
. Cuando evaluamos la expresión lambda (es decir, cuando asignamos a lambda
), inicializamos directamente __x
con x
.
En resumen, solo se realiza una copia . El constructor del tipo de cierre no está involucrado, y no es posible expresar esto en C ++ "normal" (tenga en cuenta que el tipo de cierre tampoco es un tipo agregado ).
La captura de referencia implica [expr.prim.lambda.capture]/12
:
Una entidad se captura por referencia si se captura implícita o explícitamente pero no se captura por copia. No se especifica si se declaran miembros de datos no estáticos adicionales sin nombre en el tipo de cierre para las entidades capturadas por referencia. [...]
Hay otro párrafo sobre la captura de referencias de referencias, pero no lo estamos haciendo en ninguna parte.
Entonces, para el caso 2:
Caso 2: captura por referencia / captura predeterminada por referencia
int x = 6;
auto lambda = [&x]() { std::cout << x << std::endl; };
No sabemos si un miembro se agrega al tipo de cierre. x
en el cuerpo lambda podría referirse directamente al x
exterior. Esto depende del compilador, y lo hará en alguna forma de lenguaje intermedio (que difiere de compilador a compilador), no en una transformación fuente del código C ++.
Las capturas de Init se detallan en [expr.prim.lambda.capture]/6
:
Una captura inicial se comporta como si declara y captura explícitamente una variable de la forma auto init-capture ;
cuya región declarativa es la declaración compuesta de la expresión lambda, excepto que:
- (6.1) si la captura es por copia (ver a continuación), el miembro de datos no estático declarado para la captura y la variable se tratan como dos formas diferentes de referirse al mismo objeto, que tiene la vida útil de los datos no estáticos miembro, y no se realiza ninguna copia y destrucción adicional, y
- (6.2) si la captura es por referencia, la vida útil de la variable finaliza cuando finaliza la vida útil del objeto de cierre.
Dado eso, veamos el caso 3:
Caso 3: captura inicializada generalizada
auto lambda = [x = 33]() { std::cout << x << std::endl; };
Como se dijo, imagine esto como una variable creada auto x = 33;
y capturada explícitamente por copia. Esta variable solo es "visible" dentro del cuerpo lambda. Como se señaló [expr.prim.lambda.capture]/15
anteriormente, la inicialización del miembro correspondiente del tipo de cierre ( __x
para la posteridad) es realizada por el inicializador dado al evaluar la expresión lambda.
Para evitar dudas: esto no significa que las cosas se inicializan dos veces aquí. El auto x = 33;
es un "como si" heredara la semántica de capturas simples, y la inicialización descrita es una modificación de esa semántica. Solo ocurre una inicialización.
Esto también cubre el caso 4:
auto l = [p = std::move(unique_ptr_var)]() {
// do something with unique_ptr_var
};
El miembro de tipo de cierre se inicializa __p = std::move(unique_ptr_var)
cuando se evalúa la expresión lambda (es decir, cuando l
se asigna a). Los accesos a p
en el cuerpo lambda se transforman en accesos a __p
.
TL; DR: solo se realiza el número mínimo de copias / inicializaciones / movimientos (como cabría esperar / esperar). Supongo que las lambdas no se especifican en términos de una transformación fuente (a diferencia de otro azúcar sintáctico) exactamente porque expresar cosas en términos de constructores requeriría operaciones superfluas.
Espero que esto resuelva los temores expresados en la pregunta :)