c++ - tipos - inicializar un miembro no copiable(u otro objeto) en el lugar desde una función de fábrica
ejemplo de variable local en c++ (2)
Una clase debe tener una copia válida o un constructor de movimiento para que cualquiera de esta sintaxis sea legal:
C x = factory();
C y( factory() );
C z{ factory() };
En C ++ 03 era bastante común confiar en la elección de copia para evitar que el compilador toque el constructor de copia. Cada clase tiene una firma válida de constructor de copia independientemente de si existe una definición.
En C ++ 11, un tipo no copiable debe definir C( C const & ) = delete;
, haciendo que cualquier referencia a la función no sea válida independientemente de su uso (igual para no movible). (C ++ 11 §8.4.3 / 2). GCC, por su parte, se quejará cuando intente devolver un objeto de este tipo por valor. Copia elision cesa de ayudar.
Afortunadamente, también tenemos una nueva sintaxis para expresar la intención en lugar de confiar en una brecha. La función de factory
puede devolver una lista iniciada para construir el resultado temporal en el lugar:
C factory() {
return { arg1, 2, "arg3" }; // calls C::C( whatever ), no copy
}
Edit: Si hay alguna duda, esta declaración de return
se analiza de la siguiente manera:
- 6.6.3 / 2: "Una declaración de retorno con una lista-inic de paréntesis inicializa el objeto o la referencia que se devolverá desde la función mediante la inicialización-lista-copia (8.5.4) de la lista de inicialización especificada."
- 8.5.4 / 1: "la inicialización de lista en un contexto de inicialización de copia se denomina inicialización de lista de copia". ¶3: "si T es un tipo de clase, se consideran constructores. Los constructores aplicables se enumeran y el mejor se elige mediante resolución de sobrecarga (13.3, 13.3.1.7)".
No se deje engañar por el nombre copy-list-initialization . 8.5:
13: La forma de inicialización (usar paréntesis o
=
) es generalmente insignificante, pero importa cuando el inicializador o la entidad que se está inicializando tiene un tipo de clase; vea abajo. Si la entidad que se está inicializando no tiene un tipo de clase, la lista de expresiones en un inicializador entre paréntesis será una expresión única.14: La inicialización que se produce en la forma
T x = a;
así como en el paso de argumentos, el retorno de función, el lanzamiento de una excepción (15.1), el manejo de una excepción (15.3) y la inicialización agregada de miembros (8.5.1) se denomina inicialización de copia.
Tanto la inicialización de la copia como su inicialización , la inicialización directa , siempre se refieren a la inicialización de la lista cuando el inicializador es una lista de inicio con arriostramiento. No hay un efecto semántico en la adición de =
, que es una de las razones por las que la inicialización de lista se denomina informalmente inicialización uniforme.
Existen diferencias: la inicialización directa puede invocar un constructor explícito, a diferencia de la inicialización de copia. La inicialización de copia inicializa un temporal y lo copia para inicializar el objeto, al convertir.
La especificación de las instrucciones copy-list-initialization for return { list }
simplemente especifica la sintaxis equivalente exacta como temp T = { list };
, donde =
denota la inicialización de la copia. No implica inmediatamente que se invoque un constructor de copia.
- Final de edición.
El resultado de la función se puede recibir en una referencia de rvalor para evitar copiar el temporal en un local:
C && x = factory(); // applies to other initialization syntax
La pregunta es, ¿cómo inicializar un miembro no estático de una función de fábrica que devuelve el tipo no copiable, no movible? El truco de referencia no funciona porque un miembro de referencia no prolonga la vida útil de un temporal.
Nota, no estoy considerando la inicialización agregada. Se trata de definir un constructor.
En su pregunta principal:
La pregunta es, ¿cómo inicializar un miembro no estático de una función de fábrica que devuelve el tipo no copiable, no movible?
Usted no
Su problema es que está intentando combinar dos cosas: cómo se genera el valor de retorno y cómo se utiliza el valor de retorno en el sitio de la llamada. Estas dos cosas no se conectan entre sí. Recuerde: la definición de una función no puede afectar la forma en que se usa (en términos de lenguaje), ya que esa definición no está necesariamente disponible para el compilador. Por lo tanto, C ++ no permite que la manera en que se generó el valor de retorno afecte a nada (fuera de elision, que es una optimización, no un requisito de idioma).
Para decirlo de otra manera, esto:
C c = {...};
Es diferente de esto:
C c = [&]() -> C {return {...};}()
Tienes una función que devuelve un tipo por valor. Está devolviendo una expresión prvalue de tipo C
Si desea almacenar este valor, dándole un nombre, tiene exactamente dos opciones:
Guárdelo como un
const&
o&&
. Esto extenderá la vida útil de lo temporal a la vida útil del bloque de control. No puedes hacer eso con las variables miembro; Solo se puede hacer con variables automáticas en funciones.Copiarlo / moverlo a un valor. Puede hacer esto con una variable miembro, pero obviamente requiere que el tipo se pueda copiar o mover.
Estas son las únicas opciones que C ++ pone a su disposición si desea almacenar una expresión prvalue. Por lo tanto, puede hacer que el tipo sea movible o devolver un puntero recién asignado a la memoria y almacenarlo en lugar de un valor.
Esta limitación es una gran parte de la razón por la cual el movimiento se creó en primer lugar: poder pasar cosas por valor y evitar copias caras. El idioma no se pudo cambiar para forzar la elección de valores de retorno. Así que en cambio, redujeron el costo en muchos casos.
Temas como este fueron algunas de las motivaciones principales para que el cambio en C ++ 17 permitiera estas inicializaciones (y excluyera las copias del lenguaje, no solo como una optimización).