Vous êtes nouveau sur Developpez.com ? Créez votre compte ou connectez-vous afin de pouvoir participer !

Vous devez avoir un compte Developpez.com et être connecté pour pouvoir participer aux discussions.

Vous n'avez pas encore de compte Developpez.com ? Créez-en un en quelques instants, c'est entièrement gratuit !

Si vous disposez déjà d'un compte et qu'il est bien activé, connectez-vous à l'aide du formulaire ci-dessous.

Identifiez-vous
Identifiant
Mot de passe
Mot de passe oublié ?
Créer un compte

L'inscription est gratuite et ne vous prendra que quelques instants !

Je m'inscris !

Opérateur d'affectation: copie implicite ou explicite

Le , par koala01

19PARTAGES

0  0 
Salut,

J'ouvre ce débat à la suite de cette discussion dans laquelle je présentais le principe du copy-and swap sous la forme de
Code : Sélectionner tout
1
2
3
4
5
6
7
TableauInt & operator = (TableauInt const & rhs) 
{ 
   TableauInt temp(rhs); // copie de l'élément affecté 
   swap(temp); //inversion des membre de this avec la copie 
   return *this; // renvoie une référence sur l'élément courent 
                 // la copie est détruite en sortant de la fonction 
}
et à laquelle médinoc a répondu ceci
Citation Envoyé par Médinoc  Voir le message
Juste une chose pour le copy-and-swap: prendre directement la source par valeur peut éviter la copie quand on utilise l'opérateur = avec un temporaire:
Code C++ : Sélectionner tout
1
2
3
4
TableauInt::TableauInt& operator=(TableauInt tmp) { 
	swap(tmp); 
	return *this; 
}

Après mure réflexion, j'en viens à la conclusion que cela revient "choux vert et vert choux" du fait que la seule différence présente entre les deux possibilités vient de la copie explicite dans mon code de l'objet assigné contre la copie implicite dans le code de Médinoc.

Personnellement, j'aurais cependant tendance à préférer la copie explicite pour la simple raison (sans fondement technique) que j'ai l'impression que l'on code souvent "par habitude", et que cela aura l'avantage de garder le code cohérent par rapport aux autres fonctions manipulant des paramètres qu'elles ne doivent pas modifier.

On répète en effet inlassablement que
si une fonction ne doit pas modifier l'objet qui lui est transmis en paramètre et que l'objet est plus gros qu'un simple primitif, il y a lieu de le passer par référence (pour éviter la copie de l'objet) constante (pour éviter que la fonction ne tente de le modifier indument)

Ce principe est très bien dans le sens où il permet de gagner en performances à peu de frais en évitant la copie de l'objet passé en paramètre, mais qu'en est il si la fonction en question doit, justement, travailler sur une copie de l'objet (si tant est que l'objet soit copiable)
est-il préférable de laisser la copie se faire de manière implicitement, quitte à "briser" la cohérence par rapport au reste du code
est-ce au codeur de la fonction de veiller à créer sa propre copie de l'objet en question
y a-t-il la moindre raison technique qui pourrait justifier la préférence d'une technique par rapport à l'autre

