web-dev-qa-db-fra.com

Existe-t-il une différence entre l’initialisation de la copie et l’initialisation directe?

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 texte comme preuve. Ajoutez également d'autres cas s'il vous plaît.

223
rlbond

Mise à jour C++ 17

En C++ 17, la signification de A_factory_func() est passée de la création d'un objet temporaire (C++ <= 14) à la spécification de l'initialisation de l'objet de cette expression (en gros) au C++ 17. Ces objets (appelés "objets de résultat") sont les variables créées par une déclaration (comme a1), Des objets artificiels créés lorsque l'initialisation finit par être ignoré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 A_factory_func() ne possède pas de variable ou de référence qui nécessiterait sinon l'existence d'un objet).

À titre d’exemples dans notre cas, dans le cas de a1 Et de a2, Les règles spéciales indiquent que, dans de telles déclarations, l’objet résultat d’un initialiseur de valeur du même type que a1 Est la variable a1, et donc A_factory_func() initialise directement l'objet a1. Toute conversion de style fonctionnel intermédiaire n'aurait aucun effet, car A_factory_func(another-prvalue) vient "simplement" traverser "l'objet résultat de la prvalue externe pour être également l'objet résultat de la prvalue interne.


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

Dépend du type A_factory_func() retourné. Je suppose que cela retourne un A - alors c'est la même chose - sauf que lorsque le constructeur de la copie est explicite, le premier échouera. Lire 8.6/14

double b1 = 0.5;
double b2(0.5);

Ceci est identique car il s'agit d'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());

Cela ne fait pas la même chose. Le premier défaut initialise si A n'est pas un POD, et ne fait aucune initialisation pour un POD (Read 8.6/9 ). La deuxième copie est initialisée: Value-initialise un temporaire puis copie cette valeur dans c2 (Read 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/ et 13.3.1.3/1 ) . Le troisième crée une déclaration de fonction pour une fonction c3 Renvoyant un A et prenant un pointeur de fonction sur une fonction renvoyant un A (Read 8.2 =).


Recherche dans les initialisations Initialisation directe et copie

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

T t(x);
T t = x;

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

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

Comme vous le voyez, copie de l'initialisation fait en quelque sorte partie de l'initialisation directe en ce qui concerne d'éventuelles conversions implicites: alors que l'initialisation directe dispose de tous les constructeurs disponibles, et en plus peut effectuer toutes les conversions implicites nécessaires pour faire correspondre les types d'arguments, l'initialisation de la copie ne peut définir qu'une séquence de conversion implicite.

J'ai essayé durement et obtenu le code suivant pour produire un texte différent pour chacune de ces formes , sans utiliser les constructeurs "évident" à travers 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>

Comment cela fonctionne-t-il et pourquoi produit-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 à correspondance exacte:

    B(A const&)
    

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

  2. Copier l'initialisation

    Comme indiqué ci-dessus, l'initialisation de la copie construira une séquence de conversion lorsque a n'aura pas le type B ou n'en sera dérivé (ce qui est clairement le cas ici). Donc, il va chercher 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 pointeur this qui, dans une fonction membre non-const, est à non-const. Maintenant, nous appelons ces candidats avec x comme argument. Le gagnant est la fonction de conversion: parce que si nous avons deux fonctions candidates acceptant toutes les deux une référence du même type, alors la version less const gagne (c’est aussi le mécanisme qui préfère les appels de fonction de membre non-const pour les objets non-const).

    Notez que si nous changeons la fonction de conversion en fonction membre const, alors la conversion est ambiguë (car les deux ont un type de paramètre de A const& Then): le compilateur Comeau la rejette correctement, mais GCC l’accepte de manière non -pédantique mode. En passant à -pedantic, L'avertissement d'ambiguïté approprié est également émis.

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

237

Assignment est différent de initialisation.

Les deux lignes suivantes effectuent l'initialisation . Un seul appel de 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 =

Je n'ai pas de texte pour le moment pour le prouver, mais il est très facile d'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;
}
46
Mehrdad Afshari

double b1 = 0.5; Est un 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 constantes explicites, les appels explicites et implicites sont identiques.

16

À noter:

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

C'est-à-dire pour l'initialisation de copie.

