sobrecarga copia constructores c++ initialization

c++ - constructores - ¿Hay alguna diferencia entre la inicialización de la copia y la inicialización directa?



constructor copia java (8)

Supongamos que tengo esta función:

void my_test() { A a1 = A_factory_func(); A a2(A_factory_func()); double b1 = 0.5; double b2(0.5); A c1; A c2 = A(); A c3(A()); }

En cada agrupación, ¿son idénticas estas afirmaciones? ¿O hay alguna copia adicional (posiblemente optimizable) en algunas de las inicializaciones?

He visto a gente decir ambas cosas. Por favor, cita el texto como prueba. También agregue otros casos por favor.


Actualización de C ++ 17

En C ++ 17, el significado de A_factory_func() cambió de crear un objeto temporal (C ++ <= 14) a solo especificar la inicialización de cualquier objeto con el que esta expresión se inicialice (en términos generales) en C ++ 17. Estos objetos (llamados "objetos de resultado") son las variables creadas por una declaración (como a1 ), los objetos artificiales creados cuando la inicialización termina siendo descartada, o si un objeto es necesario para el enlace de referencia (como, en A_factory_func(); . En el último caso, un objeto se crea artificialmente, llamado "materialización temporal", porque A_factory_func() no tiene una variable o referencia que de otro modo requeriría que exista un objeto).

Como ejemplos en nuestro caso, en el caso de a1 y a2 las reglas especiales dicen que en tales declaraciones, el objeto resultante de un inicializador de prvalor del mismo tipo que a1 es la variable a1 , y por A_factory_func() tanto A_factory_func() inicializa directamente el objeto a1 . Cualquier A_factory_func(another-prvalue) estilo funcional intermedia no tendría ningún efecto, ya que A_factory_func(another-prvalue) simplemente "pasa a través" del objeto de resultado del prvalue externo para que sea también el objeto de resultado del prvalor interno.

A a1 = A_factory_func(); A a2(A_factory_func());

Depende de qué tipo A_factory_func() devuelve. Supongo que devuelve una A , entonces está haciendo lo mismo, excepto que cuando el constructor de copia es explícito, el primero fallará. Leer 8.6/14

double b1 = 0.5; double b2(0.5);

Esto está haciendo lo mismo porque es un tipo incorporado (esto significa que no es un tipo de clase aquí). Lee 8.6/14 .

A c1; A c2 = A(); A c3(A());

Esto no está haciendo lo mismo. El primer valor predeterminado se inicializa si A es un POD y no realiza ninguna inicialización para un POD (lectura 8.6/9 ). La segunda copia se inicializa: El valor inicializa un temporal y luego copia ese valor en c2 (Leer 5.2.3/2 y 8.6/14 ). Por supuesto, esto requerirá un constructor de copia no explícito (Lea 8.6/14 y 12.3.1/3 y 13.3.1.3/1 ). El tercero crea una declaración de función para una función c3 que devuelve una A y que lleva un puntero a una función que devuelve una A (Lectura 8.2 ).

Profundizando en Inicializaciones Directas y Copia de inicialización.

Si bien son idénticos y se supone que deben hacer lo mismo, estas dos formas son notablemente diferentes en ciertos casos. Las dos formas de inicialización son la inicialización directa y la copia:

T t(x); T t = x;

Existe un comportamiento que podemos atribuir a cada uno de ellos:

  • La inicialización directa se comporta como una llamada de función a una función sobrecargada: las funciones, en este caso, son los constructores de T (incluidos los explicit ), y el argumento es x . La resolución de sobrecarga encontrará el mejor constructor coincidente y, cuando sea necesario, realizará cualquier conversión implícita requerida.
  • La inicialización de copia construye una secuencia de conversión implícita: intenta convertir x en un objeto de tipo T (Luego puede copiar sobre ese objeto en el objeto inicializado, por lo que también se necesita un constructor de copia, pero esto no es importante a continuación)

Como puede ver, la inicialización de la copia es de alguna manera una parte de la inicialización directa con respecto a las posibles conversiones implícitas: mientras que la inicialización directa tiene todos los constructores disponibles para llamar, y además puede hacer cualquier conversión implícita que necesite para hacer coincidir los tipos de argumentos, la inicialización de la copia sólo puede configurar una secuencia de conversión implícita.

Me esforcé y obtuve el siguiente código para generar un texto diferente para cada uno de esos formularios , sin usar el "obvio" a través de constructores explicit .

#include <iostream> struct B; struct A { operator B(); }; struct B { B() { } B(A const&) { std::cout << "<direct> "; } }; A::operator B() { std::cout << "<copy> "; return B(); } int main() { A a; B b1(a); // 1) B b2 = a; // 2) } // output: <direct> <copy>

