290 votes

Y a-t-il une différence entre l'initialisation par 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 y a-t-il une copie supplémentaire (éventuellement optimisable) dans certaines des initialisations ?

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

1 votes

Et il y a le quatrième cas discuté par @JohannesSchaub - A c1; A c2 = c1; A c3(c1); .

2 votes

Juste une note pour 2018 : Les règles ont changé en C++17 voir, par exemple, aquí . Si ma compréhension est correcte, en C++17, les deux déclarations sont effectivement les mêmes (même si le copy ctor est explicite). De plus, si l'expression init est d'un autre type que A L'initialisation de la copie ne nécessite pas l'existence du constucteur copy/move. C'est pourquoi std::atomic<int> a = 1; est acceptable en C++17 mais pas avant.

286voto

Johannes Schaub - litb Points 256113

Mise à jour C++17

En C++17, la signification de A_factory_func() est passé de la création d'un objet temporaire (C++<=14) à la simple spécification de l'initialisation de l'objet sur lequel cette expression est initialisée (au sens large) en C++17. Ces objets (appelés "objets résultat") sont les variables créées par une déclaration (comme a1 ), des objets artificiels créés lorsque l'initialisation finit par être abandonnée, ou si un objet est nécessaire pour la liaison de référence (comme dans le cas de A_factory_func(); . Dans le dernier cas, un objet est créé artificiellement, appelé "matérialisation temporaire", car A_factory_func() n'a pas de variable ou de référence qui nécessiterait l'existence d'un objet).

A titre d'exemple dans notre cas, dans le cas de a1 y a2 Des règles spéciales stipulent que dans de telles déclarations, l'objet résultat d'un initialisateur prvalue du même type que a1 est variable a1 et donc A_factory_func() initialise directement l'objet a1 . Toute coulée intermédiaire de style fonctionnel n'aurait aucun effet, car A_factory_func(another-prvalue) ne fait que "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());

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

double b1 = 0.5;
double b2(0.5);

Il fait la même chose parce que c'est un type intégré (ce qui signifie qu'il ne s'agit pas d'un type de classe). 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 A est un non-POD, et ne fait pas d'initialisation pour un POD (Read 8.6/9 ). La deuxième copie s'initialise : Value-initialise une valeur temporaire et copie ensuite cette valeur dans c2 (Lire 5.2.3/2 y 8.6/14 ). Cela nécessitera bien sûr un constructeur de copie non explicite (Read 8.6/14 y 12.3.1/3 y 13.3.1.3/1 ). La troisième crée une déclaration de fonction pour une fonction c3 qui renvoie un A et qui prend un pointeur de fonction vers une fonction retournant un A (Lire 8.2 ).


Approfondir les initialisations Initialisation du direct et de la copie

Bien qu'elles aient l'air 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 par 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 à une fonction surchargée : Les fonctions, dans ce cas, sont les constructeurs de T (y compris explicit ), et l'argument est x . La résolution de surcharge trouvera le constructeur le mieux adapté et, si nécessaire, effectuera toute conversion implicite requise.
  • L'initialisation de la copie construit une séquence de conversion implicite : Elle essaie de convertir x à un objet de type T . (Il peut ensuite copier cet objet dans l'objet à initialiser, donc un constructeur de copie est également nécessaire - mais ce n'est pas important ci-dessous).

Comme vous le voyez, initialisation de la copie fait en quelque sorte partie de l'initialisation directe en ce qui concerne les éventuelles conversions implicites : Alors que l'initialisation directe dispose de tous les constructeurs disponibles à appeler, et que en outre peut effectuer toutes les conversions implicites dont elle a besoin pour faire correspondre les types d'arguments, l'initialisation de la copie peut juste mettre en place une séquence de conversion implicite.

J'ai fait des efforts et a obtenu le code suivant pour sortir un texte différent pour chacun de ces formulaires sans recourir à l'"évidence" par explicit constructeurs.

#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 fonctionne-t-il, et pourquoi produit-il ce résultat ?

  1. Initialisation directe

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

    B(A const&)

    Aucune conversion, et encore moins une conversion définie par l'utilisateur, n'est 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 construira une séquence de conversion lorsque a n'a pas de type B ou dérivé de celui-ci (ce qui est clairement le cas ici). Il cherchera donc des moyens d'effectuer 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 du paramètre reflète le type de l'objet 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 : Car si nous avons deux fonctions candidates acceptant toutes deux une référence au même type, alors la fonction moins constant l'emporte (c'est d'ailleurs aussi le mécanisme qui préfère les appels de fonctions membres non-const pour les objets non-const).

    Notez que si nous changeons la fonction de conversion pour qu'elle soit une fonction membre const, alors la conversion est ambiguë (parce que les deux ont un type de paramètre de A const& alors) : Le compilateur Comeau le rejette correctement, mais GCC l'accepte en mode non pédant. En passant à -pedantic fait sortir l'avertissement d'ambiguïté approprié aussi, cependant.

