Y a-t-il une différence entre l'initialisation de la copie et l'initialisation directe?

244

Supposons que j'ai cette fonction:

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());
}

Dans chaque groupe, ces déclarations sont-elles identiques? Ou existe-t-il une copie supplémentaire (éventuellement optimisable) dans certaines des initialisations?

J'ai vu des gens dire les deux choses. Veuillez citer le texte comme preuve. Ajoutez également d'autres cas, s'il vous plaît.

rlbond
la source
1
Et il y a le quatrième cas discuté par @JohannesSchaub - A c1; A c2 = c1; A c3(c1);.
Dan Nissenbaum
1
Juste une note de 2018: les règles ont changé en C ++ 17 , voir, par exemple, ici . Si ma compréhension est correcte, en C ++ 17, les deux instructions sont effectivement les mêmes (même si le ctor de copie est explicite). De plus, si l'expression init devait être d'un autre type que A, l'initialisation de la copie ne nécessiterait pas l'existence d'un constructeur de copie / déplacement. C'est pourquoi std::atomic<int> a = 1;ça va en C ++ 17 mais pas avant.
Daniel Langr

Réponses:

246

Mise à jour C ++ 17

En C ++ 17, la signification de A_factory_func()changé de créer un objet temporaire (C ++ <= 14) à simplement spécifier l'initialisation de tout objet auquel cette expression est initialisée (en gros) en C ++ 17. Ces objets (appelés "objets de résultat") sont les variables créées par une déclaration (comme a1), les objets artificiels créés lorsque l'initialisation finit par être rejetée, ou si un objet est nécessaire pour la liaison de référence (comme, dans A_factory_func();. Dans le dernier cas, un objet est créé artificiellement, appelé "matérialisation temporaire", car il A_factory_func()n'a pas de variable ou de référence qui, autrement, exigerait qu'un objet existe).

Comme exemples dans notre cas, dans le cas de règles spéciales a1et a2dire que dans de telles déclarations, l'objet de résultat d'un initialiseur de valeur du même type que a1variable est a1, et donc A_factory_func()initialise directement l'objet a1. N'importe quelle distribution intermédiaire de style fonctionnel n'aurait aucun effet, parce que A_factory_func(another-prvalue)simplement "passe" l'objet résultat de la valeur externe pour être également l'objet résultat de la valeur interne.


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

Cela dépend du type de A_factory_func()retour. Je suppose qu'il renvoie un A- alors il fait de même - sauf que lorsque le constructeur de copie est explicite, le premier échouera. Lire 8.6 / 14

double b1 = 0.5;
double b2(0.5);

Cela fait la même chose car c'est un type intégré (cela ne signifie pas un type de classe ici). Lire 8.6 / 14 .

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

Ce n'est pas la même chose. Le premier initialise par défaut si Aest un non-POD, et ne fait aucune initialisation pour un POD (lire 8.6 / 9 ). La deuxième copie initialise: Value initialise un temporaire puis copie cette valeur dans c2(Lire 5.2.3 / 2 et 8.6 / 14 ). Cela nécessitera bien sûr un constructeur de copie non explicite (Lire 8.6 / 14 et 12.3.1 / 3 et 13.3.1.3/1 ). Le troisième crée une déclaration de fonction pour une fonction c3qui renvoie un Aet qui prend un pointeur de fonction vers une fonction renvoyant un A(Lire 8.2 ).


Plonger dans les initialisations directes et copier l'initialisation

Bien qu'ils semblent identiques et censés faire de même, ces deux formes sont remarquablement différentes dans certains cas. Les deux formes d'initialisation sont l'initialisation directe et la copie:

T t(x);
T t = x;

Il y a un comportement que nous pouvons attribuer à chacun d'eux:

  • L'initialisation directe se comporte comme un appel de fonction à une fonction surchargée: les fonctions, dans ce cas, sont les constructeurs de T(y compris explicitcelles), et l'argument est x. La résolution de surcharge trouvera le meilleur constructeur correspondant et, si nécessaire, effectuera toute conversion implicite requise.
  • L'initialisation de copie construit une séquence de conversion implicite: elle essaie de se convertir xen un objet de type T. (Il peut ensuite copier cet objet dans l'objet initialisé, donc un constructeur de copie est également nécessaire - mais ce n'est pas important ci-dessous)