¿Cómo funciona y por qué produce ese resultado?

  1. Inicialización directa

    Primero no sabe nada acerca de la conversión. Sólo intentará llamar a un constructor. En este caso, el siguiente constructor está disponible y es una coincidencia exacta :

    B(A const&)

    No hay conversión, y mucho menos una conversión definida por el usuario, necesaria para llamar a ese constructor (tenga en cuenta que aquí tampoco ocurre ninguna conversión de calificación constante). Y así lo llamará la inicialización directa.

  2. Copia inicializacion

    Como se dijo anteriormente, la inicialización de la copia construirá una secuencia de conversión cuando a no tenga el tipo B o se derive de ella (lo cual es claramente el caso aquí). Así buscará formas de hacer la conversión y encontrará los siguientes candidatos

    B(A const&) operator B(A&);

    Observe cómo reescribí la función de conversión: el tipo de parámetro refleja el tipo de this puntero, que en una función miembro no constante es no constante. Ahora, llamamos a estos candidatos con x como argumento. El ganador es la función de conversión: dado que si tenemos dos funciones candidatas que aceptan una referencia del mismo tipo, la versión menos constante gana (esto es, por cierto, también el mecanismo que prefiere las funciones miembro no constantes requiere no -const objetos).

    Tenga en cuenta que si cambiamos la función de conversión para que sea una función miembro constante, entonces la conversión es ambigua (porque ambos tienen un tipo de parámetro A const& luego): el compilador Comeau lo rechaza correctamente, pero GCC lo acepta en modo no pedante. -pedantic embargo, el cambio a -pedantic hace que se emita la advertencia de ambigüedad adecuada.

¡Espero que esto ayude un poco a aclarar cómo difieren estas dos formas!


De nota:

[12.2 / 1] Los Temporaries of class type are created in various contexts: ... and in some initializations (8.5).

Es decir, para copia-inicialización.

[12.8 / 15] When certain criteria are met, an implementation is allowed to omit the copy construction of a class object ...

En otras palabras, un buen compilador no creará una copia para la inicialización de la copia cuando se pueda evitar; en su lugar, solo llamará directamente al constructor, es decir, como para la inicialización directa.

En otras palabras, la inicialización de la copia es como la inicialización directa en la mayoría de los casos <opinion> donde se ha escrito un código comprensible. Como la inicialización directa puede causar conversiones arbitrarias (y, por lo tanto, probablemente desconocidas), prefiero usar siempre la inicialización de copias cuando sea posible. (Con la ventaja de que en realidad se parece a la inicialización.) </opinion>

Gordo técnico: [12.2 / 1 cont desde arriba] Even when the creation of the temporary object is avoided (12.8), all the semantic restrictions must be respected as if the temporary object was created.

Me alegro de no estar escribiendo un compilador de C ++.


Muchos de estos casos están sujetos a la implementación de un objeto, por lo que es difícil darle una respuesta concreta.

Considerar el caso

A a = 5; A a(5);

En este caso, asumiendo un operador de asignación apropiado y constructor de inicialización que acepte un solo argumento entero, la forma en que implemento dichos métodos afecta el comportamiento de cada línea. Sin embargo, es una práctica común que uno de ellos llame al otro en la implementación para eliminar el código duplicado (aunque en un caso tan simple como este no habría un propósito real).

Edición: Como se mencionó en otras respuestas, la primera línea llamará al constructor de copia. Considere los comentarios relacionados con el operador de la asignación como un comportamiento que pertenece a una asignación independiente.

Dicho esto, la forma en que el compilador optimiza el código tendrá su propio impacto. Si el constructor de inicialización llama al operador "=": si el compilador no realiza optimizaciones, la línea superior realizará 2 saltos en lugar de uno en la línea inferior.

Ahora, para las situaciones más comunes, su compilador optimizará a través de estos casos y eliminará este tipo de ineficiencias. Así que efectivamente todas las diferentes situaciones que describas resultarán igual. Si desea ver exactamente lo que se está haciendo, puede mirar el código objeto o una salida de ensamblado de su compilador.


Primer agrupamiento: depende de lo que devuelve A_factory_func . La primera línea es un ejemplo de inicialización de copia , la segunda línea es inicialización directa . Si A_factory_func devuelve un objeto A entonces son equivalentes, ambos llaman al constructor de copia para A , de lo contrario, la primera versión crea un valor de tipo A partir de un operador de conversión disponible para el tipo de retorno de A_factory_func o constructores A apropiados, y luego llama al Copia el constructor para construir a1 partir de este temporal. La segunda versión intenta encontrar un constructor adecuado que tome lo que devuelve A_factory_func , o que tome algo a lo que el valor de retorno se puede convertir implícitamente.