Ces questions sont, évidemment, à mettre en perspective par rapport à la discussion citée, à savoir, le fait de devoir redéfinir l'opérateur d'affectation dans le respect de la grande règle des trois (si l'on a du définir un comportement spécifique pour une des fonctions parmi le constructeur par copie, l'opérateur d'affectation ou le destructeur, il y a lieu de définir un comportement spécifique pour les trois).

Je sais parfaitement qu'il y a maintenant la possibilité d'utiliser éléments RAII pour s'éviter cette peine, mais je vous demande de ne pas faire entrer cet aspect en ligne de compte

Une erreur dans cette actualité ? Signalez-le nous !

Avatar de Flob90
Membre expert https://www.developpez.com
Le 06/04/2013 à 19:36
@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.
2  0 
Avatar de guillaume07
Débutant https://www.developpez.com
Le 07/04/2013 à 1:42
Citation Envoyé par koala01 Voir le message


Tu ne peux pas, avant le swap, avoir dans ton objet temporaire un pointeur qui pointe "vers l'éther" ou qui est dans les choux, vu que c'est l'adresse de ce pointeur qui sera fournie au pointeur de destination!
Franchement je ne sais pas où tu fais un blocage, mais définitivement l'objet temporaire pointe bel et bien sur une adresse valide et non "vers l'éther"
2  0 
Avatar de Médinoc
Expert éminent sénior https://www.developpez.com
Le 07/04/2013 à 10:39
En, effet. Koala01, j'ai l'impression que tu confonds "passage par valeur" et "passage par copie systématique dans tous les cas".

Quand on fait foo(1, A(10), 42) avec void foo(int, A, int), la copie sera élidée en construisant directement l'objet à sa destination (c'est-à-dire, entre les deux paramètres entiers).
La raison pour laquelle c'est acceptée est qu'il n'y a aucun moyen de référencer l'objet après l'appel de la fonction; l'appelant ne peut donc pas voir que l'objet lui-même a été modifié plutôt qu'une copie de l'objet.
2  0 
Avatar de guillaume07
Débutant https://www.developpez.com
Le 07/04/2013 à 14:56
Citation Envoyé par koala01 Voir le message
Si ce n'est qu'à ce moment là, ce n'est pas l'opérateur d'affectation "simple" qui sera utilisé,
mais l'opérateur d'affectation par mouvement (celui qui prend, d'office, une rvalue référence : A(&& rhs) ).
Si tu n'as pas implémenté operator =(A&& mais operator =(A), il faut que tu comprennes que même avec une rvalue
c'est bien operator=(A) qui sera appelé
.
Si le compilateur supporte la copy ellision, la temporarie sera bindé directement dans le paramètre de l'operator =(A) autrement
le constructeur par mouvement de A sera utilisé, ce qui tu en conviendras ne représente pas non plus une copie


Bref, quoi qu'il en soit, l'argument de l'élision de copie ne représente pas un fondement technique sur lequel se baser pour décider s'il vaut mieux transmettre l'argument par valeur ou par référence constante à l'opérateur d'affectation normal
C'est au contraire encore une fois l'unique raison pour laquelle il est préférable d'utiliser l'arguement par valeur

la seule question qui reste à ce sujet est est-il préférable de fournir deux implémentations de operator = (pour les lvalue et pour les rvalue) où pour des raisons de simplifications se contenter de la forme operator =(A) (au prix d'un appel au constructeur de mouvement si appelé avec une rvalue)
2  0 
Avatar de Flob90
Membre expert https://www.developpez.com
Le 07/04/2013 à 16:42
Citation Envoyé par koala01 Voir le message
Si ce n'est qu'à ce moment là, ce n'est pas l'opérateur d'affectation "simple" qui sera utilisé, mais l'opérateur d'affectation par mouvement (celui qui prend, d'office, une rvalue référence : A(&& rhs) ).
Franchement, tu exclus la move-semantic depuis le depuis de la conversasion, on est donc en C++03 et il n'y a pas d'opérateur d'affectation de mouvement.

Citation Envoyé par koala01 Voir le message
Je ne vois donc pas en quoi cela pourrait changer quoi que ce soit au niveau de la manière de déclarer l'argument de l'opérateur simple.
J'ai fait un bench qui montre que ca change tout dans au moins un cas. Alors en effet ce cas ne se produira pas si tu implementes la version opérateur d'affectation par mouvement, mais dans ce cas le bench n'a pas de sens (tu n'as pas le choix de la forme de l'opérateur d'affection normal, c'est nécessairement const T& pour éviter les conflit avec T&&, cependant tu as exclus la move-semantic, donc ca ne rentre pas en compte).

Citation Envoyé par koala01 Voir le message
L'une des solutions qui pourraient être envisagée, c'est, bien sur, de faire appel à l'opérateur d'affectation par mouvement dans le corps de l'opérateur d'affectation simple, mais, pour que cela marche, il faut que l'argument que l'on transmettra soit un objet temporaire non nommé, sous une forme proche de
Code : Sélectionner tout
1
2
3
4
A & operator=(A /* const & */ rhs){
   *this.operator=(A(rhs)); 
    return this;
}
A ce moment là, tu auras peut etre une élision de la copie au niveau de l'opérateur par mouvement, mais comme tu devras, de toutes manières, faire une copie implicite, autant passer rhs par référence constante pour s'éviter d'avoir, en plus, une copie supplémentaire au moment de l'appel de l'opérateur d'affectation si on vient à passer rhs par valeur.
Ce paragraphe n'a aucun sens, tu ne peux pas mettre const & en commentaire si tu as aussi la version T&& de l'opérateur d'affectation. Si tu n'as pas cette autre version mais que tu as un move-ctor, c'est en effet lui qui est appelé en cas de temporaire et éventuellement élidé.

Citation Envoyé par koala01 Voir le message
Bref, quoi qu'il en soit, l'argument de l'élision de copie ne représente pas un fondement technique sur lequel se baser pour décider s'il vaut mieux transmettre l'argument par valeur ou par référence constante à l'opérateur d'affectation normal
Si ce n'est un bench qui montre 50% de perf en plus dans au moins un cas d'utilisation, en effet il n'y a aucun argument ...
2  0 
Avatar de Flob90
Membre expert https://www.developpez.com
Le 30/03/2013 à 18:27
Bonjour,

Cette technique présentée par Medinoc est "assez" vielle (présentée en 2009 sur C++Next et probablement bien plus ancienne que ça). Son seule fondement est d'éviter une copie (et donc potentiellement gagner en performance) dans le cas où l’affection est faite depuis un temporaire.

Cette technique n'est pas toujours applicable (typiquement pour les opérateurs binaires, le gain de performance n'est pas évident (*)), mais dans ce cas il y a certitude que ça ne pourra être qu'un gain (éventuellement nul, mais ça ne sera pas une perte).