Comme vous le voyez, l' initialisation de la copie fait en quelque sorte partie de l'initialisation directe en ce qui concerne les conversions implicites possibles: alors que l'initialisation directe a tous les constructeurs disponibles pour appeler, et en plus peut effectuer toute conversion implicite dont elle a besoin pour faire correspondre les types d'arguments, copier l'initialisation peut simplement configurer une séquence de conversion implicite.

J'ai essayé dur et j'ai obtenu le code suivant pour sortir un texte différent pour chacun de ces formulaires , sans utiliser le "évident" via les explicitconstructeurs.

#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>

Comment cela fonctionne-t-il et pourquoi génère-t-il ce résultat?

  1. Initialisation directe

    Tout d'abord, il ne sait rien de la conversion. Il va juste essayer d'appeler un constructeur. Dans ce cas, le constructeur suivant est disponible et correspond exactement :

    B(A const&)

    Il n'y a pas de conversion, encore moins une conversion définie par l'utilisateur, nécessaire pour appeler ce constructeur (notez qu'aucune conversion de qualification const ne se produit ici non plus). Et donc l'initialisation directe l'appellera.

  2. Initialisation de la copie

    Comme indiqué ci-dessus, l'initialisation de la copie va construire une séquence de conversion lorsqu'elle an'a pas été typée Bou dérivée de celle-ci (ce qui est clairement le cas ici). Il cherchera donc des moyens de faire la conversion et trouvera les candidats suivants

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

    Remarquez comment j'ai réécrit la fonction de conversion: Le type de paramètre reflète le type du thispointeur, qui dans une fonction membre non const est à non-const. Maintenant, nous appelons ces candidats avec xcomme argument. Le gagnant est la fonction de conversion: parce que si nous avons deux fonctions candidates acceptant toutes les deux une référence au même type, alors la version moins const l' emporte (c'est d'ailleurs le mécanisme qui préfère la fonction membre non const appelle non -const objets).

    Notez que si nous changeons la fonction de conversion pour être une fonction membre const, alors la conversion est ambiguë (car les deux ont un type de paramètre A const&alors): le compilateur Comeau la rejette correctement, mais GCC l'accepte en mode non pédant. -pedanticCependant, le basculement vers le produit génère également l'avertissement d'ambiguïté approprié.

J'espère que cela aide un peu à rendre plus claire la différence entre ces deux formes!

Johannes Schaub - litb
la source
Sensationnel. Je ne m'étais même pas rendu compte de la déclaration de fonction. Je dois à peu près accepter votre réponse simplement parce que je suis le seul à le savoir. Y a-t-il une raison pour que les déclarations de fonction fonctionnent de cette façon? Il serait préférable que c3 soit traité différemment à l'intérieur d'une fonction.
rlbond
4
Bah, désolé, mais j'ai dû supprimer mon commentaire et le poster à nouveau, à cause du nouveau moteur de formatage: c'est parce que dans les paramètres de fonction, R() == R(*)()et T[] == T*. En d'autres termes, les types de fonction sont des types de pointeur de fonction et les types de tableau sont des types de pointeur vers élément. Ça craint. Il peut être contourné par A c3((A()));(parens autour de l'expression).
Johannes Schaub - litb
4
Puis-je demander ce que signifie "'Lire 8.5 / 14'"? À quoi cela fait-il référence? Un livre? Un chapitre? Un site Web?
AzP
9
@AzP beaucoup de gens sur SO veulent souvent des références à la spécification C ++, et c'est ce que j'ai fait ici, en réponse à la demande de rlbond "Veuillez citer le texte comme preuve.". Je ne veux pas citer la spécification, car cela gonfle ma réponse et c'est beaucoup plus de travail pour rester à jour (redondance).
Johannes Schaub - litb
1
@luca je recommande de commencer une nouvelle question pour que les autres puissent bénéficier de la réponse que les gens donnent aussi
Johannes Schaub - litb
49

L'affectation est différente de l' initialisation .

Les deux lignes suivantes effectuent l' initialisation . Un seul appel constructeur est effectué:

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

mais ce n'est pas équivalent à:

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

Pour l'instant, je n'ai pas de texte pour le prouver mais c'est très simple à expérimenter:

