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'où la définition de l'operateur d'assigment comme ça
plutôt que comme ça
Code : Sélectionner tout - Visualiser dans une fenêtre à part T& operator =(T t)
dû à l'optimisation du passage par valeur i.e: copy elision
Code : Sélectionner tout - Visualiser dans une fenêtre à part T& operartor=(const T& t)
Mais il est impossible d'avoir une élision de copie, quoi qu'il advienne!!!
Tu ne peux avoir une élision de copie qu'à partir du moment où tu as la certitude que la copie reste inchangée entre le moment où elle est créée et le moment où elle est renvoyée.
si tu dois, pour les besoins impérieux de ta fonction, modifier la copie, que ce soit en swappant certaines données ou simplement en les modifiant avant de renvoyer la dite copie, il n'y a, à mon sens du moins (et depuis le temps que je l'affirme, je présumes que quelqu'un se serait fait un plaisir de me contredire si je me trompais sur ce coup là ) aucune optimisation possible en terme d'élision de copie!!!
Si tu décides de ne pas créer une copie qui est destinée à être modifiée, dis moi à quoi tu compte appliquer les changements en question, c'est aussi simple que ca
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
La seule certitude que tu as en déclarant operator=(T t), c'est que la copie sera bel et bien effectuée, l'argument étant passé par valeur.
Cela ne signifie absolument pas qu'il y ait élision de la copie, bien au contraire, ca signifie que tu force le compilateur à faire une copie, vu qu'il ne pourra pas l'élider pour cause de modification
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
justement non!
prends par exemple le cas suivant:
le retour de func sera directemnt injecté en lieu et place du paramètre de l'operateur d'assignement et aucun copie n'aura lieu (dans le cas de la signature suivante T& operator =(T t))
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3 T func(){ return T(); } T f; f = func();
Oui, parce que T n'est pas modifié entre le moment où il est créé et le moment où il est renvoyé.
On pourrais donc tout à fait avoir élision de la copie avec un code proche de
et ce, même s'il y a effectivement du code entre la création de temp et son renvoi (pour autant qu'il n'y ait pas modification (j'en doute un peu, mais, après tout, pourquoi pas ).
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7 T func(){ T temp; /* !!! AUCUNE MODIFICATION DE TEMP ICI !!! */ return temp; } T f; f = func();
Je serais tout aussi d'accord pour admettre la possibilité d'une élision de copie dans le cadre de la structure Point que j'ai présentée dans mes deux ou trois interventions précédentes, dans le sens où l'opérateur d'affectation n'aurait aucun besoin de modifier la copie de l'objet qu'il obtient.
Mais, dans le cadre particulier d'une classe devant gérer des ressources allouées dynamiquement, tu n'as aucun moyen (*) d'empêcher la création d'une "copie de travail" d'éviter l'ensemble des écueils relatifs à la gestion manuelle de la mémoire.
(*) enfin, qui évite autant que faire se peut d'avoir à dupliquer une partie du code du constructeur par copie et une autre partie du destructeur
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
Mais ca ne vaut que parce que l'opérateur d'affectation est trivial.
Or, la règle des trois grands te dit que, à partir du moment où tu fournis une implémentation particulière pour le constructeur par copie, l'opérateur d'affectation ou le destructeur, tu es obligé de fournir une implémentation particulière pour ces trois fonctions.
Tu ne peux donc pas avoir un opérateur d'affectation trivial dés le moment où tu as un destructeur qui fait un delete sur ne serait-ce qu'un seul de ses objets membres, parce qu'il faut éviter que la copie n'occasionne un partage des ressources, et parce qu'il faut éviter que l'affectation n'occasionne soit un partage des ressources soit une fuite mémoire.
Et le seul moyen d'éviter la copie intégrale de l'objet serait d'avoir un opérateur d'affectation qui reprend (au moins en partie) le code du constructeur par copie et celui du destructeur, sous une forme proche de
(en plus, à bien y réfléchir, tu en arrive quand même à avoir copié l'intégralité de l'objet, car size_ = rhs.size_ ne fait rien d'autre que... copier l'entier ))
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9 TableauInt & operator= (TableauInt /* const & */ rhs){ int * temp = new int[rhs.size_]; // on profite que new lance std::bad_alloc pour s'éviter la vérification memcpy(temp,rhs.ptr_, rhs.size_* sizeof(int)); /* Maintenant, on peut détruire ptr_ */ delete [] ptr_; ptr_=temp; size_ = rhs.size_; return *this; }
Mais à ce moment là, tu as tout intérêt à effectivement passer rhs par référence constante plutôt que par valeur, afin d'être certain qu'il n'y aura de toutes façons pas de copie du paramètre, parce qu'il serait totalement aberrant de le passer par valeur, et donc de laisser la possibilité au compilateur de créer effectivement la copie.
C'est pour cela que je dis que, à partir du moment où tu te trouves dans une situation dans laquelle tu dois redéfinir l'opérateur d'affectation, il n'y a plus aucun moyen d'assurer une élision de copie, simplement parce que tu devras, de toutes manières, faire une copie de la totalité des membres de ton objet
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
je suis désolé mais encore une fois non. Cela est valable quel que soit le niveau de complexité de ton opérateur d'affectation.
@koala01: La condition "pas de modification" c'est toi qui l'invente, jamais la norme ne mentionne une telle condition. Les conditions sont plutôt liées aux types (après avoir enlevé les qualificatifs const/volatile), et le caractère volatile de l'objet cible.
Par contre tester la "présence" effective de cette elision n'est pas simple (ce n'est pas réelement faisable en introduisant du code : il est suceptible de changer les décisions du compilateur), je dirais même que c'est impossible à moins que le compilateur offre un outil qui te permet de les visualiser directement. Le meileur test étant encore de faire deux versions d'une même classe, avec const T& et T, et bencher dans le cas d'une elision possible (en optimisé bien entendu), si les performances sont différences, elles seront surment le signe d'une elision.
Je ne vois pas ce qui te fait penser que cette condition de "non modification" soit nécessaire ? L'elision c'est réalisé en construisant directement l'objet dans la cible, les modifications faites entre l'original et la cible sont tout simplement directement affectés à la cible. Je suis loin de pouvoir développer un compilateur, mais ca me semble être un simple traitement de l'AST.
Ce caractère un peu magique et non totalement déterministe (ie ca dépend du compilateur), est une des raisons pour profiter de la move-semantic quand on peut et de ne pas juste compter sur l'elision. Elle [la move-semantic] est déterministe.
Du coup ça m'a donné envie de tester
std::chrono n'a pas l'air d'avoir la précision de QueryPerformanceCounter sous Windows, du coup j'utilise QueryPerformanceCounter si CHRONO n'est pas défini.
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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149 //g++ -std=c++11 -O3 -DCHRONO -o foo foo.cpp && ./foo 2> /dev/null #include <iostream> #include <ctime> #include <memory> #include <cstring> const int size = 5000000; const int nb_tests = 100; #ifdef CHRONO # include <chrono> # define RESET duration = 0; # define START t0 = clock::now(); # define END duration += duration_cast<time_type>(clock::now() - t0).count(); #else # define WIN32_LEAN_AND_MEAN # include <Windows.h> # define RESET duration = 0.0; # define START QueryPerformanceCounter(&begin); # define END QueryPerformanceCounter(&end); duration += (end.QuadPart - begin.QuadPart) / freq; #endif // CHRONO class TableauIntRef { private: int* tableau; int taille; public : explicit TableauIntRef(): taille(0), tableau(nullptr) { } explicit TableauIntRef(int t): taille(t), tableau(new int[t]) { } explicit TableauIntRef(const TableauIntRef& t): taille(t.taille), tableau(new int[t.taille]) { memcpy(tableau, t.tableau, sizeof(int) * taille); } ~TableauIntRef() { delete tableau; } int getElement(int i) const { return tableau[i]; } void setElement(int i, int elt) { tableau[i] = elt; } int getTaille() const { return taille; } void swap(TableauIntRef& t) { std::swap(tableau, t.tableau); std::swap(taille, t.taille); } TableauIntRef& operator=(const TableauIntRef& t) { TableauIntRef cp(t); swap(cp); return *this; } }; class TableauIntCpy { private: int* tableau; int taille; public : explicit TableauIntCpy(): taille(0), tableau(nullptr) { } explicit TableauIntCpy(int t): taille(t), tableau(new int[t]) { } TableauIntCpy(const TableauIntCpy& t): taille(t.taille), tableau(new int[t.taille]) { memcpy(tableau, t.tableau, sizeof(int) * taille); } ~TableauIntCpy() { delete tableau; } int getElement(int i) const { return tableau[i]; } void setElement(int i, int elt) { tableau[i] = elt; } int getTaille() const { return taille; } void swap(TableauIntCpy& t) { std::swap(tableau, t.tableau); std::swap(taille, t.taille); } TableauIntCpy& operator=(TableauIntCpy cp) { swap(cp); return *this; } }; template <class T> void useData(T& t0, T& t1) { int a=0, b=0; for(int i=0; i<size; ++i) { t0.setElement(i, rand()); } for(int i=0; i<size; ++i) { a += t0.getElement(i); b += t1.getElement(i); } std::cerr << a << b << std::endl; } int main(int argc, char **argv) { #ifdef CHRONO typedef std::chrono::high_resolution_clock clock; typedef clock::time_point time_point; typedef std::chrono::microseconds time_type; using std::chrono::duration_cast; time_point t0; long long duration; #else LARGE_INTEGER qfreq, begin, end; QueryPerformanceFrequency(&qfreq); double freq = qfreq.QuadPart / (double) 1000000; // µs double duration; #endif // CHRONO TableauIntRef r0(size), r1; TableauIntCpy c0(size), c1; srand(time(0)); for(int i=0; i<size; ++i) { r0.setElement(i, rand()); c0.setElement(i, rand()); } RESET; for(int i=0; i<nb_tests; ++i) { START; r1 = r0; END; // utiliser les données pour pas que le compilo vire le code useData(r0, r1); } std::cout << duration / (double)nb_tests << "us (ref)" << std::endl; RESET; for(int i=0; i<nb_tests; ++i) { START; c1 = c0; END; // utiliser les données pour pas que le compilo vire le code useData(c0, c1); } std::cout << duration / (double)nb_tests << "us (copy)" << std::endl; return 0; }
g++ 4.8, binaire 32bits, sur une VM openSuse 32 bits
9308.129344.98 µs (ref constante)
8493.0210317.2 µs (copie)
VS2012, binaire 32 bits, sur Windows 7 64bits
5722.985724.37 µs (ref constante)
5694.415738.59 µs (copie)
VS2012, binaire 64 bits, sur Windows 7 64bits
5891.855988.55 µs (ref constante)
5833.755923.47 µs (copie)
On dirait bien qu'une optimisation est possible. (sous g++ du moins )
edit:
Satané copié / collé...
Le gain vient de la mise en cache du coup, je rédit avec les bons temps bientot.
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2 TableauIntRef r0(size), r1; TableauIntRef c0(size), c1;
re-édit: voila les bons temps.
Bah c'est moins glorieux d'un coup , g++ n'a pas trop l'air d'aimer la copie explicite.
Mais je crois qu'elle tombe sous le sens...
Expliques moi comment tu veux envisager envisager de modifier la copie si... tu n'a pas de copie à modifier
L'idée, c'est que l'opérateur d'affectation puisse ne pas forcément faire appel au constructeur par copie pour affecter correctement les différents membres de ta classe ou de ta structure (ou alors, c'est que j'ai vraiment mal compris le principe de l'élision de copie )
Le problème pour que cela puisse fonctionner, c'est qu'il faut que le comportement global de l'opérateur d'affectation soit identique à celui du constructeur par copie.
Or, selon moi (mais prouvez moi que je me trompe ) on ne peut être sur que c'est le cas que si la copie "superficielle" des membres est suffisante, à l'instar de l'implémentation que fournit le compilateur.
Alors, oui, tant que l'opérateur d'affectation ne fait, en définitive, rien d'autre que copier les membre de l'objet qu'il reçoit en argument à la place de ses propres membres (ou, comme ce sera sans doute le cas, affecter la valeur des membres de l'objet passé en paramètre à ses propres membres), il n'y a strictement aucun problème, il y a de fortes chances que l'élision de la copie puisse effectivement se faire, à la condition toutefois que l'élision de la copie puisse, aussi, se faire pour les membres en question
Ainsi, pour reprendre mon exemple de la tantôt, je ne doute absolument pas que l'on observera une élision de copie sur une classe proche de
(oui, je ne voulais pas donner l'impression que je ne l'envisageais que pour des type POD ) soit tout à fait possible.
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9 class Point{ public: Point(int x, int y):x_(x),y_(y){} int x() const{return x_;} int y() const{return y_;} private: int x_; int y_; };
La raison est ici simple : x_ et y_ sont trivialement constructible, trivialement copiable et trivialement assignables.
L'opérateur d'affectation ne doit strictement rien faire d'autre que ce que fait le constructeur par copie.
Par contre, dés le moment où la gestion dynamique de la mémoire entre en jeu, il en va tout autrement.
Le problème commence avec le fait qu'il faut s'assurer que la mémoire allouée à un pointeur est correctement libérée lorsque l'objet est détruit.
On commence donc tout naturellement par implémenter un destructeur en y mettant le delete qui va bien.
On continue fatalement en se disant que c'est très bien de veiller à libérer la mémoire, mais que si l'on écrit un code proche de
il faut absoluement veiller à ce que t et copy ne partagent pas les même ressources pour éviter la tentative de double libération de la mémoire.
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2 UnType t; UnType copy(t);
On implémente donc le constructeur par copie de manière à ce qu'il fasse une copie en profondeur:A partir de là, je ne vois sincèrement aucun moyen d'éviter la copie, pour la simple et bonne raison que, si on ne fait pas la copie en profondeur, on se retrouvera avec un pointeur pointant sur une adresse mémoire qui risque d'être libérée à plusieurs endroits.
- Allocation d'une zone de mémoire suffisante pour maintenir l'ensemble des données
- copie des données de l'objet d'origine dans l'espace nouvellement alloué
On termine enfin en se disant que c'est très bien de permettre que la copie de l'objet évite de partager ses ressources avec l'objet d'origine, mais qu'il faut aussi veiller à ce que l'opérateur d'affectation occasionne la libération correcte de la mémoire allouée à l'origine, avant d'en perdre toute trace (règle des trois grands).
Alors, bien sur, on peut s'amuser à tout faire (allocation de la mémoire pour un pointeur temporaire, copie du contenu de l'objet assigné dans la mémoire allouée au pointeur temporaire, libération du pointeur d'origine, affectation de la mémoire allouée au pointeur temporaire au pointeur d'origine et affectation des membres "classiques" un à un) comme je l'ai fait dans mon intervention précédente, mais, encore une fois, cela revient strictement à... effectuer une copie de l'objet
Pour éviter les duplications de code (car on se retrouve au final avec une partie du code du constructeur par copie et une partie du code du destructeur dans l'opérateur d'affectation) et s'assurer que "tout est fait dans le bon ordre", on préfère utiliser l'idiome "copy and swap": on crée une copie (implicite ou explicite, là est la question à la base ), et on "swape" les différents membres.
J'hésite d'ailleurs fortement à affirmer (car je n'en ai aucune preuve, si ce n'est mon intuition ) que la copie ne pourra être éludée que si tous les membres sont trivialement copiable (et affectable) de manière récursive.
Ainsi, je ne crois sincèrement pas qu'il soit possible d'envisager d'éluder une copie (sauf en utilisant le principe de la "lazy copy") d'une structure proche de
pour la simple et bonne raison que, bien qu'elle soit défaut constructible, copiable et affectable, std::string fournit (de manière totalement transparente pour l'utilisateur, je vous l'accorde) des comportements spécifiques de gestion dynamique de la mémoire qui doivent être pris en compte au niveau de la copie et de l'affectation
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4 struct Adresse{ int numro; std::string rue; };
Encore une fois, peut etre ne fais-je simplement pas assez confiance à mon compilateur sur ce coup là, et peut etre que je n'ai qu'une vision bien étriquée sur le sujet, mais dans ce cas, montrez moi le défaut dans mon raisonnement
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
Et en sauvegardant 100 fois le résultat de ce tests, tu verrais peut etre certaines valeurs s'inverser plus ou moins régulièrement
Plus sérieusement...
Une différence maximale d'une milli seconde (même pas tout à fait, à voir tes chiffres ) peut parfaitement s'expliquer par le simple fait que ton système d'exploitation ait décidé, à un moment donné, qu'il était temps de "donner la main" à un autre processus dont la priorité était devenue supérieure à celle de ton test
Il n'est pas impossible que tu obtiennes des résultats en permanence très similaires en faisant tourner ce test de manière prolongée mais j'aurais personnellement tendance à mettre ces différences sur le fait que l'on ne travaille, malheureusement, pas sur des machines en temps réel (enfin, en temps constant)
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
En 3 le compilateur peut élider la copie, il construit directement l'objet en 1, cet objet sera détruit en 2. Je ne vois pas de problème.
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13 A& operator=(A a) { //1 using std::swap; swap(*this,a); return *this; //2 } //... A a; a = A(); //3
Le cas plus complexe c'est celui-ci :
En 5 le compilateur peut élider la copie et construire l'objet directement en 1, il sera détruit en 2. En 2 il peut élider la copie de 3 et le construire directement en 4, il sera détruit plus tard (en sorti du scope de c). Au final tu n'as aucune copie, juste la construction de ton temporaire et de ton objet. Et je ne vois pas ce que des éventuelles modification en 6 viendrait changer.
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13 A foo(A a) { //1 A b; //3 //5 using std::swap; swap(a,b); return b; //2 } A c /*4*/ = foo(A()/*5*/);
Bench adapté de celui de Stepanov pour quantifier la pénalité d'abstraction :
VC++ en Ox Ob2 Ot
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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72 #include <stddef.h> #include <stdio.h> #include <time.h> #include <math.h> #include <stdlib.h> #include<utility> int iterations = INT_MAX/50; int current_test = 0; double result_times[2]; void summarize() { printf("\ntest absolute\n"); printf("number time\n\n"); int i; for (i = 0; i < current_test; ++i) printf("%2i %5.2fsec\n", i, result_times[i]); } clock_t start_time, end_time; inline void start_timer() { start_time = clock(); } inline double timer() { end_time = clock(); return (end_time - start_time)/double(CLOCKS_PER_SEC); } template <class T> void test() { int i; T t; start_timer(); for(i = 0; i < iterations; ++i) t = T(); result_times[current_test++] = timer(); t.foo(); } struct A { int* i; A() : i(new int) {} A(const A& a) : i(new int(*a.i)) {} A& operator=(const A& a) { A b(a); std::swap(i,b.i); return *this; } ~A() { delete i; } void foo(){} }; struct B { int* i; B() : i(new int) {} B(const B& a) : i(new int(*a.i)) {} B& operator=(B a) { std::swap(i,a.i); return *this; } ~B() { delete i; } void foo(){} }; int main() { test<A>(); test<B>(); summarize(); }
GCC 4.7.1 en O2test absolute
number time
0 8.79sec
1 4.41sec
Appuyez sur une touche pour continuer...
Processeur : Intel Core i3 à 2.10 GHzg++ (tdm64-1) 4.7.1
Copyright (C) 2012 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
test absolute
number time
0 12.11sec
1 5.43sec
Appuyez sur une touche pour continuer...
OS : Windows Seven SP1
Tant que l'ensemble des membres de A est trivialement constructible, je ne vois strictement aucune objection.
Là où je mets en doute la capacité d'éluder la copie, c'est lorsque la copie en elle-même ne peut pas se contenter d'être superficielle.
Nous sommes cependant d'accord que le (1) pourrait parfaitement être explicite si l'argument était passé par référence: c'est une copie obligatoire
1' : comment pourrais tu éviter la copie en (1) (qu'elle soit implicite ou explicite, je te l'accorde), alors que tu dois t'assurer qu'un des membres qui est un pointeur pointe sur un espace mémoire suffisant pour représenter l'ensemble des données pointées par le pointeur équivalent de a et qu'elles ont été correctement copiées
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13 A& operator(A const & a) { A temp(a);//1 using std::swap; swap(*this,a); // 1' return *this; //2 } //... A a; a = A(); //3
Tu ne peux pas te contenter de copier ce "qui est pointé par le pointeur" de a sur la pile, et encore moins décider de fournir l'adresse sur la pile de cette donnée à ton pointeur pour lequel tu vas appeler delete!
Tout à fait d'accord, tant que la copie ne nécessite rien d'autre qu'une copie en surface.En 3 le compilateur peut élider la copie, il construit directement l'objet en 1, cet objet sera détruit en 2. Je ne vois pas de problème.
Le cas plus complexe c'est celui-ci :
En 5 le compilateur peut élider la copie et construire l'objet directement en 1, il sera détruit en 2. En 2 il peut élider la copie de 3 et le construire directement en 4, il sera détruit plus tard (en sorti du scope de c). Au final tu n'as aucune copie, juste la construction de ton temporaire et de ton objet. Et je ne vois pas ce que des éventuelles modification en 6 viendrait changer.
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13 A foo(A a) { //1 A b; //3 //5 using std::swap; swap(a,b); return b; //2 } A c /*4*/ = foo(A()/*5*/);
Mais il faut prendre conscience du fait que l'appel à new a un effet bien particulier qui est de te donner l'entière responsabilité de la mémoire qui a été allouée par cette commande.
Si, pour une raison ou une autre, tu perds l'adresse de cette mémoire sans l'avoir explicitement libérée, tu obtiens une fuite mémoire que seul l'arrêt total de l'application pourra résoudre et si la fuite mémoire survient de manière trop systématique, c'est la stabilité de carrément tout le système qui risque d'être compromise à terme.
Comme tu ne peux pas remplacer une adresse pour laquelle il faut appeler delete par une adresse de la pile tu es obligé de veiller à ce que l'adresse de remplacement soit aussi une adresse dynamiquement allouée.
Comme, de plus, cette adresse dynamiquement allouée doit refléter un état bien particulier qui n'est pas forcément l'état qu'elle aurait lors de la création par défaut de l'objet, tu es, en plus, obligé de veiller à ce qu'elle reflète l'état actuel de l'objet copié.
Tu ne pourrais pas envisager décemment d'intervertir les pointeurs de la copie (quelle que soit la manière dont tu l'obtiens) et de l'objet se trouvant à gauche de l'opérateur sans avoir la certitude que ces deux conditions soient remplies.
Alors, bien sur, il reste le cas de "tous les autres membres" qui sont (peut on l'espérere) trivialement constructible et trivialement copiables.
Mais, toute choses étant égales, que tu les copies "directement" depuis leur origine vers leur destination ou que tu utilises le constructeur par copie afin de t'assurer qu'ils présentent eux aussi un état correct par rapport à l'objet d'origine ne fera sans doute pas énormément de différence.
La seule différence que j'envisage effectivement, c'est qu'il n'est peut etre pas obligatoire de swaper ces valeurs, mais qu'il est peut etre préférable d'assigner directement les valeurs en question
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
Si je suis bien, ton problème ce situe dans un cas où A ressemble à un truc comme :
Je ne vois pas où est le problème. Prenons :
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8 struct A { int* i; A() : i(new int) {} A(const A& a) : i(new int(*a.i)) {} ~A() { delete i; } };
Supposons que je schématise le tas. Sans élisions :
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14 A& operator=(A a) { //3 using std::swap; swap(*this,a); //4 return *this; //5 } //... A a; //1 a = A() /*2*/; //6
Avec elision ://1
add1 size(int) data1 //ctor new
//2
add1 size(int) data1
add2 size(int) data2 //ctor new
//3
add1 size(int) data1
add2 size(int) data2
add3 size(int) data2 //copy-ctor new
//4
add1 size(int) data2 //swap
add2 size(int) data2
add3 size(int) data1 //swap
//5
add1 size(int) data2
add2 size(int) data2
//delete add3 (parametre)
//6
add1 size(int) data2
//delete add2 (temporaire)
Je ne vois pas où il y aurait fuite mémoire.//1
add1 size(int) data1 //ctor new
//2
add1 size(int) data1
add2 size(int) data2 //ctor new
//3
add1 size(int) data1
add2 size(int) data2
//copy-ctor elide
//4
add1 size(int) data2 //swap
add2 size(int) data1 //swap
//la fonction utilise directement add2
//5
add1 size(int) data2
add2 size(int) data1
//rien ne se passe
//6
add1 size(int) data2
//delete add2 (temporaire)
Pour le second code, le raisonnement est le même.
Tu commence à approcher du problème, mais tu n'as apparemment toujours pas de vue d'ensemble.
Pour rappel, il y a la règle des trois grands, qui dit que tu dois définir l'opérateur d'affectation, le constructeur par copie et le destructeur si tu donnes une définition pour ne serait-ce qu'une des fonctions en question.
Nous partons donc sur une base proche de
Tu es d'accord avec moi que, pour qu'un code proche de
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 struct A{ A(int taille = 0): taille(taille), ptr(taille==0? 0 : new int[taille]){} A(A const & a): taille(a.taille), ptr(a.taille==0? 0 : new int[a.taille]) {/* COPIE de ce qui est pointé par a.ptr */;} // !!!! A & operator = (A const & a){ A temp(a); //1 swap(ptr, temp.ptr); taille = a.taille; // c'est pas le plus important ;) return *this; } /* OU OU OU */ A & operator = (A temp){ // comme ca, je garde le même nom ;) //1 swap(ptr, temp.ptr); taille = temp.taille; // c'est pas le plus important ;) return *this; } int taille; int ptr *; };
soit valide, on est bel et bien obligé d'avoir un constructeur par copie (non trivial) dans le genre de celui que je présente, oui
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9 int main(){ A a(15); /* ... */ A copy(a); assert(a.ptr[3] == copy.ptr[3]); copy.ptr[3]= 125; //en fait, tout autre valeur que la valeur de a.ptr[3] assert(a.ptr[3]!= copy.ptr[3]); return 0; }
Et tu seras d'accord pour estimer que si l'on veut écrire un code proche de
il faut impérativement, dans l'opérateur d'affectation:
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10 int main(){ A a(15); A assign(20); /* ... */ A assing = a; assert(a.ptr[3] == assing .ptr[3]); assing .ptr[3]= 125; //en fait, tout autre valeur que la valeur de a.ptr[3] assert(a.ptr[3]!= assing .ptr[3]); return 0; }
Finalement, le seul point qui ne posera aucun problème, c'est le membre taille, parce qu'on peut tout aussi bien le copier que l'assigner sans que cela fasse la moindre différence.
- que l'on dispose d'un pointeur sur un bloc mémoire correctement alloué de taille suffisante pour représenter les 15 éléments contenus à l'adresse correspondant à a.ptr
- que ce bloc mémoire doit est différent de a.ptr
- que les 15 éléments de a.ptr aient été correctement copiés dans le bloc mémoire
- que l'on ait veillé, d'une manière ou d'une autre,
- à libérer la mémoire allouée à assing.ptr
- à assigner à assing.ptr l'adresse du bloc mémoire dans lequel on a copié les éléments
Au pire, nous pourrions dire à son sujet qu'il est sans doute préférable de l'assigner directement plutôt que de se taper un processus de copie + swap (car meme une copie (qui n'est qu'une affectation dans le cas présent) et une affectation a de grandes chances d'être plus rapide qu'une copie et un swap du fait que le swap est l'équivalent à deux Xor)
Ce qui importe surtout, c'est que, quelle que soit la manière dont on puisse envisager les choses, il faut forcément que l'on dispose d'un pointeur (temp.ptr dans mon exemple) qui représente un état strictement similaire, en nombre d'éléments et en valeurs des différents éléments à celui de l'objet d'origine.
Comme je l'ai dit plus tôt, il y a effectivement moyen de ne pas passer par la copie complète de l'objet, en travaillant avec un pointeur temporaire, mais, à partir du moment où l'on part du principe que l'on fait une copie, qu'elle soit implicite (transmission par valeur) ou explicite (transmission par référence constante + copie), il faut que cette copie existe et soit cohérente.
Il y a peut etre moyen d'éviter une copie avant d'arriver à l'opérateur d'affectation, il y a peut etre moyen d'en éviter une après l'opérateur d'affectation, mais, si l'on veut que l'opérateur d'affectation (et encore, j'ai des doutes du fait que la copie devient un prérequis au comportement adéquat de l'opérateur d'affectation, mais comme je n'ai que mon intuition sur ce point... )
On a beau se dire que temp est, quoi qu'il arrive un objet temporaire, temp n'est ni un objet construit par défaut, ni un objet seulement "partiellement construit", mais bel et bien une copie conforme, pleine et entière de l'objet qui aura servi de paramètre.
- évite les fuites mémoire (en s'assurant que la temp.ptr prend l'adresse "de base" de assing, dans mon exemple)
- assigne bel et bien à assing.ptr un état en tout point conforme à celui que l'on observe auprès de a.ptr
S'il n'en allait pas ainsi, nous ne pourrions en aucun cas prévoir où se retrouverait temp.ptr, mais surement pas dans l'éther, et de préférence à une adresse mémoire à laquelle il serait particulièrement malencontreux d'essayer d'accéder en écriture (ou d'essayer d'invoquer delete dessus)
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
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