Code : Sélectionner tout
On répète en effet inlassablement que
On pourrait objecter que la façon décrite par Medinoc est aussi idiomatique pour certain codeur :
That realization leads us directly to this guideline:

Guideline: Don’t copy your function arguments. Instead, pass them by value and let the compiler do the copying.

At worst, if your compiler doesn’t elide copies, performance will be no worse. At best, you’ll see an enormous performance boost.
(Dave Abrahams)

Ce constat nous conduit directement à ce conseil :

Conseil: Ne copiez pas les arguments de votre fonction. A la place, passer les par valeur et laissez le compilateur faire la copie.

Au pire, si votre compilateur n'élide pas les copies, la performance ne sera pas pire. Au mieux, vous obtiendrez un énorme gain de performance.
(traduction libre)

Bien entendu l'article, et même la série, est plus complète que ça et d'autre cas doivent être traités (**). Cependant dans le cas de l'opérateur d'affectation, le conseil s'applique parfaitement.

Donc pour tes questions, dans l'ordre, mon avis est :
  1. Oui, pour obtenir les meilleurs performances possible. Ce n'est pas contraignant niveau écriture, il n'y a donc pas de raisons de s'en priver, quelque soit le gain, AMA.
  2. J'ai un peu de mal à comprendre le sens de cette question. Que tu fasses la copie implicitement ou explicitement, c'est bien toi codeur qui en prend la décision. Une solution ou l'autre (en supposant que tu puisses écrire les deux) n'impactent pas la façon dont la fonction sera utilisable.
  3. Oui, profiter de l’élision proposée par le compilateur.


(*) Pour deux raisons, liés au retour, qui est un nouvel objet, et le caractère binaire et donc de priorité d'opérateur.

(**) Le premier point étant lorsque la fonction retourne aussi un objet du même type que le paramètre. L'objectif serait de profiter de deux élisions. La technique suggéré étant d'utiliser un swap :
Code : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
A foo(A a)
{
  using std::swap;

  A r;
  //stuff
  swap(a,r);
  //stuff
  return r;
}
J'avouerais ne pas avoir tester si le compilateur est bel et bien capable d'effectuer les deux élisions avec un tel code.

PS: En C++11, et si l'on écrit autre chose qu'un opérateur d'affectation par copie, la donne peut légèrement changer selon la façon dont l'on veut profiter de la move semantic.
1  0 
Avatar de Flob90
Membre expert https://www.developpez.com
Le 31/03/2013 à 1:09
Citation Envoyé par koala01 Voir le message

C'est à peu près le raisonnement que je suivais
C'est équivalent à la différence près que la version const T& empêche toute élision d'éventuelles copies de temporaires : c'est donc potentiellement moins performant. Et ce n'est pas un gain d'écriture ni un gain conceptuel assez important pour justifier de se passer d'un éventuel gain de performance, AMA.

Si on reprend les conseils qu'on a l'habitude de donner aux débutants, l'un qui revient est de ne pas chercher à faire à la main ce que le compilateur / la bibliothèque standard fait mieux que nous. Et le conseil de Dave Abrahams est à rapproché de ça : tu vas avoir besoin d'une copie d'un argument ? Alors laisse le compilateur la faire bien mieux que toi.