[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 va pas créer une copie pour l’initialisation de la copie lorsque cela peut être évité; au lieu de cela, il appellera simplement le constructeur directement, c'est-à-dire comme pour l'initialisation directe.

En d’autres termes, l’initialisation de copie ressemble à l’initialisation directe dans la plupart des cas <opinion> où du code compréhensible a été écrit. Etant donné que l'initialisation directe provoque potentiellement des conversions arbitraires (et donc probablement inconnues), je préfère toujours utiliser l'initialisation de copie lorsque cela est possible. (Avec le bonus que cela ressemble en réalité à l'initialisation.) </ Opinion>

Technicité: [12.2/1 suite du dessus] 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.

Je suis content de ne pas écrire de compilateur C++.

4
John H.

Premier groupe: cela dépend de ce que A_factory_func résultats. La première ligne est un exemple de initialisation de copie, la deuxième ligne est initialisation directe. Si A_factory_func renvoie un objet A alors ils sont équivalents, ils appellent tous les deux le constructeur de copie pour A, sinon la première version crée une rvalue de type A à partir d'un opérateur de conversion disponible. pour le type de retour de A_factory_func ou les constructeurs A appropriés, puis appelle le constructeur de copie pour construire a1 de cette temporaire. La deuxième version tente de trouver un constructeur approprié qui prenne tout ce que A_factory_func retourne ou prend quelque chose pour lequel la valeur de retour peut être convertie implicitement.

Deuxième groupe: la même logique est vraie, sauf que les types intégrés n'ont pas de constructeurs exotiques, ils sont donc pratiquement identiques.

Troisième groupe: c1 est initialisé par défaut, c2 est copié à partir d'une valeur temporaire initialisée. Tous les membres de c1 qui ont un type de pod (ou des membres de membres, etc., etc.) peuvent ne 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 s'il existe un constructeur de copie fourni par l'utilisateur et s'il initialise correctement ces membres, mais les membres du temporaire seront tous initialisés (zéro initialisé sinon autrement explicitement initialisé). Comme l 'ai vu, c3 est un piège. C'est en fait une déclaration de fonction.

3
CB Bailey

Répondre à cette partie:

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

Comme la plupart des réponses sont antérieures à c ++ 11, j'ajoute ce que c ++ 11 dit à ce sujet:

Un spécificateur de type simple (7.1.6.2) ou un spécificateur de nom de nom de type (14.6) suivi d'une liste d'expressions entre parenthèses construit une valeur du type spécifié à partir de la liste d'expressions. Si la liste d'expressions est une expression unique, l'expression de conversion de type est équivalente (de manière définie et si sa signification est définie) à l'expression de conversion 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, le type doit être une classe avec un constructeur correctement déclaré (8.5, 12.1), et l'expression T (x1, x2, ...) est équivalente à la déclaration T t (x1, x2, ...); pour une variable temporaire inventée t, le résultat étant la valeur de t en tant que prvalue.

Donc, optimisation ou non, ils sont équivalents selon la norme. Notez que cela est conforme à ce que d’autres réponses ont mentionné. Je viens de citer ce que la norme a à dire pour des raisons de rectitude.

3
bashrc

Vous pouvez voir sa différence dans les types de constructeur explicit et implicit lorsque vous initialisez un objet:

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 lemainfonction:

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 s'appelle implicit, vous 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 comme explicit, vous n’avez qu’un seul moyen direct:

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
1
BattleTested

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

Considérons le cas

A a = 5;
A a(5);

Dans ce cas, en supposant un opérateur d’affectation approprié et un constructeur d’initialisation acceptant un seul argument entier, la façon dont je mets en œuvre lesdites méthodes affecte le comportement de chaque ligne. Cependant, il est de pratique courante que l’un d’entre eux appelle l’autre dans l’implémentation afin d’éliminer le code en double (bien que, dans un cas aussi simple que cela, il n’aurait aucun but réel.)

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 relatif à une affectation autonome.

Cela dit, la manière dont le compilateur optimise le code aura alors son propre impact. Si le constructeur d'initialisation appelle l'opérateur "=" - si le compilateur n'optimise pas, la ligne du haut effectuera alors 2 sauts au lieu d'un dans la ligne du bas.

Désormais, dans les situations les plus courantes, votre compilateur optimisera ces cas et éliminera ce type d’inefficacité. Si bien que toutes les situations que vous décrivez seront donc identiques. Si vous voulez voir exactement ce qui est fait, vous pouvez regarder le code objet ou une sortie Assembly de votre compilateur.

0
dborba