Estoy tratando de entender / aclarar el código de código que se genera cuando las capturas se pasan a lambdas, especialmente en las capturas de inicio generalizadas agregadas en C ++ 14.
Dé los siguientes ejemplos de código enumerados a continuación, esta es mi comprensión actual de lo que generará el compilador.
Caso 1: captura por valor / captura predeterminada por valor
int x = 6;
auto lambda = [x]() { std::cout << x << std::endl; };
Equivaldría a:
class __some_compiler_generated_name {
public:
__some_compiler_generated_name(int x) : __x{x}{}
void operator()() const { std::cout << __x << std::endl;}
private:
int __x;
};
Por lo tanto, hay varias copias, una para copiar en el parámetro constructor y otra para copiar en el miembro, lo que sería costoso para tipos como vector, etc.
Caso 2: captura por referencia / captura predeterminada por referencia
int x = 6;
auto lambda = [&x]() { std::cout << x << std::endl; };
Equivaldría a:
class __some_compiler_generated_name {
public:
__some_compiler_generated_name(int& x) : x_{x}{}
void operator()() const { std::cout << x << std::endl;}
private:
int& x_;
};
El parámetro es una referencia y el miembro es una referencia, por lo que no hay copias. Agradable para tipos como vector, etc.
Caso 3:
Captura inicializada generalizada
auto lambda = [x = 33]() { std::cout << x << std::endl; };
Mi comprensión es que esto es similar al Caso 1 en el sentido de que se copia en el miembro.
Supongo que el compilador genera código similar a ...
class __some_compiler_generated_name {
public:
__some_compiler_generated_name() : __x{33}{}
void operator()() const { std::cout << __x << std::endl;}
private:
int __x;
};
También si tengo lo siguiente:
auto l = [p = std::move(unique_ptr_var)]() {
// do something with unique_ptr_var
};
¿Cómo sería el constructor? ¿También lo mueve al miembro?
Respuestas:
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
: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
[expr.prim.lambda.capture]/11
[expr.prim.lambda.capture]/15
Apliquemos esto a su caso 1:
El tipo de cierre de este lambda tendrá un miembro de datos no estático sin nombre (llamémoslo
__x
) de tipoint
(yax
que no es una referencia ni una función), y los accesosx
dentro del cuerpo lambda se transforman en accesos__x
. Cuando evaluamos la expresión lambda (es decir, cuando asignamos alambda
), inicializamos directamente__x
conx
.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
:Hay otro párrafo sobre la captura de referencias de referencias, pero no lo estamos haciendo en ninguna parte.
Entonces, para el caso 2:
No sabemos si un miembro se agrega al tipo de cierre.
x
en el cuerpo lambda podría referirse directamente alx
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
:Dado eso, veamos el caso 3:
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:
El miembro de tipo de cierre se inicializa
__p = std::move(unique_ptr_var)
cuando se evalúa la expresión lambda (es decir, cuandol
se asigna a). Los accesos ap
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 :)
fuente
Caso 1
[x](){}
: El constructor generado aceptará su argumento porconst
referencia posiblemente calificada para evitar copias innecesarias:Caso 2
[x&](){}
: Sus suposiciones aquí son correctas,x
se pasan y se almacenan por referencia.Caso 3
[x = 33](){}
: De nuevo correcto,x
se inicializa por valor.Caso 4
[p = std::move(unique_ptr_var)]
: El constructor se verá así:así que sí,
unique_ptr_var
se "trasladó" al cierre. Consulte también el Artículo 32 de Scott Meyer en Effective Modern C ++ ("Usar la captura de inicio para mover objetos a los cierres").fuente
const
-calificado" ¿Por qué?const
no puede doler aquí debido a alguna ambigüedad / mejor coincidencia cuando no,const
etc. De todos modos, ¿crees que debería eliminar elconst
?Hay menos necesidad de especular, usando cppinsights.io .
Caso 1:
Código
El compilador genera
Caso 2:
Código
El compilador genera
Caso 3:
Código
El compilador genera
Caso 4 (extraoficialmente):
Código
El compilador genera
Y creo que este último código responde a su pregunta. Se produce un movimiento, pero no [técnicamente] en el constructor.
Las capturas en sí no lo son
const
, pero puede ver que laoperator()
función sí lo es. Naturalmente, si necesita modificar las capturas, marque la lambda comomutable
.fuente