Segunda agrupación: se mantiene exactamente la misma lógica, excepto que los tipos incorporados no tienen constructores exóticos, por lo que son, en la práctica, idénticos.

Tercer grupo: c1 está inicializado por defecto, c2 se inicializa por copia desde un valor inicializado temporalmente. Cualquier miembro de c1 que tenga tipo pod (o miembros de miembros, etc., etc.) no se puede inicializar si el usuario suministró los constructores predeterminados (si corresponde) no los inicializa explícitamente. Para c2 , depende de si hay un constructor de copia provisto por el usuario y de si se inicializan adecuadamente los miembros, pero los miembros del temporal se inicializarán (cero inicializado si no se inicializa explícitamente de otra manera). Como litb manchado, c3 es una trampa. Es en realidad una declaración de función.


Puede ver su diferencia en los tipos de constructores explicit e implicit cuando inicializa un objeto:

Clases:

class A { A(int) { } // converting constructor A(int, int) { } // converting constructor (C++11) }; class B { explicit B(int) { } explicit B(int, int) { } };

Y en la función main :

int main() { A a1 = 1; // OK: copy-initialization selects A::A(int) A a2(2); // OK: direct-initialization selects A::A(int) A a3 {4, 5}; // OK: direct-list-initialization selects A::A(int, int) A a4 = {4, 5}; // OK: copy-list-initialization selects A::A(int, int) A a5 = (A)1; // OK: explicit cast performs static_cast // B b1 = 1; // error: copy-initialization does not consider B::B(int) B b2(2); // OK: direct-initialization selects B::B(int) B b3 {4, 5}; // OK: direct-list-initialization selects B::B(int, int) // B b4 = {4, 5}; // error: copy-list-initialization does not consider B::B(int,int) B b5 = (B)1; // OK: explicit cast performs static_cast }

De forma predeterminada, un constructor es tan implicit que tiene dos formas de inicializarlo:

A a1 = 1; // this is copy initialization A a2(2); // this is direct initialization

Y al definir una estructura como explicit solo tiene una forma directa:

B b2(2); // this is direct initialization B b5 = (B)1; // not problem if you either use of assign to initialize and cast it as static_cast


Respondiendo con respecto a esta parte:

A c2 = A (); A c3 (A ());

Como la mayoría de las respuestas son anteriores a c ++ 11, estoy agregando lo que c ++ 11 tiene que decir al respecto:

Un especificador de tipo simple (7.1.6.2) o un especificador de nombre de tipo (14.6) seguido de una lista de expresiones entre paréntesis construye un valor del tipo especificado dada la lista de expresiones. Si la lista de expresiones es una expresión única, la expresión de conversión de tipo es equivalente (en definición, y si se define en significado) a la expresión de conversión correspondiente (5.4). Si el tipo especificado es un tipo de clase, el tipo de clase estará completo. Si la lista de expresiones especifica más de un solo valor, el tipo será una clase con un constructor debidamente declarado (8.5, 12.1), y la expresión T (x1, x2, ...) es equivalente en efecto a la declaración T t (x1, x2, ...); para algunas variables temporales inventadas t, con el resultado es el valor de t como prvalue.

Por lo tanto, la optimización o no son equivalentes según el estándar. Tenga en cuenta que esto está de acuerdo con lo que otras respuestas han mencionado. Solo citando lo que la norma tiene que decir en aras de la corrección.


double b1 = 0.5; Es implícita la llamada del constructor.

double b2(0.5); Es una llamada explícita.

Mira el siguiente código para ver la diferencia:

#include <iostream> class sss { public: explicit sss( int ) { std::cout << "int" << std::endl; }; sss( double ) { std::cout << "double" << std::endl; }; }; int main() { sss ddd( 7 ); // calls int constructor sss xxx = 7; // calls double constructor return 0; }

Si su clase no tiene constuctores explícitos, las llamadas explícitas e implícitas son idénticas.


La asignación es diferente de la inicialización .

Las dos líneas siguientes hacen la inicialización . Se realiza una sola llamada de constructor:

A a1 = A_factory_func(); // calls copy constructor A a1(A_factory_func()); // calls copy constructor

pero no es equivalente a:

A a1; // calls default constructor a1 = A_factory_func(); // (assignment) calls operator =

No tengo un texto en este momento para demostrarlo, pero es muy fácil de experimentar:

#include <iostream> using namespace std; class A { public: A() { cout << "default constructor" << endl; } A(const A& x) { cout << "copy constructor" << endl; } const A& operator = (const A& x) { cout << "operator =" << endl; return *this; } }; int main() { A a; // default constructor A b(a); // copy constructor A c = a; // copy constructor c = b; // operator = return 0; }