45 votes

Que se passe-t-il techniquement dans ce code C++ ?

J'ai une classe B qui contient un vecteur de classe A . Je veux initialiser ce vecteur par le constructeur. Classe A produit des informations de débogage pour que je puisse voir quand il est construit, détruit, copié ou déplacé.

#include <vector>
#include <iostream>

using namespace std;

class A {
public:
    A()           { cout << "A::A" << endl; }        
    ~A()          { cout << "A::~A" << endl; }               
    A(const A& t) { cout <<"A::A(A&)" << endl; }              
    A(A&& t)      { cout << "A::A(A&&)" << endl; }            
};

class B {
public:
    vector<A> va;
    B(const vector<A>& va) : va(va) {};
};

int main(void) {
    B b({ A() });
    return 0;
}

Maintenant, lorsque j'exécute ce programme (Compilé avec l'option GCC -fno-elide-constructors pour que les appels au constructeur de déplacement ne soient pas optimisés), j'obtiens le résultat suivant :

A::A
A::A(A&&)
A::A(A&&)
A::A(A&)
A::A(A&)
A::~A
A::~A
A::~A
A::~A
A::~A

Ainsi, au lieu d'une seule instance de A le compilateur en génère cinq instances. A est déplacé deux fois et il est copié deux fois. Je ne m'attendais pas à cela. Le vecteur est passé par référence au constructeur, puis copié dans le champ de la classe. Je me serais donc attendu à une seule opération de copie ou même juste une opération de déplacement (parce que j'espérais que le vecteur que je passe au constructeur est juste une valeur de référence), pas deux copies et deux déplacements. Quelqu'un peut-il m'expliquer ce qui se passe exactement dans ce code ? Où et pourquoi crée-t-il toutes ces copies de A ?

42voto

T.C. Points 22510

Il peut être utile de parcourir les appels au constructeur dans l'ordre inverse.

B b({ A() });

Pour construire un B le compilateur doit appeler le constructeur de B qui prend une valeur de const vector<A>& . Ce constructeur doit à son tour faire une copie du vecteur, y compris de tous ses éléments. C'est le deuxième appel au constructeur de copie que vous voyez.

Pour construire le vecteur temporaire qui sera transmis à B le compilateur doit invoquer le constructeur de la classe initializer_list constructeur de std::vector . Ce constructeur, à son tour, doit faire une copie de ce qui est contenu dans le dossier de l'utilisateur. initializer_list * . C'est le premier appel au constructeur de copie que vous voyez.

La norme précise comment initializer_list sont construits dans §8.5.4 [dcl.init.list]/p5 :

Un objet de type std::initializer_list<E> est construit à partir d'une liste d'initialisation comme si l'implémentation allouait un tableau de N éléments de type const E ** où N est le nombre d'éléments de la liste d'initialisation. liste d'initialisation. Chaque élément de ce tableau est initialisé par copie avec l'élément correspondant de la liste des initialisateurs, et le std::initializer_list<E> est construit pour faire référence à ce tableau.

La copie-initialisation d'un objet à partir d'un objet du même type utilise la résolution de surcharge pour sélectionner le constructeur à utiliser (§8.5 [dcl.init]/p17), donc avec une rvalue du même type, elle invoquera le constructeur move s'il est disponible. Ainsi, pour construire l'objet initializer_list<A> à partir de la liste des initialisateurs entre accolades, le compilateur construira d'abord un tableau d'un const A en passant du temporaire A construit par A() ce qui provoque l'appel du constructeur "move", puis la construction de l'objet initializer_list pour faire référence à ce tableau.

Je n'arrive pas à comprendre d'où vient l'autre mouvement dans g++, par contre. initializer_list ne sont généralement rien d'autre qu'une paire de pointeurs, et la norme exige que la copie de l'un d'entre eux n'entraîne pas la copie des éléments sous-jacents. g++ semble considérer que appeler deux fois le constructeur de mouvement lors de la création d'un initializer_list d'un temporaire. Il a même appelle le constructeur de mouvement lors de la construction d'un initializer_list à partir d'une lvalue.

Je pense qu'il s'agit d'une mise en œuvre littérale de l'exemple non normatif de la norme. La norme fournit l'exemple suivant :

struct X {
    X(std::initializer_list<double> v);
};

X x{ 1,2,3 };

L'initialisation sera implémentée d'une manière à peu près équivalente à ceci : **

const double __a[3] = {double{1}, double{2}, double{3}};
X x(std::initializer_list<double>(__a, __a+3));

en supposant que l'implémentation peut construire un objet initializer_list avec une paire de pointeurs.

Donc, si vous prenez cet exemple au pied de la lettre, le tableau sous-jacent à l'élément initializer_list dans notre cas, sera construit comme si par :

const A __a[1] = { A{A()} };

ce qui entraîne deux appels au constructeur mobile parce qu'il construit un fichier temporaire A copier-initialiser un deuxième fichier temporaire A du premier, puis copie-initialise le membre du tableau du second temporaire. Le texte normatif de la norme, cependant, indique clairement qu'il ne devrait y avoir qu'une seule copie-initialisation, et non deux, donc cela semble être un bug.

Enfin, le premier A::A provient directement de A() .

Il n'y a pas grand-chose à dire sur les appels au destructeur. Tous les temporaires (quel que soit leur nombre) créés lors de la construction de b sera détruite à la fin de la déclaration dans l'ordre inverse de la construction, et celle A stocké dans b sera détruite lorsque b sort du champ d'application.


* Le site <code>initializer_list</code> des conteneurs de la bibliothèque standard sont définis comme étant équivalents à l'invocation du constructeur prenant deux itérateurs avec la valeur <code>list.begin()</code> et <code>list.end()</code> . Ces fonctions membres renvoient un <code>const T*</code> Il ne peut donc pas être déplacé. En C++14, le tableau de sauvegarde est rendu <code>const</code> Il est donc encore plus clair que vous ne pouvez pas vous en éloigner ou le changer.

** Cette réponse citait à l'origine la norme N3337 (la norme C++11 plus quelques modifications éditoriales mineures), dans laquelle le tableau a des éléments de type <code>E</code> plutôt que <code>const E</code> et le tableau dans l'exemple étant de type <code>double</code> . En C++14, le tableau sous-jacent est devenu <code>const</code> à la suite de <a href="http://www.open-std.org/jtc1/sc22/wg21/docs/cwg_defects.html#1418" rel="nofollow">CWG 1418 </a>.

5voto

Mine Points 924

Essayez de diviser un peu le code pour mieux comprendre le comportement :

int main(void) {
    cout<<"Begin"<<endl;
    vector<A> va({A()});

    cout<<"After va;"<<endl;
    B b(va);

    cout<<"After b;"<<endl;
    return 0;
}

Le résultat est similaire (notez le -fno-elide-constructors est utilisé)

Begin
A::A        <-- temp A()
A::A(A&&)   <-- moved to initializer_list
A::A(A&&)   <-- no idea, but as @Manu343726, it's moved to vector's ctor
A::A(A&)    <-- copied to vector's element
A::~A
A::~A
A::~A
After va;
A::A(A&)    <-- copied to B's va
After b;
A::~A
A::~A

3voto

Manu343726 Points 8803

Considérez ceci :

  1. Le temporaire A est instancié : A()
  2. Cette instance est déplacée dans la liste des initialisateurs : A(A&&)
  3. La liste d'initialisation est déplacée vers le vecteur ctor, donc ses éléments sont déplacés : A(A&&) . EDIT : Comme T.C. l'a remarqué, les éléments de l'initializer_list ne sont pas déplacés/copiés lors du déplacement/copie de l'initializer_list. Comme le montre son exemple de code, il semble que deux appels de ctor rvalue soient utilisés pendant l'initialisation de l'initializer_list.
  4. L'élément du vecteur est initialisé par valeur, au lieu d'être initialisé par move (Pourquoi ?, je ne suis pas sûr) : A(const A&) EDIT : Encore une fois, ce n'est pas le vecteur mais la liste d'initialisation.
  5. Votre ctor récupère ce vecteur temporel et le copie (Note votre initialisateur de vecteur), donc les éléments sont copiés : A(const A&)

0voto

BЈовић Points 28674

A::A

Le constructeur est exécuté, lorsque l'objet temporaire est créé.

premier A::A(A&&)

L'objet temporaire est déplacé dans la liste d'initialisation (qui est aussi rvalue).

seconde A::A(A&&)

La liste d'initialisation est déplacée dans le constructeur du vecteur.

premier A::A(A&)

Le vecteur est copié car le constructeur de B prend la lvalue, et une rvalue est passée.

seconde A::A(A&)

Encore une fois, le vecteur est copié lors de la création de la variable membre de la B va .

A::~A
A::~A
A::~A
A::~A
A::~A

Le destructeur est appelé pour chaque rvalue et lvalue (chaque fois que les constructeurs, copy ou move sont appelés, le destructeur est exécuté lorsque les objets sont détruits).

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