J'espère que cela vous aidera à mieux comprendre la différence entre ces deux formes !

0 votes

Wow. Je n'avais même pas réalisé pour la déclaration de fonction. Je dois accepter votre réponse parce que vous êtes le seul à le savoir. Y a-t-il une raison pour laquelle les déclarations de fonction fonctionnent de cette façon ? Ce serait mieux si c3 était traité différemment à l'intérieur d'une fonction.

5 votes

Bah, désolé les gars, mais j'ai dû enlever mon commentaire et le poster à nouveau, à cause du nouveau moteur de formatage : C'est parce que dans les paramètres des fonctions, R() == R(*)() y T[] == T* . En d'autres termes, les types de fonctions sont des pointeurs de fonctions, et les types de tableaux sont des pointeurs d'éléments. Ça craint. Il est possible de contourner le problème en A c3((A())); (les parenthèses autour de l'expression).

0 votes

@Johannes : Explication géniale, surtout l'expérimentation avec le petit code pourtant joliment écrit.

58voto

Mehrdad Afshari Points 204872

Affectation est différent de initialisation .

Les deux lignes suivantes font initialisation . Un seul appel au 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 de l'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;
}

2 votes

Bonne référence : "The C++ Programming Language, Special Edition" par Bjarne Stroustrup, section 10.4.4.1 (page 245). Décrit l'initialisation de la copie et l'affectation de la copie et explique pourquoi elles sont fondamentalement différentes (bien qu'elles utilisent toutes deux l'opérateur = comme syntaxe).

0 votes

Petit détail, mais je n'aime vraiment pas quand les gens disent que "A a( x )" et "A a = x" sont égaux. Strictement, elles ne le sont pas. Dans de nombreux cas, ils feront exactement la même chose, mais il est possible de créer des exemples où, en fonction de l'argument, différents constructeurs sont appelés.

0 votes

Je ne parle pas d'"équivalence syntaxique". Sémantiquement, les deux façons de initialisation sont les mêmes.

34voto

Kirill V. Lyadvinsky Points 47627

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 constucteur explicite, les appels explicites et implicites sont identiques.

9 votes

+1. Bonne réponse. C'est bien de noter aussi la version explicite. Au fait, il est important de noter que l'on ne peut pas avoir les deux d'une surcharge de constructeur unique en même temps. Donc, il ne compilerait pas dans le cas explicite. Si les deux compilent, ils doivent se comporter de manière similaire.

2 votes

Cela doit être la réponse acceptée ! Exemple court et clair.

5voto

John H. Points 143

A 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 la 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 pas créer une copie pour l'initialisation par copie lorsque cela peut être évité ; à la place, il appellera le constructeur directement -- c'est-à-dire, comme pour l'initialisation directe.

En d'autres termes, l'initialisation par copie est tout comme l'initialisation directe dans la plupart des cas <opinion> où un code compréhensible a été écrit. Puisque 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 que cela ressemble réellement à une initialisation.)</opinion>.

Le gigantisme technique : [12.2/1 suite du précédent] 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.

Heureusement que je n'écris pas un compilateur C++.

4voto

Charles Bailey Points 244082

Premier regroupement : cela dépend de ce que A_factory_func retours. La première ligne est un exemple de initialisation de la copie la deuxième ligne est initialisation directe . Si A_factory_func renvoie un A alors ils sont équivalents, ils appellent tous deux le constructeur de copie de l'objet A sinon la première version crée une valeur r de type A d'un opérateur de conversion disponible pour le type de retour de A_factory_func ou approprié A et ensuite appelle le constructeur de copie pour construire a1 de ce temporaire. La deuxième version essaie de trouver un constructeur approprié qui prend n'importe quel A_factory_func retourne, ou qui prend quelque chose en quoi la valeur de retour peut être implicitement convertie.

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

Troisième groupe : c1 est initialisé par défaut, c2 est initialisé par copie à partir d'une valeur initialisée temporaire. Tout membre de c1 qui ont un pod-type (ou des membres de membres, etc., etc.) peuvent ne pas être initialisés si les constructeurs par défaut fournis par l'utilisateur (s'il y en a) ne les initialisent pas explicitement. Pour c2 Dans le cas d'une copie, 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 s'ils ne sont pas explicitement initialisés). Comme litb l'a remarqué, c3 est un piège. Il s'agit en fait d'une déclaration de fonction.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X