#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
la source
2
Bonne référence: «Le langage de programmation C ++, édition spéciale» par Bjarne Stroustrup, section 10.4.4.1 (page 245). Décrit l'initialisation et l'affectation de copie et pourquoi elles sont fondamentalement différentes (bien qu'elles utilisent toutes les deux l'opérateur = comme syntaxe).
Naaff le
Nit mineur, mais je n'aime vraiment pas quand les gens disent que "A a (x)" et "A a = x" sont égaux. Strictement pas. Dans de nombreux cas, ils feront exactement la même chose, mais il est possible de créer des exemples où, selon l'argument, différents constructeurs sont réellement appelés.
Richard Corden
Je ne parle pas «d'équivalence syntaxique». Sémantiquement, les deux modes d' initialisation sont les mêmes.
Mehrdad Afshari
@MehrdadAfshari Dans le code de réponse de Johannes, vous obtenez une sortie différente en fonction de celui que vous utilisez.
Brian Gordon
1
@BrianGordon Oui, vous avez raison. Ils ne sont pas équivalents. J'avais adressé le commentaire de Richard dans mon montage depuis longtemps.
Mehrdad Afshari
22

double b1 = 0.5; est l'appel implicite du constructeur.

double b2(0.5); est un appel explicite.

Regardez le code suivant pour voir la différence:

#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 votre classe n'a pas de constucteurs explicites, les appels explicites et implicites sont identiques.

Kirill V. Lyadvinsky
la source
5
+1. Bonne réponse. Bon à noter également la version explicite. Soit dit en passant, il est important de noter que vous ne pouvez pas avoir les deux versions d'une surcharge de constructeur unique en même temps. Il ne suffirait donc pas de compiler dans le cas explicite. S'ils compilent tous les deux, ils doivent se comporter de la même manière.
Mehrdad Afshari
4

Premier regroupement: cela dépend de ce qui A_factory_funcrevient. La première ligne est un exemple d' initialisation de copie , la deuxième ligne est l'initialisation directe . Si A_factory_funcrenvoie un Aobjet, alors ils sont équivalents, ils appellent tous les deux le constructeur de copie pour A, sinon la première version crée une valeur r de type à Apartir des opérateurs de conversion disponibles pour le type de retour de A_factory_funcou les Aconstructeurs appropriés , puis appelle le constructeur de copie pour construire à a1partir de ce temporaire. La deuxième version tente de trouver un constructeur approprié qui accepte tout ce qui A_factory_funcretourne, ou qui prend quelque chose dans lequel la valeur de retour peut être implicitement convertie.

Deuxième regroupement: exactement la même logique s'applique, sauf que les types intégrés n'ont pas de constructeurs exotiques, ils sont donc, en pratique, identiques.

Troisième regroupement: c1est initialisé par défaut, c2est initialisé en copie à partir d'une valeur initialisée temporaire. Tous les membres de c1ce type pod (ou membres de membres, etc., etc.) ne peuvent pas être initialisés si les constructeurs par défaut fournis par l'utilisateur (le cas échéant) ne les initialisent pas explicitement. Pour c2, cela dépend de l'existence d'un constructeur de copie fourni par l'utilisateur et de l'initialisation appropriée de ces membres, mais les membres du temporaire seront tous initialisés (initialisés à zéro sinon autrement explicitement initialisés). Comme litb repéré, c3est un piège. C'est en fait une déclaration de fonction.

CB Bailey
la source
4

À noter:

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

C'est-à-dire pour la copie-initialisation.

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

En d'autres termes, un bon compilateur ne créera pas de copie pour l'initialisation de copie quand cela peut être évité; au lieu de cela, il appellera simplement le constructeur directement - c'est-à-dire, tout comme pour l'initialisation directe.

En d'autres termes, la copie-initialisation est exactement comme l'initialisation directe dans la plupart des cas <opinion> où du code compréhensible a été écrit. Étant donné que l'initialisation directe provoque potentiellement des conversions arbitraires (et donc probablement inconnues), je préfère toujours utiliser l'initialisation par copie lorsque cela est possible. (Avec le bonus qu'il ressemble en fait à l'initialisation.) </opinion>

Sommité technique: [12,2 / 1 cont d'en haut] 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.

Heureux de ne pas écrire de compilateur C ++.

John H.
la source
4

Vous pouvez voir sa différence explicitet ses implicittypes de constructeurs lorsque vous initialisez un objet:

Des classes :

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

class B
{
    explicit B(int) { }
    explicit B(int, int) { }
};

Et dans la main fonction:

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
}

