IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)
Navigation

Inscrivez-vous gratuitement
pour pouvoir participer, suivre les réponses en temps réel, voter pour les messages, poser vos propres questions et recevoir la newsletter

C++ Discussion :

opérateur d'affectation: copie implicite ou explicite [Débat]


Sujet :

C++

  1. #1
    Expert éminent sénior
    Avatar de koala01
    Homme Profil pro
    aucun
    Inscrit en
    Octobre 2004
    Messages
    11 612
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 52
    Localisation : Belgique

    Informations professionnelles :
    Activité : aucun

    Informations forums :
    Inscription : Octobre 2004
    Messages : 11 612
    Points : 30 611
    Points
    30 611
    Par défaut opérateur d'affectation: copie implicite ou explicite
    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 - Visualiser dans une fenêtre à part
    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 - Visualiser dans une fenêtre à part
    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
    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

  2. #2
    Inactif  


    Homme Profil pro
    Inscrit en
    Novembre 2008
    Messages
    5 288
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 48
    Localisation : France, Rhône (Rhône Alpes)

    Informations professionnelles :
    Secteur : Santé

    Informations forums :
    Inscription : Novembre 2008
    Messages : 5 288
    Points : 15 620
    Points
    15 620
    Par défaut
    Salut

    Je conseille la lecture de la série "Object Swapping" de Andrew Koenig sur Dr. Dobb's (part 1, part 2, part 3, part 4, part 5, part 6 et part 7)

    Pour résumer :
    * il faut aussi la move semantic :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    TableauInt & operator = (TableauInt && rhs)
    {
       swap(temp);
       return *this;
    }
    * sans move semantic, (TableauInt const& rhs) et (TableauInt rhs), c'est pareil (on évite juste de nommer la temporaire dans le second cas)
    * avec la move semantic, il y a ambiguïté entre (TableauInt rhs) et (TableauInt && rhs) pour les rvalues, donc il faut mieux utiliser (TableauInt const& rhs)

  3. #3
    En attente de confirmation mail

    Homme Profil pro
    Ingénieur développement logiciels
    Inscrit en
    Août 2004
    Messages
    1 391
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 33
    Localisation : France, Doubs (Franche Comté)

    Informations professionnelles :
    Activité : Ingénieur développement logiciels

    Informations forums :
    Inscription : Août 2004
    Messages : 1 391
    Points : 3 311
    Points
    3 311
    Par défaut
    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 - Visualiser dans une fenêtre à part
    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 - Visualiser dans une fenêtre à part
    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.

  4. #4
    Expert éminent sénior
    Avatar de koala01
    Homme Profil pro
    aucun
    Inscrit en
    Octobre 2004
    Messages
    11 612
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 52
    Localisation : Belgique

    Informations professionnelles :
    Activité : aucun

    Informations forums :
    Inscription : Octobre 2004
    Messages : 11 612
    Points : 30 611
    Points
    30 611
    Par défaut
    Citation Envoyé par gbdivers Voir le message
    Pour résumer :
    * il faut aussi la move semantic :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    TableauInt & operator = (TableauInt && rhs)
    {
       swap(temp);
       return *this;
    }
    Je présumes que c'est une erreur de typo et que tu voulais écrire
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    TableauInt & operator = (TableauInt && rhs)
    {
       swap(rhs);
       return *this;
    }

    * sans move semantic, (TableauInt const& rhs) et (TableauInt rhs), c'est pareil (on évite juste de nommer la temporaire dans le second cas)
    C'est à peu près le raisonnement que je suivais
    * avec la move semantic, il y a ambiguïté entre (TableauInt rhs) et (TableauInt && rhs) pour les rvalues, donc il faut mieux utiliser (TableauInt const& rhs)
    Mais, mis en perspective avec la discussion précitée, est-ce réellement à prendre en compte

    Je rappelle que la discussion s'adresse à un débutant qui doit remettre quelque chose de valable à un prof.

    La move semantic étant sommes toutes assez récente, je crains que ca ne passe largement au dessus de la tête et du prof et de l'étudiant, non

    Ici, je voudrais vraiment nous placer dans un contexte ou:
    1. il faut connaitre la forme canonique orthodoxe de coplien
    2. la grande loi des trois est d'application
    Toute considération n'entrant pas dans ce contexte étant à éviter si possible (bien qu'il soit utile de les préciser le cas échéant )
    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

  5. #5
    En attente de confirmation mail

    Homme Profil pro
    Ingénieur développement logiciels
    Inscrit en
    Août 2004
    Messages
    1 391
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 33
    Localisation : France, Doubs (Franche Comté)

    Informations professionnelles :
    Activité : Ingénieur développement logiciels

    Informations forums :
    Inscription : Août 2004
    Messages : 1 391
    Points : 3 311
    Points
    3 311
    Par défaut
    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 - Visualiser dans une fenêtre à part
      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 - Visualiser dans une fenêtre à part
      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.

  6. #6
    Expert éminent sénior
    Avatar de koala01
    Homme Profil pro
    aucun
    Inscrit en
    Octobre 2004
    Messages
    11 612
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 52
    Localisation : Belgique

    Informations professionnelles :
    Activité : aucun

    Informations forums :
    Inscription : Octobre 2004
    Messages : 11 612
    Points : 30 611
    Points
    30 611
    Par défaut
    Je comprends très bien ce que tu expliques, mais il y a quand même une chose qui me travaille.

    La l'élision de copie ne peut, à mon sens, être opérable que lorsque il n'y a rien entre la création de la copie et le moment où la copie est renvoyée, non

    Ainsi, une fonction
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    UnType copy(Untype t)
    {
        return t;
    }
    pourrait sans doute très bien profiter de l'élision de copie par rapport à sa version
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    UnType copy(Untype const &t)
    {
        return UnType(t); // appel explicite au constructeur par copie
    }
    Sur ce point, je suis tout à fait d'accord

    Là où je me pose sérieusement la question, c'est quand il y a forcément quelque chose à faire entre le moment de la création de la copie et celui de son renvoi.

    Je rappel le contexte d'origine:

    Un étudiant nous a posé la question de "ce qui ne va pas avec sa classe TableauInt" qui se présentait sous la forme de
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    class TableauInt
    {
        public:
            TableauInt();
            TableauInt(int taille);
            ~TableauInt(){delete[] ptr;}
             /* ni constructeur par copie ni opérateur d'affectation */
        private:
            int * ptr;
            int taille;
    };
    En précisant que comme c'était pour son prof (hé oui, il y en a toujours qui croient qu'il faut fatalement gérer la mémoire à la main en C++ ), il ne pouvait pas se faciliter la tache en utilisant std::vector

    Le but étant de pouvoir écrire un code ressemblant à peu près à ceci
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    int main()
    {
        TableauInt tab1(10); // 10 éléments dans le tableau
        TableauInt tab2(20); // et 20 dans celui-ci
        tab2[2] = 5;         // (dans la discussion d'origine, le PO utilisait setEleement, mais bon... :D)
         TableauInt tab3(tab2); // tab3[2] == 5
         tab3[2]= 10;           // tab3[2] == 10 tab2[2] == 5 (toujours)
         tab2[N] = x;           // tant que N <20, pas de problème ;)
         tab1 = tab[2];         // prérequis : pas de fuites mémoire et l'utilisation de tab1 
                                // n'interfère pas sur le contenu de tab2
    }
    (en gros, cela revient à fournir une classe RAIIsante ayant sémantique de valeur, sans profiter des bienfaits de C++11 )

    S'il n'y avait aucun accès en écriture à la copie (comprends: si l'on n'appelait que des fonctions membres constantes de la copie ou, le cas échéant des fonctions libres qui l'utilise sous la forme de référence constante), il n'est pas impossible (mais cela reste à vérifier ) que le compilateur puisse se rendre compte que la copie n'est jamais modifiée et qu'il mette en place l'élision de la copie.
    Mais, dans ce cadre particulier où il faut d'office veiller à la copie en profondeur du pointeur "copié" et à la libération de la mémoire "d'origine", j'ai beaucoup de mal à imaginer que le compilateur puisse faire une élision de copie sachant que, entre la copie et le renvoi de celle-ci, il y aura, fatalement, un swap des données et l'appel obligatoire au destructeur sur la copie (dont le pointeur contiendra à ce moment là l'adresse de la donnée d'origine).

    Mais peut etre suis-je limité dans ma réflexion par l'a priori selon lequel le compilateur n'est qu'un "bête programme"
    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

  7. #7
    Expert confirmé
    Homme Profil pro
    Étudiant
    Inscrit en
    Juin 2012
    Messages
    1 711
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France

    Informations professionnelles :
    Activité : Étudiant

    Informations forums :
    Inscription : Juin 2012
    Messages : 1 711
    Points : 4 442
    Points
    4 442
    Par défaut
    Bien intéressé par la discussion, naïvement je dirais que la solution de koala01 (1er post) est meilleure que celle de Médinoc (1er post aussi) car je vois ça comme deux copies dans la solution de Médinoc (bien que très certainement optimisé par le compilo, ce qui fait gagner un passage de pointeur + un déréférencement de pointeur, mais .... A moins de vérifier le code généré on est jamais sur ).

    Mais ici utiliser la "move semantic" peut apporter un gain appréciable.

    edit:
    Citation Envoyé par koala01 Voir le message
    (hé oui, il y en a toujours qui croient qu'il faut fatalement gérer la mémoire à la main en C++ )
    C'est un autre sujet, mais desfois on a pas le choix (Mais entièrement d'accord que quand on peut éviter, il faut pas s'en priver.)

  8. #8
    Expert éminent sénior
    Avatar de koala01
    Homme Profil pro
    aucun
    Inscrit en
    Octobre 2004
    Messages
    11 612
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 52
    Localisation : Belgique

    Informations professionnelles :
    Activité : aucun

    Informations forums :
    Inscription : Octobre 2004
    Messages : 11 612
    Points : 30 611
    Points
    30 611
    Par défaut
    Citation Envoyé par Iradrille Voir le message
    Mais ici utiliser la "move semantic" peut apporter un gain appréciable.
    Et encore, si j'ai bonne souvenance, les pointeurs de l'objet d'origine (celui qui est passé en paramètre) sont invalidés avec la sémantique de mouvement...

    Du coup, les lignes 7 et 8 du dernier code présenté dans mon intervention précédente pourraient poser problème
    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

  9. #9
    Membre chevronné
    Avatar de Joel F
    Homme Profil pro
    Chercheur en informatique
    Inscrit en
    Septembre 2002
    Messages
    918
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 43
    Localisation : France, Essonne (Île de France)

    Informations professionnelles :
    Activité : Chercheur en informatique
    Secteur : Service public

    Informations forums :
    Inscription : Septembre 2002
    Messages : 918
    Points : 1 921
    Points
    1 921
    Par défaut
    Le vrai interet de copier l'argument implicitement est que ca renforce l'exception level de =. En effet si la copie de l'argument echoue a cause d'une exception, la cible reste inchangé, rendant la transaction roll-backable.

  10. #10
    gl
    gl est déconnecté
    Rédacteur

    Homme Profil pro
    Inscrit en
    Juin 2002
    Messages
    2 165
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 45
    Localisation : France, Isère (Rhône Alpes)

    Informations forums :
    Inscription : Juin 2002
    Messages : 2 165
    Points : 4 637
    Points
    4 637
    Par défaut
    Citation Envoyé par Joel F Voir le message
    Le vrai interet de copier l'argument implicitement est que ca renforce l'exception level de =. En effet si la copie de l'argument echoue a cause d'une exception, la cible reste inchangé, rendant la transaction roll-backable.
    Pas sur de comprendre ton point ici. Que la copie soit explicite ou implicite, si la copie échoue, le swap n'est pas fait et on ne change pas la cible ! Non ?

  11. #11
    Rédacteur/Modérateur


    Homme Profil pro
    Network game programmer
    Inscrit en
    Juin 2010
    Messages
    7 113
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 36
    Localisation : Canada

    Informations professionnelles :
    Activité : Network game programmer

    Informations forums :
    Inscription : Juin 2010
    Messages : 7 113
    Points : 32 958
    Points
    32 958
    Billets dans le blog
    4
    Par défaut
    Personnellement j'aurais tendance à favoriser dans un premier temps l'écriture explicite.
    En cas de recherche d'optimisations, et à ce moment-là uniquement, je m'attarderais à tester la copie implicite pour voir s'il s'agit d'une piste intéressante.
    Pensez à consulter la FAQ ou les cours et tutoriels de la section C++.
    Un peu de programmation réseau ?
    Aucune aide via MP ne sera dispensée. Merci d'utiliser les forums prévus à cet effet.

  12. #12
    Débutant
    Profil pro
    Inscrit en
    Mai 2006
    Messages
    688
    Détails du profil
    Informations personnelles :
    Localisation : France

    Informations forums :
    Inscription : Mai 2006
    Messages : 688
    Points : 176
    Points
    176
    Par défaut
    la bonne forme est bien la suivante :

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    Type& operator =(Type type)

    Même dans le cas d'une rvalue le type passsé en paramètre pourra être move-constructed et donc pas de copie.

    si on écrit

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    Type& operator =(Type&& type)
    on empêche cet operateur d'être appelé avec une lvalue ce qui n'est généralement pas ce que l'on souhaite

    enfin cette écriture

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    Type& operator =(const Type& type)
    n'est pas la meilleure dans la mesure où l'on devra nous même effectuer une copie à l'intérieur de l'operator ce qui n'est pas bon en terme de factorisation de code car si l'on respecte "the Rule of Three" (qui est maintenant "Rule of Four" avec le c++11) on se doit d'avoir également un constructeur par copie.

  13. #13
    Expert éminent sénior
    Avatar de koala01
    Homme Profil pro
    aucun
    Inscrit en
    Octobre 2004
    Messages
    11 612
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 52
    Localisation : Belgique

    Informations professionnelles :
    Activité : aucun

    Informations forums :
    Inscription : Octobre 2004
    Messages : 11 612
    Points : 30 611
    Points
    30 611
    Par défaut
    Citation Envoyé par guillaume07 Voir le message

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    Type& operator =(Type&& type)
    on empêche cet operateur d'être appelé avec une lvalue ce qui n'est généralement pas ce que l'on souhaite
    Le fait est que cet opérateur a pour résultat, lorsqu'il est utilisé, d'invalidé l'objet en paramètre, ou, à défaut, de faire en sorte que l'objet passé en paramètre et l'objet qui reçoit l'affectation partagent des ressources communes.

    Tant que ta classe n'utilise pas de pointeur, cela ne pose pas de problème, mais, dés qu'elle utilise un pointeur en interne, ca peut occasionner du dégât, cf mon intervention précédante
    enfin cette écriture

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    Type& operator =(const Type& type)
    n'est pas super dans la mesure où l'on devra nous même effectuer une copie à l'intérieur de l'operator ce qui n'est pas top en terme de factorisation de code car si l'on respecte "the Rule of Three" (qui est maintenant "Rule of Four" avec le c++11) on se doit d'avoir également un constructeur par copie.
    J'ai envie de dire "oui, et alors "

    A partir du moment où l'on décide de ne pas utiliser les solutions RAIIsantes que peut offrir C++11, la règle des trois (ou quatre en C++11) est, d'office, d'application.

    Le but de cette règle étant de permettre une gestion "bas niveau" des ressources en évitant les problèmes liés à cette gestion de bas niveaux qui sont:
    1. le partage de ressources entre différentes instances de la classe
    2. le risque de tentatives de double libération de la mémoire
    3. le risque de corruption des données d'une instance en travaillant sur une autre
    4. les fuites mémoire lors de (ré)affectation
    A ne pas utiliser les possibilités nouvelles, nous sommes forcément tenu de respecter les règles anciennes de l'art non
    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

  14. #14
    Débutant
    Profil pro
    Inscrit en
    Mai 2006
    Messages
    688
    Détails du profil
    Informations personnelles :
    Localisation : France

    Informations forums :
    Inscription : Mai 2006
    Messages : 688
    Points : 176
    Points
    176
    Par défaut
    Citation Envoyé par koala01 Voir le message
    J'ai envie de dire "oui, et alors "
    Je te conseil vivement d'éviter toute duplication de code au sein d'une application pour plusieurs raisons assez évidente pour que je n'ai pas à les énumèrer ici.

  15. #15
    Expert éminent sénior
    Avatar de koala01
    Homme Profil pro
    aucun
    Inscrit en
    Octobre 2004
    Messages
    11 612
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 52
    Localisation : Belgique

    Informations professionnelles :
    Activité : aucun

    Informations forums :
    Inscription : Octobre 2004
    Messages : 11 612
    Points : 30 611
    Points
    30 611
    Par défaut
    Quelle duplication de code

    Si tu regarde le code que je présentais et qui est à l'origine de cette discussion (allez, je te redonne le lien vers mon intervention d'origine ), tu constateras qu'il n'y a strictement aucune duplication de code, vu que je propose quelque chose de 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
    21
    22
    23
    24
    25
    26
    class TableauInt{
        public:
            TableauInt(int size):size_(size),ptr(size>0? new int[size]:0){}
            TableauInt(TableauInt const & rhs):size_(rhs.size),
                 ptr_(rhs.size>0? new int[rhs.size]:0){ 
                 memncpy(ptr_, rhs.ptr_, taille * sizeof(int));
            }
            TableauInt & operator= (TableauInt const & rhs){
                TableaInt temp(rhs);
                swap(rhs);
                return *this;
            }
            ~TableauInt(){delete[] ptr_}
        private:
           void swap(TableauInt & rhs){
               /* idéalement, utiliser std::swap ;) */
               int * tabtemp= ptr; 
               ptr_= rhs.ptr_;
               rhs.ptr_= temp
               int tailleTemp = taille;
               taille = rhs.taille;
               rhs.taille = tailleTemp;
           }
           int size_;
           int  * ptr_;
    };
    La seule chose qui soit dupliquée (parce que l'on n'a définitivement pas d'autre solution "raisonnable", c'est la liste d'initialisation dans le constructeur et le constructeur par copie
    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

  16. #16
    Débutant
    Profil pro
    Inscrit en
    Mai 2006
    Messages
    688
    Détails du profil
    Informations personnelles :
    Localisation : France

    Informations forums :
    Inscription : Mai 2006
    Messages : 688
    Points : 176
    Points
    176
    Par défaut
    Citation Envoyé par koala01 Voir le message
    Le fait est que cet opérateur a pour résultat, lorsqu'il est utilisé, d'invalidé l'objet en paramètre, ou, à défaut, de faire en sorte que l'objet passé en paramètre et l'objet qui reçoit l'affectation partagent des ressources communes.

    Tant que ta classe n'utilise pas de pointeur, cela ne pose pas de problème, mais, dés qu'elle utilise un pointeur en interne, ca peut occasionner du dégât, cf mon intervention précédante
    tout comme le constructeur prenant une référence vers une rvalue le fera si une rvalue est pasé à un operateur d'assignement défini ainsi:

    sous réserve bien sûr qu'un constructeur prenant une rvalue reference soit définit pour le type T

  17. #17
    Débutant
    Profil pro
    Inscrit en
    Mai 2006
    Messages
    688
    Détails du profil
    Informations personnelles :
    Localisation : France

    Informations forums :
    Inscription : Mai 2006
    Messages : 688
    Points : 176
    Points
    176
    Par défaut
    Citation Envoyé par koala01 Voir le message
    Quelle duplication de code

    Si tu regarde le code que je présentais et qui est à l'origine de cette discussion (allez, je te redonne le lien vers mon intervention d'origine ), tu constateras qu'il n'y a strictement aucune duplication de code, vu que je propose quelque chose de 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
    21
    22
    23
    24
    25
    26
    class TableauInt{
        public:
            TableauInt(int size):size_(size),ptr(size>0? new int[size]:0){}
            TableauInt(TableauInt const & rhs):size_(rhs.size),
                 ptr_(rhs.size>0? new int[rhs.size]:0){ 
                 memncpy(ptr_, rhs.ptr_, taille * sizeof(int));
            }
            TableauInt & operator= (TableauInt const & rhs){
                TableaInt temp(rhs);
                swap(rhs);
                return *this;
            }
            ~TableauInt(){delete[] ptr_}
        private:
           void swap(TableauInt & rhs){
               /* idéalement, utiliser std::swap ;) */
               int * tabtemp= ptr; 
               ptr_= rhs.ptr_;
               rhs.ptr_= temp
               int tailleTemp = taille;
               taille = rhs.taille;
               rhs.taille = tailleTemp;
           }
           int size_;
           int  * ptr_;
    };
    La seule chose qui soit dupliquée (parce que l'on n'a définitivement pas d'autre solution "raisonnable", c'est la liste d'initialisation dans le constructeur et le constructeur par copie
    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

  18. #18
    Expert éminent sénior
    Avatar de koala01
    Homme Profil pro
    aucun
    Inscrit en
    Octobre 2004
    Messages
    11 612
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 52
    Localisation : Belgique

    Informations professionnelles :
    Activité : aucun

    Informations forums :
    Inscription : Octobre 2004
    Messages : 11 612
    Points : 30 611
    Points
    30 611
    Par défaut
    Citation Envoyé par guillaume07 Voir le message
    tout comme le constructeur prenant une référence vers une rvalue le fera si une rvalue est appelé sur un operateur d'assignement défini ainsi:
    Oh que non!
    Par défaut,; il n'y a pas invalidation de l'élément transmis, mais partage des ressource, ce qui n'est bien sur pas ce que l'on souhaite, vu que ce que l'on souhaite, c'est d'obtenir un comportement similaire à ce qui est observé avec des classes n'utilisant aucune donnée allouée dynamiquement, à savoir que chaque instance dispose de ses propres données et peut etre modifiée sans avoir d'impact sur les autres instances, y compris sur celles qui leur auraient été assignées.
    Car, si tu prend une classe toute simple qui ne gère aucune donnée de manière dynamqiue, tu t'attends à pouvoir écrire 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
    /* soit une structure Point proche de*/
    struct Point
    {
        /* je ne met qu'un constructeur, pour bien indiquer mes intentions ;) */
        Point(int x, int y):x(x),y(y){}
        int x;
        iny y;
    }
    int main(){
        Point P1(2,3);
        Point P2(4,5);
        Point P3 = P1;
        P3.x = 10; // ne modifie en rien P1
        P3.y = 15;
        /* P1 est toujours accessible
            P1.x == 2, P1.y == 3
         */
        /* ... */
        return 0;
    }
    A la remarque près d'une meilleure encapsulation éventuelle (mais ca, c'est un débat qui suit son cours par ailleurs ), on s'attend à ce que la classe "TableauInt" réagisse de manière strictement similaire, non

    C'est pour cela que le principe du copy and swap existe (on copie, d'une manière ou d'une autre) le paramètre et on inverse les données gérées dynamiquement de la copie et de l'objet de l'assignation de manière à éviter les fuites mémoire.
    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

  19. #19
    Débutant
    Profil pro
    Inscrit en
    Mai 2006
    Messages
    688
    Détails du profil
    Informations personnelles :
    Localisation : France

    Informations forums :
    Inscription : Mai 2006
    Messages : 688
    Points : 176
    Points
    176
    Par défaut
    Citation Envoyé par koala01 Voir le message
    Oh que non!
    Par défaut,; il n'y a pas invalidation de l'élément transmis, mais partage des ressource,
    si tu as définit un operator = et que tu souhaites que ta classe supporte la "sémantique de move" alors c'est que tu as définis un constructeur de copie prenant une rvalue reference...donc si il n'y pas d'invalidation de l'élément transmis c'est que tu as délibérement fait ce choix lors de ton implémentation du constructeur de copie prenant une rvalue reference

  20. #20
    Expert éminent sénior
    Avatar de koala01
    Homme Profil pro
    aucun
    Inscrit en
    Octobre 2004
    Messages
    11 612
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 52
    Localisation : Belgique

    Informations professionnelles :
    Activité : aucun

    Informations forums :
    Inscription : Octobre 2004
    Messages : 11 612
    Points : 30 611
    Points
    30 611
    Par défaut
    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 - Visualiser dans une fenêtre à part
    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 - Visualiser dans une fenêtre à part
    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 - Visualiser dans une fenêtre à part
    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
    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

Discussions similaires

  1. Réponses: 7
    Dernier message: 17/08/2014, 16h20
  2. Constructeur de copie, et opérateur d'affectation.
    Par Invité dans le forum Débuter
    Réponses: 49
    Dernier message: 03/04/2010, 14h13
  3. Réponses: 12
    Dernier message: 12/07/2007, 15h17
  4. Classe Interface et Opérateur d'affectation!
    Par Rodrigue dans le forum C++
    Réponses: 6
    Dernier message: 07/02/2007, 15h45
  5. [langage] opérateur d'affectation binaires
    Par biglebowski13 dans le forum Langage
    Réponses: 6
    Dernier message: 21/11/2006, 10h51

Partager

Partager
  • Envoyer la discussion sur Viadeo
  • Envoyer la discussion sur Twitter
  • Envoyer la discussion sur Google
  • Envoyer la discussion sur Facebook
  • Envoyer la discussion sur Digg
  • Envoyer la discussion sur Delicious
  • Envoyer la discussion sur MySpace
  • Envoyer la discussion sur Yahoo