salut,
pourquoi implémenter le constructeur de copie en utilisant la méthode swap ? versus une simple affectation ?
Merci.
salut,
pourquoi implémenter le constructeur de copie en utilisant la méthode swap ? versus une simple affectation ?
Merci.
Bonjour,
Ça n'existe pas le "SWAP IDIOM COPY CONSTRUCTOR".
L'idiome copy and swap est utilisé pour l'opérateur d'affectation pas pour le constructeur de copie.
Pourquoi quoi ?
Pourquoi on n'utilise pas l'idiome copy and swap pour le constructeur de copie ?
Parce que ça ne sert à rien. En cas d'exception, l'objet n'est tout simplement pas construit, il n'y a donc pas de précaution particulière à prendre pour conserver l'objet (qui n'existera pas) dans l'état initial ou dans un état cohérent.
En outre, que veux tu échanger puisque l'objet n'a pas d'existence (on est en train de le construire).
ok je reformule
pourquoi utilise t-on swap plutôt qu'une bête affectation.
je peux le formuler également comme ceci :
pourquoi
tmp=a
a=b
b=tmp
plutot que
a=b
pourquoi utilise t-on swap plutôt qu'une bête affectation.
Mais justement on ne le fait pas !
Il n'y a pas besoin de swap dans un constructeur par copie. Comme l'a dit gl l'objet n'est même pas encore construit, avec quoi ferait-on des échanges ?
Parce que avec le copy & swap il est beaucoup, *beaucoup* plus simple de coder un opérateur d'affectation correct (et en particulier qui donne de bonnes garanties face aux exceptions).
Mais attention :
1) Dans l'idiome copy-and-swap on n'utilise jamais le swap bête par défaut. Il faut rajouter à la classe une fonction swap un peu plus intelligente qui va swapper les parties internes à la classe.
2) il ne faut pas employer le copy-and-swap à tout bout de champ, en fait son utilisation est même plutôt rare. On n'utilise *pas* le copy-and-swap dans les cas suivants
- Si la classe n'est pas copiable. Ça peut paraître évident, mais perso vu que la plupart de mes classes sont des entités je désactive presque tout le temps la copie.
- Si la classe ne contient comme données membres que des types de base (int, float, double...). Pas besoin de coder l'op=, celui généré par défaut suffit.
- Si la classe ne contient comme données membres que des types qui savent se copier eux-même (c.a.d dont le copy ctor et l'op= est définit, comme par exemple std::string, std::map, std::shared_ptr...). Pas besoin non plus de coder l'op=, celui généré par défaut est là aussi suffisant
Finalement le copy-and-swap n'est généralement utile que si la classe est copiable, qu'elle possède des pointeurs nus sur des ressources, et que pour X raisons on ne peut pas encapsuler ces pointeurs nus dans des smarts pointeurs.
Donc c'est pas souvent mais quand on se retrouve dans ce cas le copy-and-swap est une bénédiction. Par exemple, je reprends l'exemple donné dans cet article éclairant. On a une classe TFoo toute bête :
Si l'on ne peut pas encapsuler les deux pointeurs dans des shared_ptr et si l'on ne veut pas utiliser le copy-and-swap, alors coder un op= correct pour cette classe tourne vite au cauchemar. L'article détaille un par un tous les pièges à éviter et fini par obtenir l'horreur (correcte!) suivante
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6 class TFoo : public TSuperFoo { TBar* fBar1; TBar* fBar2; // various method definitions go here... }
Ça m'étonnerais que beaucoup de programmeurs C++ arrivent à coder un op= comme celui-ci du premier coup ! Il faut une concentration extraordinaire. Alors qu'avec un petit coup d'idiome copy-and-swap, il suffit de définir une fonction swap très simple qui échange les deux pointeurs bar1 et bar2 et l'opérateur = devient simplissime à coder tout en étant correct même en cas d'exception :
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26 TFoo& TFoo::operator=(const TFoo& that) { if (this != &that) { TBar* bar1 = 0; TBar* bar2 = 0; try { bar1 = new TBar(*that.fBar1); bar2 = new TBar(*that.fBar2); } catch (...) { delete bar1; delete bar2; throw; } TSuperFoo::operator=(that); delete fBar1; fBar1 = bar1; delete fBar2; fBar2 = bar2; } return *this; }
Donc au final je dirais que le principal intérêt du copy-and-swap c'est d'économiser du temps de cerveau disponible.
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14 void TFoo::swap(const TFoo& that) { TSuperFoo::swap(that); std::swap(fBar1, that.fBar1); std::swap(fBar2, that.fBar2); } TFoo& TFoo::operator=(const TFoo& that) { TFoo tmp(that); this->swap(tmp); return *this; }
ok pas mal
mais pourquoi à la place de la méthode swap on a pas une méthode equal
qui fait de l'affectation plutot que du swap ?
parce que une affection n'est pas exception safe. Un swap oui.
Ben lis l'article que j'ai linké plus haut. Il explique en détail toutes les erreurs qu'on peut faire et tous les problèmes qu'on peut rencontrer si l'on essaye de faire des assignements avec une classe qui possède des pointeurs nus sur des ressources.
Edit : Puis bon tout bêtement si tu assignes des pointeurs sans se soucier de rien, alors tu vas avoir deux objets dont les membres pointent vers la même ressource, ressource qui sera détruite dès que l'un des deux objets sera détruit.
ça pourrait faire une entré dans la fac!
EDIT: bon je sens bien que mon dernier post va faire un flop
Salut,Parce que, justement, toutes les classes ne sont pas assignables
De plus, copy and swap est, justement, utilisé pour implémenter... l'opérateur d'affectation.
Et il est utilisé pour assurer... la libération correcte des ressources utilisées par... l'objet auquel on affecte la copie d'un autre.
L'échange des ressources entre la copie de l'objet affecté et l'objet qui subit l'affectation est, en définitive, le seul moyen d'assurer cette libération correcte.
Un petit exemple: Soit une classe (copiable et assignable) utilisant en interne un pointeur quelconque:
Si tu ne définis pas toi-même le constructeur par copie, celui qui sera fourni par le compilateur effectuera une copie membre à membre de la classe.
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10 class MaClass { public: MaClass(int i):ptr_(new Type[i]), nbr_(i){ assert(i>0);} /* pour éviter les fuites mémoires */ ~MaClass(){ delete [] ptr_;} private: Type * ptr_; int nbr_; };
Pour le membre nbr_, il n'y a aucun problème: la copie d'un type primitif se fait tout à fait correctement.
Mais, pour le membre ptr_; il faut savoir qu'un pointeur, ce n'est jamais... qu'une valeur numérique qui représente l'adresse à laquelle se trouve la variable pointée.
C'est donc l'adresse représentée par le pointeur qui serait copiée
Si donc tu copie l'objet, sous une forme proche de
obj.ptr_ pointe sur... la même adresse mémoire que cpy.ptr_.
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2 MaClass obj(7); Maclass cpy(obj);
Si tu modifie "ce qui est pointé" par obj.ptr_, tu modifie donc également ce qui... est pointé par cpy.ptr_, et inversement. Et ce n'est sans doute pas ce que tu aurais espéré
De plus, tu sera confronté à un autre problème:
Le destructeur va (tenter de) libérer la pointeur allouée à ptr_.
Que ce soit cpy ou obj qui soit détruit en premier, tu n'auras pas de problème lorsque le premier objet est détruit, mais, quand le second sera détruit, le destructeur de l'objet sera de nouveau invoqué, et tu auras une deuxième tentative de libération de la mémoire... ce qui enverra le programme "cueillir les pâquerettes"
Tu dois donc redéfinir le constructeur par copie de manière à faire en sorte que chaque copie utilise une adresse mémoire qui lui est propre pour ptr_, sous une forme sans doute proche de
Cela résout, certes, le problème de la copie, mais pas le problème de l'affectation...
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5 MaClass::MaClass(MaClass const & c):ptr_(new Type[c.nbr_]),nbr_(c.nbr_) { /* si Type est un type POD, la copie se fera sous la forme de */ memcpy(ptr_,ptr_, nbr_*sizeof(Type)); }
En effet, l'opérateur d'affectation généré automatiquement par le compilateur effectue... une affectation membre à membre.
Une même cause ayant les mêmes effets, tu te retrouves avec le même problèmes: l'affectation d'un pointeur fait que... deux pointeurs pointent... vers la même adresse, et, fatalement, tu encours le risque d'une double libération de la mémoire, avec le résultat que l'on connait
Mais il y a un problème supplémentaire: le membre ptr_ (dans mon exemple) de l'objet qui subit l'affectation pointe... vers une adresse mémoire qui a été allouée dynamiquement
Si on affecte, simplement, l'adresse pointée par l'un à l'autre, on perd... l'adresse pointée à l'origine, et on occasionne donc... une fuite mémoire, avec comme résultat certain, quel qu'en soit le terme, de finir par saturer le système, et de finir par rendre carrément le système d'exploitation totalement instable.
Tu comprendra surement que l'on ne peut se permettre une telle perspective
Il faut donc prendre des dispositions pour éviter d'en arriver là, en veillant à libérer correctement la mémoire allouée au membre ptr_ de l'objet qui subit l'affectation, mais, uniquement si... on a pu copier l'objet affecté
Or, si on en arrive à écrire un code proche de
1- this->ptr_ et cpy.ptr_ pointent... vers la même adresse mémoire... avec les problèmes que l'on connait. (n'oublie pas que cpy est automatiquement détruit lorsque l'on atteint l'accolade fermante de la portée dans laquelle il est déclaré )
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7 MaClass & MaClass::operator = (MaClass const & c) { MaClass cpy(c); // on copie correctement l'objet assigné ptr_=cpy.ptr_; //crack nbr_=cpy.nbr_; return *this; } // BOUM (1)
2- on n'empêche pas la fuite mémoire
Il faut donc bel et bien faire en sorte que:
Cela s'appelle... un échange de données, autrement dit, un swap
- l'adresse mémoire représentée par this->ptr soit utilisée par... cpy.ptr (pour que ce soit l'adresse qui était à l'origine représentée par this->ptr qui soit libérée)
- m'adresse mémoire représentée par cpy.ptr soit utilisée par... this->ptr_
Maintenant, la manière dont tu t'organise pour échanger les adresses représentées par this->ptr_ et par cpy.ptr_ ne regarde que toi, mais il faut avouer que, entre:
d'une part
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3 Type * temp = ptr_; ptr_=cpy.ptr_; cpy.ptr_;
d'autre part et enfin
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3 ptr_^=cpy.ptr_; cpy.ptr_^=ptr_; ptr_^=cpy.ptr_;
(une simple affectation suffit parfaitement pour nbr_ )
Code : Sélectionner tout - Visualiser dans une fenêtre à part std::swap(ptr_,cpy.ptr_);
La troisième manière de faire est, en définitive, plus simple à appliquer (y a qu'une instruction à écrire, au lieu de 3) et permet, en outre, de clairement indiquer ce que l'on fait, y compris à quelqu'un de très distrait
A méditer: La solution la plus simple est toujours la moins compliquée
Ce qui se conçoit bien s'énonce clairement, et les mots pour le dire vous viennent aisément. Nicolas Boileau
Compiler Gcc sous windows avec MinGW
Coder efficacement en C++ : dans les bacs le 17 février 2014
mon tout nouveau blog
Oui, d'ailleurs c'est le cas : Comment écrire un opérateur d'affectation correct ?
voilà ça c'est convainquant!merci
je suis toujours interrogatif quant à l'argument d'exception safe pour tenter de justifier l'emploi de swap, car selon moi un swap réalise également une affectation (plusieurs même), et donc si l'affectation n'est pas exception safe à fortiori la méthode swap non plus...
PS:Si l'on utilise un shared_ptr, on a donc pas à ce soucier de ça ?idem pour les ptr_vector and co ?
Et pourtant, c'est bien l'exception safety le coeur du problème. Si l'affectation ne pouvait pas échouer, il n'y aurait pas besoin de cet idiome.
En effet le swap reste basiquement une suite d'affectation mais portant sur des éléments dont l'affetation ne lance pas d'exception (entre autres des types primitifs dont int et pointeur). En particulier il n'y a aucune acquisition de ressources dans le swap.
Pour faire simple, l'idiome copy and swap se décompose en trois étapes :
- Construction d'un objet temporaire. C'est dans cette phase que l'on fait l'acquisition des ressources et que l'on risque de recevoir une exception. Si une telle exception survient, l'objet temporaire est proprement détruit et l'objet initial n'est pas modifié.
- Echange de l'objet temporaire et de l'objet de destination. Cet échange s'effectue grâce à des opérations qui ne peuvent pas échouer (dont des affectations de types primitifs).
- Destruction de l'objet temporaire (puisqu'on sort de la portée où il est défini) qui a maintenant la responsabilité des ressources initiales de l'objet de destination qui seront donc libérées.
Effectivement, cet idiome permet de gérerla libération des ressources devenues inutiles ainsi que l'auto-affectation, mais ces deux points restent relativement simples à implémenter sans recourir à cet idiome.
Là où il prend réellement son importance c'est pour garantir qu'en cas d'exception lors de l'acquisition des ressources, tout ce qui a été déjà acquis sera libéré et l'objet de destination n'est pas modifié (et est donc dans un état cohérent et connu).
une petite question :
dans le code de boost::shared_ptr l'operateur d'affectation est defini ainsi :
Pourquoi pas de swap ici ?
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6 shared_ptr & operator=(shared_ptr const & r) // never throws { px = r.px; pn = r.pn; // shared_count::op= doesn't throw return *this; }
Vous avez un bloqueur de publicités installé.
Le Club Developpez.com n'affiche que des publicités IT, discrètes et non intrusives.
Afin que nous puissions continuer à vous fournir gratuitement du contenu de qualité, merci de nous soutenir en désactivant votre bloqueur de publicités sur Developpez.com.
Partager