Ensuite pour la move-semantic dans ce cas, il faut que les débutants se rendent comptent que les arguments peuvent être de deux genres :
  • lvalue, la source est un objet qui n'a pas vocation à être détruite sous peu : il ne faut pas l'utiliser.
  • rvalue, la source est un objet qui va être détruite sous peu : on peut l'utiliser.

Et c'est là que la décision d'implémenter une ou deux versions d'une fonction intervient : est-ce que notre fonction doit (pour être performante) se comporter différemment dans les deux cas. Reprenons notre opérateur d'affectation :
  • C'est identique, alors :
    Code : Sélectionner tout
    1
    2
    3
    4
    5
    6
    7
    8
    A& operator=(A rhs)
    {
      using std::swap;
      //stuff on rhs
      swap(*this,rhs);
      return *this;
    }
    Fait le travail, il est même possible que le comportement soit différent selon les deux cas en fonction de constructeur de copie/move : font-ils une distinction ou non ? (*)
  • C'est différent, alors
    Code : Sélectionner tout
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    A& operator=(const A& rhs)
    {
      A tmp = rhs;
      //stuff on tmp
      return *this = std::move(tmp);
    }
    
    A& operator=(A&& rhs)
    {
      using std::swap;
      //stuff on rhs
      swap(*this,rhs);
      //or simpler than swap, but safe anyway
      return *this;
    }
    Fait le travail. Il n'est pas obligatoire de faire un appel de l'un vers l'autre, mais c'est typiquement ce qui peut se passer.


(*) En réalité on peut toujours effectuer une distinction, si l'on veut être certain d'éviter les constructions par copie/move sans compter sur l'élision (qui n'est pas obligatoire), c'est un moyen certain d'y arriver.
1  0 
Avatar de koala01
Expert éminent sénior https://www.developpez.com
Le 06/04/2013 à 16:58
Citation Envoyé par guillaume07 Voir le message
Oui effectivement pas de duplication de code ici, mais tu perds comme il a été mentionné dans ce thread si je ne me trompe pas une optimisation éventuel (cf: http://cpp-next.com/archive/2009/08/...pass-by-value/), c'est gratuit ça serait dommage de se priver
Justement, je mets en doute la possibilité d'optimisation par élision de copie, à tout le moins pour l'opérateur d'affectation, et sans doute pour toute fonction qui nécessite un accès en écriture à la copie.

Comprenons nous bien...

soit la structure simple
Code : Sélectionner tout
1
2
3
4
5
struct Point{
    Point(int x, int y):x(x),y(y){}
    int x;
    int y;
};
et une fonction libre proche de
Code : Sélectionner tout
1
2
3
4
5
6
Point multiplyValue(Point const & point, int value){
    Point temp(point);
    temp.x *=value;
    temp.y *=value;
    return temp;
}
il y a, potentiellement, deux copies qui sont effectuée, mais une seule peut, le cas échéant, être évitée:
Code : Sélectionner tout
1
2
3
4
5
6
Point multiplyValue(Point const & point, int value){
    Point temp(point); // impossible à éviter, meme si on transmet point par valeur
    temp.x *=value;
    temp.y *=value;
    return temp; // éventuellement élidée
}
Le compilateur ne peut élider une copie que s'il se rend compte que la copie n'est pas modifiée entre le moment où elle est créée et le moment où elle est renvoyée.

Car, s'il élide indument une copie qui est destinée à être modifiée, la valeur renvoyée sera incohérente par rapport à la valeur attendue.

A partir du moment où il y a un "swap" de certaines informations dans l'opérateur d'affectation, il y a d'office une copie non élidée.

La seule question est dés lors : n'est il pas simplement préférable de garder un prototype qui respecte l'un des grands conseils généralement admis (AKA : transmettez toute structure plus grosse qu'un type primitif par référence, éventuellement constante), quitte à s'imposer de faire une copie explicite du paramètre reçu
1  0 
Avatar de guillaume07
Débutant https://www.developpez.com
Le 06/04/2013 à 17:41
Citation Envoyé par koala01 Voir le message
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
tu peux avoir la certitude que la copy ellision aura lieu .... tout simplement si ton operator =(T t) est appelé avec une rvalue
1  0 
Avatar de guillaume07
Débutant https://www.developpez.com
Le 06/04/2013 à 18:17
Citation Envoyé par koala01 Voir le message
Oui, parce que T n'est pas modifié entre le moment où il est créé et le moment où il est renvoyé.
Non c'est parceque l'objet retourné par func() et injecté dans l'operator = est une rvalue peu importe si dans func l'objet retourné est modifié une ou cent fois
1  0