Par défaut, un constructeur est comme implicitvous avez donc deux façons de l'initialiser:

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

Et en définissant une structure aussi explicitsimplement, vous avez une façon aussi directe:

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
BattleTested
la source
3

Réponse à l'égard de cette partie:

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

Comme la plupart des réponses sont pré-c ++ 11, j'ajoute ce que c ++ 11 a à dire à ce sujet:

Un spécificateur de type simple (7.1.6.2) ou un spécificateur de nom de type (14.6) suivi d'une liste d'expressions entre parenthèses construit une valeur du type spécifié en fonction de la liste d'expressions. Si la liste d'expressions est une expression unique, l'expression de conversion de type est équivalente (en définition et si elle est définie en signification) à l'expression de transtypage correspondante (5.4). Si le type spécifié est un type de classe, le type de classe doit être complet. Si la liste d'expressions spécifie plus d'une valeur unique, le type doit être une classe avec un constructeur correctement déclaré (8.5, 12.1), et l'expression T (x1, x2, ...) est équivalente en effet à la déclaration T t (x1, x2, ...); pour certaines variables temporaires inventées t, le résultat étant la valeur de t comme valeur.

Donc, optimisation ou non, ils sont équivalents selon la norme. Notez que cela est conforme à ce que d'autres réponses ont mentionné. Je cite simplement ce que la norme a à dire par souci d'exactitude.

bashrc
la source
Aucune des "listes d'expressions de vos exemples ne spécifie plus d'une seule valeur". En quoi cela est-il pertinent?
underscore_d
0

Beaucoup de ces cas sont soumis à l'implémentation d'un objet, il est donc difficile de vous donner une réponse concrète.

Considérez le cas

A a = 5;
A a(5);

Dans ce cas, en supposant un opérateur d'affectation approprié et un constructeur d'initialisation qui acceptent un seul argument entier, la façon dont j'implémente lesdites méthodes affecte le comportement de chaque ligne. Cependant, il est courant que l'un de ceux-ci appelle l'autre dans la mise en œuvre pour éliminer le code en double (bien que dans un cas aussi simple que cela, il n'y aurait pas de véritable objectif).

Edit: Comme mentionné dans d'autres réponses, la première ligne appellera en fait le constructeur de copie. Considérez les commentaires relatifs à l'opérateur d'affectation comme un comportement se rapportant à une affectation autonome.

Cela dit, la façon dont le compilateur optimise le code aura alors son propre impact. Si j'ai le constructeur d'initialisation appelant l'opérateur "=" - si le compilateur ne fait aucune optimisation, la ligne du haut effectuerait alors 2 sauts par opposition à un dans la ligne du bas.

Maintenant, pour les situations les plus courantes, votre compilateur optimisera à travers ces cas et éliminera ce type d'inefficacité. Donc, effectivement, toutes les situations différentes que vous décrivez seront les mêmes. Si vous voulez voir exactement ce qui se fait, vous pouvez regarder le code objet ou une sortie d'assembly de votre compilateur.

dborba
la source
Ce n'est pas une optimisation . Le compilateur doit appeler le constructeur de la même façon dans les deux cas. En conséquence, aucun d'entre eux ne se compilera si vous venez de le faire operator =(const int)et non A(const int). Voir la réponse de @ jia3ep pour plus de détails.
Mehrdad Afshari
Je crois que vous avez raison en fait. Cependant, il compilera très bien en utilisant un constructeur de copie par défaut.
dborba
De plus, comme je l'ai mentionné, il est courant qu'un constructeur de copie appelle un opérateur d'affectation, auquel cas les optimisations du compilateur entrent en jeu.
dborba
0

Il s'agit du langage de programmation C ++ de Bjarne Stroustrup:

Une initialisation avec un = est considérée comme une initialisation de copie . En principe, une copie de l'initialiseur (l'objet à partir duquel nous copions) est placée dans l'objet initialisé. Cependant, une telle copie peut être optimisée (élidée) et une opération de déplacement (basée sur la sémantique de déplacement) peut être utilisée si l'initialiseur est une valeur r. Le fait de laisser le = rend l'initialisation explicite. L'initialisation explicite est appelée initialisation directe .

Bharat
la source