¿Hay alguna diferencia entre la inicialización de copia y la inicialización directa?

244

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 grupo, ¿son idénticas estas afirmaciones? ¿O hay una copia adicional (posiblemente optimizable) en algunas de las inicializaciones?

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

rlbond
fuente
1
Y está el cuarto caso discutido por @JohannesSchaub - A c1; A c2 = c1; A c3(c1);.
Dan Nissenbaum
1
Solo una nota de 2018: las reglas han cambiado en C ++ 17 , consulte, por ejemplo, aquí . Si mi comprensión es correcta, en C ++ 17, ambas declaraciones son efectivamente las mismas (incluso si el copiador es explícito). Además, si la expresión init fuera de otro tipo que A, la inicialización de la copia no requeriría la existencia de un constructor de copia / movimiento. Por eso std::atomic<int> a = 1;está bien en C ++ 17 pero no antes.
Daniel Langr

Respuestas:

246

Actualización de C ++ 17

En C ++ 17, el significado de A_factory_func()cambiar de crear un objeto temporal (C ++ <= 14) a solo especificar la inicialización de cualquier objeto al que se inicialice esta expresión (en términos generales) en C ++ 17. Estos objetos (llamados "objetos de resultado") son las variables creadas por una declaración (como a1), objetos artificiales creados cuando la inicialización termina siendo descartada, o si se necesita un objeto 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 lo contrario requeriría que exista un objeto).

Como ejemplos en nuestro caso, en el caso de a1y a2las reglas especiales dicen que en tales declaraciones, el objeto resultante de un inicializador prvalue del mismo tipo que a1es variable a1, y por lo tanto, A_factory_func()inicializa directamente el objeto a1. Cualquier molde intermedio de estilo funcional 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 ser también el objeto de resultado del prvalue interno.


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

Depende de qué tipo A_factory_func()devuelve. Supongo que devuelve un A, luego está haciendo lo mismo, excepto que cuando el constructor de copia es explícito, entonces 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í). Leer 8,6 / 14 .

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

Esto no está haciendo lo mismo. El primer valor predeterminado se inicializa si no Aes un POD y no se inicializa para un POD (Leer 8.6 / 9 ). La segunda copia se inicializa: el valor inicializa un valor 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 c3que devuelve un Ay que toma un puntero de función a una función que devuelve un A(Leer 8.2 ).


Profundizando en Inicializaciones Directa y Copia de inicialización

Si bien se ven 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 directa e inicialización de copia:

T t(x);
T t = x;

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

  • La inicialización directa se comporta como una llamada a una función sobrecargada: las funciones, en este caso, son las constructoras T(incluidas explicitlas), 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 xa 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 copia es de alguna manera parte de la inicialización directa con respecto a 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 emparejar tipos de argumentos, inicialización de copia solo puede configurar una secuencia de conversión implícita.

Lo intenté mucho y obtuve el siguiente código para generar texto diferente para cada una de esas formas , sin usar el "obvio" a través de los explicitconstructores.

#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é genera ese resultado?

  1. Inicialización directa

    Primero no sabe nada acerca de la conversión. Solo 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í, la inicialización directa lo llamará.

  2. Copia de inicialización

    Como se dijo anteriormente, la inicialización de copia construirá una secuencia de conversión cuando ano se ha escrito Bo derivado de ella (lo cual es claramente el caso aquí). Por lo tanto, buscará formas de realizar 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 del thispuntero, que en una función miembro no constante es no constante. Ahora, llamamos a estos candidatos xcomo argumento. El ganador es la función de conversión: porque si tenemos dos funciones candidatas que aceptan una referencia al mismo tipo, entonces la versión menos constante gana (esto es, por cierto, también el mecanismo que prefiere llamadas de funciones miembro no constantes para -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 de A const&entonces): el compilador Comeau la rechaza correctamente, pero GCC la acepta en modo no pedante. Sin -pedanticembargo, cambiar a hace que también genere la advertencia de ambigüedad adecuada.

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

Johannes Schaub - litb
fuente
Guau. Ni siquiera me di cuenta de la declaración de la función. Tengo que aceptar tu respuesta solo por ser el único en saberlo. ¿Hay alguna razón por la cual las declaraciones de funciones funcionan de esa manera? Sería mejor si c3 fuera tratado de manera diferente dentro de una función.
rlbond
44
Bah, lo siento amigos, pero tuve que eliminar mi comentario y publicarlo nuevamente, debido al nuevo motor de formateo: es porque en los parámetros de la función, R() == R(*)()y T[] == T*. Es decir, los tipos de función son tipos de puntero de función y los tipos de matriz son tipos de puntero a elemento. Esto apesta. Puede ser A c3((A()));solucionado por (parens alrededor de la expresión).
Johannes Schaub - litb
44
¿Puedo preguntar qué significa "'Leer 8.5 / 14'"? ¿A qué se refiere eso? ¿Un libro? ¿Un capítulo? ¿Una página web?
AzP
99
@AzP muchas personas en SO a menudo quieren referencias a la especificación de C ++, y eso es lo que hice aquí, en respuesta a la solicitud de rlbond "Por favor, cite el texto como prueba". No quiero citar las especificaciones, ya que eso aumenta mi respuesta y es mucho más trabajo mantener actualizado (redundancia).
Johannes Schaub - litb
1
@luca, recomiendo comenzar una nueva pregunta para que otros puedan beneficiarse de la respuesta que la gente también da
Johannes Schaub - litb
49

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

