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 :

[Débutant] Surcharge de l'opérateur = ; deux façons de faire, laquelle choisir?


Sujet :

C++

Vue hybride

Message précédent Message précédent   Message suivant Message suivant
  1. #1
    Vol
    Vol est déconnecté
    Membre averti
    Profil pro
    Inscrit en
    Décembre 2005
    Messages
    42
    Détails du profil
    Informations personnelles :
    Localisation : France

    Informations forums :
    Inscription : Décembre 2005
    Messages : 42
    Par défaut [Débutant] Surcharge de l'opérateur = ; deux façons de faire, laquelle choisir?
    Bonjour, je viens de débuter à apprendre le C++ et je me pose une question sur la surcharge de l'opérateur = (tout est fonctionnel, mais j'aimerais comprendre mieux). Je veux, par exemple, avec cette surcharge pouvoir copier les attributs d'un objet et les affecter à un autre objet du même type. Par exemple :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    ClasseToto instance1;
    ClasseToto instance2(10,20);
     
    instance1 = instance2;
    Dans la documentation que j'ai, il est indiqué qu'il faut avoir la signature suivante (suivi d'une déclaration quelconque) et alors, il est nécessaire de retourner *this :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    const ClasseToto & ClasseToto::operator = (const ClasseToto &instance)
    {
        SetAttribut1(instance.GetAttribut1());
        SetAttribut2(instance.GetAttribut2());
        return *this;
    }
    Par contre, puisque l'opérande de gauche (instance1 dans notre exemple) est l'instance propriétaire de la méthode, je trouve un peu aberrant le retour de soi-même (*this). D'intuition, j'aurais eu plus tendance à faire ceci :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    ClasseToto::operator = (const ClasseToto &instance)
    {
        SetAttribut1(instance.GetAttribut1());
        SetAttribut2(instance.GetAttribut2());
    }
    J'ai testé les deux et j'obtiens les mêmes résultats. Par contre, sont-elles vraiment équivalentes? Si non, qu'elles en sont les différences? Est-ce que l'utilisation d'un return nécessite plus de temps de traitement (aussi minime soit-il)? Est-ce qu'une des deux est à prévaloir?


    Question secondaire:
    Avant d'écrire ce message, j'ai cherché pour trouver d'autres documentations sur la surcharge d'opérateurs et souvent, la signature suivante est présentée :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    ClasseToto ClasseToto::operator = (const ClasseToto &instance)
    {
        SetAttribut1(instance.GetAttribut1());
        SetAttribut2(instance.GetAttribut2());
        return *this;
    }
    Si je comprends bien, cela signifie que le *this retourné sera une copie de l'opérande de gauche. Si c'est exact, malgré que le traitement pour effectuer la copie peut-être négligeable dans bien des cas, pourquoi est-ce que l'on retrouve souvent cette signature dans les exemples au lieu de la signature suivante?
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    const ClasseToto & ClasseToto::operator = (const ClasseToto &instance)

  2. #2
    Expert confirmé
    Avatar de Luc Hermitte
    Homme Profil pro
    Développeur informatique
    Inscrit en
    Août 2003
    Messages
    5 296
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Haute Garonne (Midi Pyrénées)

    Informations professionnelles :
    Activité : Développeur informatique
    Secteur : Aéronautique - Marine - Espace - Armement

    Informations forums :
    Inscription : Août 2003
    Messages : 5 296
    Par défaut
    Par convention, il est attendu que l'opérateur d'affectation renvoie une référence non constante sur l'objet courant.
    Cela permet de chainer des affectations, et diverses autres bidouilles.
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    // de tête ->
    a = b = c = 42;
    (toto = Toto(42)).setJ(43);
    La signature type devient donc
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    T& T::operator=(T const& rhs);
    Si la classe est responsable de ressources (et que donc la règle de deux (rule of big two) n'est plus suffisante -- i.e., en particulier, on ne peut plus se reposer sur l'implémentation par défaut de l'opérateur d'affectation), alors il faut faire attention à divers détails. Dans le cas où le swap ne coûte rien (relativement à l'affectation même), alors le corps type de l'opérateur est:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    T& T::operator=(T const& rhs) {
        T temp(rhs); // copy-construction
        temp.swap(*this); // on échange les contenus et responsabilités
        return *this;
    } // ici, les anciennes ressources de l'objet courant sont restituées
    Implémentation qui est exception-safe, et qui n'est pas impactée par les très éventuels problèmes de réaffectation.


    Evidemment, si la classe ne peut avoir de sémantique de valeur -- typiquement une classe issue d'une hiérarchie polymorphe impliquant une sémantique antagoniste d'entité -- on désactive l'opérateur d'affection. Si j'ouvre cette parenthèse, c'est parce que les cours semblent laisser croire aux débutants (et aux autres) qu'il faut toujours définir l'opérateur d'affectation ainsi que le constructeur de recopie.
    Blog|FAQ C++|FAQ fclc++|FAQ Comeau|FAQ C++lite|FAQ BS|Bons livres sur le C++
    Les MP ne sont pas une hotline. Je ne réponds à aucune question technique par le biais de ce média. Et de toutes façons, ma BAL sur dvpz est pleine...

  3. #3
    Membre émérite
    Profil pro
    Inscrit en
    Mars 2005
    Messages
    1 064
    Détails du profil
    Informations personnelles :
    Âge : 40
    Localisation : Belgique

    Informations forums :
    Inscription : Mars 2005
    Messages : 1 064
    Par défaut
    Retiens cette règle qui pourra t'être utile:
    "Si dans une classe on doit redéfinir le constructeur de copie, l'opérateur d'affectation ou le destructeur, on doit généralement redéfinir les trois en même temps."
    C'est logique, idéalement on veut toujours avoir un constructeur de copie et un opérateur d'affectation qui font strictement la même chose, donc l'un devrait toujours appeler l'autre. Tout le monde ne le sait pas mais le morceau de code suivant
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    T unobjetT = unautreobjetT;
    n'apelle pas du tout le constructeur par défaut puis l'opérateur d'affectation de unobjetT, il appelle le constructeur de copie.
    Pour ce qui est du destructeur, si tu dois gérer manuellement la copie de l'objet c'est que tu as probablement des données pointées, donc il y a fort à parier que tu dois aussi les gérer dans le destructeur.
    Sinon, pour expliquer ce que luc vient de dire avec des mots peut-être trop compliqués, il n'y a rien de mal à empècher la copie d'un objet. C'est même une très bonne habitude à prendre quand tu fais de l'héritage et du polymorphisme. Pour cela il suffit de mettre ton constructeur de copie et ton opérateur d'affectation en private, et de ne même pas définir leur code tant qu'on y est.

  4. #4
    Vol
    Vol est déconnecté
    Membre averti
    Profil pro
    Inscrit en
    Décembre 2005
    Messages
    42
    Détails du profil
    Informations personnelles :
    Localisation : France

    Informations forums :
    Inscription : Décembre 2005
    Messages : 42
    Par défaut
    Merci pour les réponses

    Citation Envoyé par Luc Hermitte
    Par convention, il est attendu que l'opérateur d'affectation renvoie une référence non constante sur l'objet courant.
    Cela permet de chainer des affectations, et diverses autres bidouilles.
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    // de tête ->
    a = b = c = 42;
    (toto = Toto(42)).setJ(43);
    La signature type devient donc
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    T& T::operator=(T const& rhs);
    Enfin, je comprends finalement pourquoi la signature sans retour (ici-bas) n'est pas utilisée.
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    T::operator=(T const& rhs);
    Je n'avais pas remarqué qu'avec cette dernière il n'est pas possible de compiler "a = b = c = 42". Je crois aussi comprendre pourquoi l'auteur de ma doc ne semble pas aimer la signature type par convention que tu me proposes Luc:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    T& T::operator=(T const& rhs);
    L'auteur mentionne que l'utilisation de ceci:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    (toto = Toto(42)).setJ(43);
    constitue un bri d'encapsulation. En effet, l'utilisation de la signature par convention permet de retourner *this ou bien l'opérande de droite (edit: j'avais écrit gauche... ). Donc, selon lui, l'affectation entre parenthèses suivies d'un set/get ne garantit pas le résultat puisque l'opérande de gauche ou de droite pourrait être retournée... Je trouve cela un peu poussé du fait que par convention (enfin, je suppose que ceci est une convention) ça devrait toujours être l'opérande de gauche qui devrait être retournée, soit le *this. Donc, dans ce cas, est-ce vrai de dire qu'il n'y a pas de bris d'encapsulation? Dans le fond, j'imagine que l'utilisation de l'une ou de l'autre (const T&:oper:... ou T&::oper...) relève d'un point de vue de vision de POO (philosophique?) et non seulement de fonctionnalité?

    Citation Envoyé par Luc Hermitte
    Evidemment, si la classe ne peut avoir de sémantique de valeur -- typiquement une classe issue d'une hiérarchie polymorphe impliquant une sémantique antagoniste d'entité -- on désactive l'opérateur d'affection. Si j'ouvre cette parenthèse, c'est parce que les cours semblent laisser croire aux débutants (et aux autres) qu'il faut toujours définir l'opérateur d'affectation ainsi que le constructeur de recopie.
    Citation Envoyé par zais_ethael
    Sinon, pour expliquer ce que luc vient de dire avec des mots peut-être trop compliqués, il n'y a rien de mal à empècher la copie d'un objet. C'est même une très bonne habitude à prendre quand tu fais de l'héritage et du polymorphisme. Pour cela il suffit de mettre ton constructeur de copie et ton opérateur d'affectation en private, et de ne même pas définir leur code tant qu'on y est.
    Merci de l'explication. À la première lecture, je n'avais pas réussi à comprendre le dernier paragraphe de Luc Je me rappelais du comment faire pour "désactiver" le constructeur de copie et l'opérateur d'affectation, mais je n'avais pas saisi dans quel cas il était une bonne chose de l'appliquer. Je vais faire bien attention à ceci lorsque j'aurai du polymorphisme.

    Citation Envoyé par zais_ethael
    Retiens cette règle qui pourra t'être utile:
    "Si dans une classe on doit redéfinir le constructeur de copie, l'opérateur d'affectation ou le destructeur, on doit généralement redéfinir les trois en même temps."
    C'est logique, idéalement on veut toujours avoir un constructeur de copie et un opérateur d'affectation qui font strictement la même chose, donc l'un devrait toujours appeler l'autre. Tout le monde ne le sait pas mais le morceau de code suivant
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    T unobjetT = unautreobjetT;
    n'apelle pas du tout le constructeur par défaut puis l'opérateur d'affectation de unobjetT, il appelle le constructeur de copie.
    Pour ce qui est du destructeur, si tu dois gérer manuellement la copie de l'objet c'est que tu as probablement des données pointées, donc il y a fort à parier que tu dois aussi les gérer dans le destructeur.
    Je vais noter cette règle, elle a bien du bon sens.

    Je laisse le sujet non résolu pour quelque temps au cas ou d'autres personnes voudraient émettre d'autres opinions ou bien si j'ai dit une bêtise dans ce message et que quelqu'un voudrait bien me corriger!

    Merci encore pour les réponses

  5. #5
    Membre émérite
    Profil pro
    Inscrit en
    Mars 2005
    Messages
    1 064
    Détails du profil
    Informations personnelles :
    Âge : 40
    Localisation : Belgique

    Informations forums :
    Inscription : Mars 2005
    Messages : 1 064
    Par défaut
    Citation Envoyé par Vol
    constitue un bri d'encapsulation. En effet, l'utilisation de la signature par convention permet de retourner *this ou bien l'opérande de gauche. Donc, selon lui, l'affectation entre parenthèses suivies d'un set/get ne garantit pas le résultat puisque l'opérande de gauche ou de droite pourrait être retournée... Je trouve cela un peu poussé du fait que par convention (enfin, je suppose que ceci est une convention) ça devrait toujours être l'opérande de gauche qui devrait être retournée, soit le *this. Donc, dans ce cas, est-ce vrai de dire qu'il n'y a pas de bris d'encapsulation? Dans le fond, j'imagine que l'utilisation de l'une ou de l'autre (const T&:oper:... ou T&::oper...) relève d'un point de vue de vision de POO (philosophique?) et non seulement de fonctionnalité?
    Je ne dirais pas que ca relève d'un point de vue philosophique. C'est n'est pas rare de faire en sorte qu'une fonction renvoyant logiquement rien du tout renvoie une réfèrence vers l'objet courant à la place (même dans d'autres langages). Ca permet de ne pas étaler sur plusieurs lignes un appel successif à une fonction. Par exemple:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    unesliste.add(untruc).add(unautretruc).add(encoreuntruc);
    Pour ce qui est de la lisibiliité du code c'est selon le gout de chacun mais en tous cas pour l'opérateur d'affectation du C++ c'est une convention.

  6. #6
    Rédacteur/Modérateur
    Avatar de JolyLoic
    Homme Profil pro
    Développeur informatique
    Inscrit en
    Août 2004
    Messages
    5 463
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 50
    Localisation : France, Yvelines (Île de France)

    Informations professionnelles :
    Activité : Développeur informatique
    Secteur : High Tech - Éditeur de logiciels

    Informations forums :
    Inscription : Août 2004
    Messages : 5 463
    Par défaut
    Citation Envoyé par zais_ethael
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    unesliste.add(untruc).add(unautretruc).add(encoreuntruc);
    Pour ce qui est de la lisibiliité du code c'est selon le gout de chacun mais en tous cas pour l'opérateur d'affectation du C++ c'est une convention.
    L'exemple le plus canonique en C++ étant encoe l'opérateur << (ou >>) et son utilisation pour les flux.
    Ma session aux Microsoft TechDays 2013 : Développer en natif avec C++11.
    Celle des Microsoft TechDays 2014 : Bonnes pratiques pour apprivoiser le C++11 avec Visual C++
    Et celle des Microsoft TechDays 2015 : Visual C++ 2015 : voyage à la découverte d'un nouveau monde
    Je donne des formations au C++ en entreprise, n'hésitez pas à me contacter.

  7. #7
    Expert confirmé
    Avatar de Luc Hermitte
    Homme Profil pro
    Développeur informatique
    Inscrit en
    Août 2003
    Messages
    5 296
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Haute Garonne (Midi Pyrénées)

    Informations professionnelles :
    Activité : Développeur informatique
    Secteur : Aéronautique - Marine - Espace - Armement

    Informations forums :
    Inscription : Août 2003
    Messages : 5 296
    Par défaut
    Citation Envoyé par Vol
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    T& T::operator=(T const& rhs);
    a- L'auteur mentionne que l'utilisation de ceci:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    (toto = Toto(42)).setJ(43);
    constitue un bri d'encapsulation.
    b- En effet, l'utilisation de la signature par convention permet de retourner *this ou bien l'opérande de gauche.
    a- Hein ? Quel raport avec l'encapsulation
    b- Que nenni. Le membre droit (Right HandSide comme son sigle l'indique) ne peut être renvoyé. NB: *this EST le membre gauche ; je soupçonne le lapsus. Ce qui est peut-être bien, maintenant que j'y pense, la vraie raison qui fait que la signature conventionnelle renvoie une référence non constante. Seul l'objet courant peut ainsi être renvoyé avec les opérateurs d'affectation qui n'altèrent pas le membre-droit.

    > [snip: règle de trois]
    Je vais noter cette règle, elle a bien du bon sens.
    IIRC, Dans la FAQ, section RAII, tu as un article qui propose un raffinement faisant descendre de 3 à 2 le nombre d'opérations affectées.
    Blog|FAQ C++|FAQ fclc++|FAQ Comeau|FAQ C++lite|FAQ BS|Bons livres sur le C++
    Les MP ne sont pas une hotline. Je ne réponds à aucune question technique par le biais de ce média. Et de toutes façons, ma BAL sur dvpz est pleine...

  8. #8
    Vol
    Vol est déconnecté
    Membre averti
    Profil pro
    Inscrit en
    Décembre 2005
    Messages
    42
    Détails du profil
    Informations personnelles :
    Localisation : France

    Informations forums :
    Inscription : Décembre 2005
    Messages : 42
    Par défaut
    Merci pour les exemples d'appels sucessifs sur une fonction. Cela me permet de comprendre mieux le pourquoi de la chose

    Citation Envoyé par Luc Hermitte
    a- Hein ? Quel raport avec l'encapsulation
    b- Que nenni. Le membre droit (Right HandSide comme son sigle l'indique) ne peut être renvoyé. NB: *this EST le membre gauche ; je soupçonne le lapsus. Ce qui est peut-être bien, maintenant que j'y pense, la vraie raison qui fait que la signature conventionnelle renvoie une référence non constante. Seul l'objet courant peut ainsi être renvoyé avec les opérateurs d'affectation qui n'altèrent pas le membre-droit.
    Effectivement, il y avait un lapsus. J'ai corrigé cette erreur. Je n'ai pas de compilateur avec moi donc je ne peux tester ce qui suit...

    Ce qui suit est une version abérégée de ma doc
    Si on a la signature qui retourne un référence non constante, comme ceci:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    ClasseToto & ClasseToto::operator = (const ClasseToto &instance)
    {
        SetAttribut1(instance.GetAttribut1());
        SetAttribut2(instance.GetAttribut2());
        return *this;
    }
    alors on pourrait aussi écrire ceci:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    const ClasseToto & ClasseToto::operator = (const ClasseToto &instance)
    {
        SetAttribut1(instance.GetAttribut1());
        SetAttribut2(instance.GetAttribut2());
        return instance;
    }
    Alors, si on choisit l'une ou l'autre des implémentations et qu'on exécute
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    (instance1 = instance2).SetAttribut1(valeur_quelconque);
    le résultat de l'affectation sera imprévisible. Si la méthode qui surcharge = retourne *this, alors le SetAttribut1() modifiera Attribut1 de instance1 (opérande de gauche). Sinon, si l'opérande de droite est retournée par la méthode de la surcharge, SetAttribut1() modifiera Attribut1 de instance2 (opérande de droite).
    Citation Envoyé par tirée de la doc
    Le principe d'encapsulation nous dit que les choix d'implémentation ne devraient pas affecter de manière adverse l'utilisation qui sera faite des objets. Cette utilisation devrait reposer sur l'interface (les méthodes publiques) des objets en question, dont fait partie l'opérateur = ici.
    fin de la version abrégée de ma doc...

    C'est pour ces raisons que l'auteur suggérait la signature suivante:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    const T& T::operator=(T const& rhs);
    pour ainsi empêcher l'utilisation de
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    (instance1 = instance2).SetAttribut1(valeur_quelconque);
    Est-ce qu'il y a bris d'encapsulation ou non?!? Bah... je ne suis pas assez callé dans le domaine pour porter un jugement formel. Pour parler de bris ici, je dirais que c'est chercher des vers, surtout que par convention, c'est *this qui doit être retourné. En tk, c'est pour cette raison que j'avais parlé d'encapsulation...

    Citation Envoyé par Luc Hermitte
    IIRC, Dans la FAQ, section RAII, tu as un article qui propose un raffinement faisant descendre de 3 à 2 le nombre d'opérations affectées.
    Merci, je n'avais pas pris le temps de lire cette FAQ dans sa totalité. Bien intéressant cet article!

    Merci pour les réponses

    (Résolu mais je continuerai de voir si des ajouts sont apportés...)

  9. #9
    Expert confirmé
    Avatar de Luc Hermitte
    Homme Profil pro
    Développeur informatique
    Inscrit en
    Août 2003
    Messages
    5 296
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Haute Garonne (Midi Pyrénées)

    Informations professionnelles :
    Activité : Développeur informatique
    Secteur : Aéronautique - Marine - Espace - Armement

    Informations forums :
    Inscription : Août 2003
    Messages : 5 296
    Par défaut
    Non.
    Si tu utilises la signature conventionnelle, alors seule une référence vers un objet modifiable peut être renvoyée via une référence non constante. Hors rhs est une référence constante, et ne peut donc pas être renvoyé via une référence non constante. (const peut se rajouter, mais pas s'enlever).

    L'ambiguité ne peut avoir lieu qu'avec la signature de ton auteur. Pas avec la signature conventionnelle qui ne permet de renvoyer que l'objet courant.
    Blog|FAQ C++|FAQ fclc++|FAQ Comeau|FAQ C++lite|FAQ BS|Bons livres sur le C++
    Les MP ne sont pas une hotline. Je ne réponds à aucune question technique par le biais de ce média. Et de toutes façons, ma BAL sur dvpz est pleine...

+ Répondre à la discussion
Cette discussion est résolue.

Discussions similaires

  1. Surcharge de l'opérateur new
    Par :Bronsky: dans le forum C++
    Réponses: 17
    Dernier message: 27/10/2010, 21h33
  2. Réponses: 2
    Dernier message: 03/10/2007, 14h59
  3. [Débutant]Surcharge opérateur +
    Par Geolem dans le forum Débuter
    Réponses: 13
    Dernier message: 05/12/2005, 10h16
  4. [Débutant] Créer des belles fenêtres à la façon Linux ou Win
    Par wikers dans le forum x86 32-bits / 64-bits
    Réponses: 10
    Dernier message: 10/02/2005, 13h17
  5. Réponses: 15
    Dernier message: 25/01/2005, 16h51

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