Ambas líneas 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 probar esto, pero es muy fácil 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;
}
Mehrdad Afshari
fuente
2
Buena referencia: "El lenguaje de programación C ++, edición especial" de Bjarne Stroustrup, sección 10.4.4.1 (página 245). Describe la inicialización y la asignación de copias y por qué son fundamentalmente diferentes (aunque ambas usan el operador = como sintaxis).
Naaff
Nit menor, pero realmente no me gusta cuando la gente dice que "A a (x)" y "A a = x" son iguales. Estrictamente no lo son. En muchos casos, harán exactamente lo mismo, pero es posible crear ejemplos en los que, según el argumento, se llaman realmente diferentes constructores.
Richard Corden
No estoy hablando de "equivalencia sintáctica". Semánticamente, ambas formas de inicialización son iguales.
Mehrdad Afshari
@MehrdadAfshari En el código de respuesta de Johannes obtienes resultados diferentes en función de cuál de los dos usas.
Brian Gordon
1
@BrianGordon Sí, tienes razón. No son equivalentes. Había abordado el comentario de Richard en mi edición hace mucho tiempo.
Mehrdad Afshari
22

double b1 = 0.5; Es una llamada implícita 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 constructores explícitos, las llamadas explícitas e implícitas son idénticas.

Kirill V. Lyadvinsky
fuente
55
+1. Buena respuesta. Es bueno señalar también la versión explícita. Por cierto, es importante tener en cuenta que no puede tener ambas versiones de una sobrecarga de un solo constructor al mismo tiempo. Por lo tanto, simplemente no se compilaría en el caso explícito. Si ambos compilan, tienen que comportarse de manera similar.
Mehrdad Afshari
4

Primera agrupación: depende de lo que A_factory_funcregrese. La primera línea es un ejemplo de inicialización de copia , la segunda línea es inicialización directa . Si A_factory_funcdevuelve un Aobjeto, entonces son equivalentes, ambos llaman al constructor de la copia A, de lo contrario, la primera versión crea un valor de tipo a Apartir de operadores de conversión disponibles para el tipo de retorno A_factory_funco los Aconstructores apropiados , y luego llama al constructor de la copia para construir a a1partir de este temporal. La segunda versión intenta encontrar un constructor adecuado que tome lo que sea que A_factory_funcdevuelva, o que tome algo a lo que el valor de retorno se pueda convertir implícitamente.

Segunda agrupación: 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.

Tercera agrupación: c1se inicializa por defecto, c2se inicializa por copia desde un valor inicializado temporalmente. c1No se puede inicializar ningún miembro que tenga un tipo de pod (o miembros de miembros, etc., etc.) si el usuario proporcionó los constructores predeterminados (si los hay) no los inicializa explícitamente. Para c2, depende de si hay un constructor de copias proporcionado por el usuario y si eso inicializa adecuadamente a esos miembros, pero los miembros de la temporal se inicializarán todos (inicializados a cero si no se inicializan explícitamente). Como Litb vio, c3es una trampa. En realidad es una declaración de función.

CB Bailey
fuente
4

De nota:

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

Es decir, para inicialización de copia.

[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á al constructor directamente, es decir, al igual que para la inicialización directa.

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

Goriness 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 ++.

John H.
fuente
4

Puede ver su diferencia explicity implicitlos tipos de constructor 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 main función:

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
}

Por defecto, un constructor es implicitasí 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 explicitsolo tienes 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
Batalla probada
fuente
3

Respondiendo con respecto a esta parte:

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

Como la mayoría de las respuestas son pre-c ++ 11, estoy agregando lo que c ++ 11 tiene que decir sobre esto:

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 sola expresión, la expresión de conversión de tipo es equivalente (en definición y si está definida 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 deberá estar completo. Si la lista de expresiones especifica más de un valor único, el tipo será una clase con un constructor adecuadamente 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 de ser el valor de t como un prvalor.

Entonces, 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. Simplemente citando lo que el estándar tiene que decir en aras de la corrección.

bashrc
fuente
La "lista de expresiones de ninguno de sus ejemplos especifica más de un solo valor". ¿Cómo es relevante algo de esto?
underscore_d
0

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

Considera el caso

A a = 5;
A a(5);

En este caso, suponiendo un operador de asignación adecuado y un constructor de inicialización que acepte un único argumento entero, la forma en que implemente 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).

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

Dicho esto, cómo el compilador optimiza el código tendrá su propio impacto. Si tengo el constructor de inicialización que 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á estos casos y eliminará este tipo de ineficiencias. Entonces, efectivamente, todas las diferentes situaciones que describas serán las mismas. Si desea ver exactamente lo que se está haciendo, puede mirar el código objeto o la salida de un ensamblador de su compilador.

dborba
fuente
No es una optimización . El compilador tiene que llamar al constructor por igual en ambos casos. Como resultado, ninguno de ellos se compilará si solo tiene operator =(const int)y no A(const int). Vea la respuesta de @ jia3ep para más detalles.
Mehrdad Afshari
Creo que tienes razón en realidad. Sin embargo, se compilará bien utilizando un constructor de copia predeterminado.
dborba
Además, como mencioné, es una práctica común que un constructor de copia llame a un operador de asignación, momento en el cual las optimizaciones del compilador entran en juego.
dborba
0

Esto es del lenguaje de programación C ++ de Bjarne Stroustrup:

Una inicialización con an = se considera una inicialización de copia . En principio, una copia del inicializador (el objeto del que estamos copiando) se coloca en el objeto inicializado. Sin embargo, dicha copia puede optimizarse (eliminarse) y puede usarse una operación de movimiento (basada en la semántica de movimiento) si el inicializador es un valor r. Dejar fuera = hace explícita la inicialización. La inicialización explícita se conoce como inicialización directa .

Bharat
fuente