Module 4 - cours du 8 septembre (quiz 3)

Pointeur this, surcharge des opérateurs, constructeur de copie et opérateur d'affectation

Leçons Moodle « Pointeur this & surcharge des opérateurs », « Constructeur par copie & opérateur d'affectation » | Mis en pratique au TP2 (copie du robot, +=, -=, [], <<)

thisAppels en cascadeOpérateur membre ou friendoperator<<++ préfixe et suffixeCopie superficielle et profondeRègle des trois

Le module le plus sollicité aux intras : « donnez l'implémentation de la surcharge de ... », « quelles fonctions membres manquent pour éviter la copie superficielle ? », « combien de fois le constructeur de copie est-il appelé ? ». Tout ce qu'il faut est ici.

1. Le pointeur this

Dans toute méthode, this est un pointeur vers l'objet courant (celui sur lequel la méthode a été appelée). Il est implicite : écrire nom_ revient à écrire this->nom_. On l'explicite dans trois situations :

  1. Conflit de noms : un paramètre porte le même nom qu'un attribut. void setNom(string nom) { this->nom = nom; }. (Avec la convention du suffixe _, le conflit n'arrive pas : nom_ = nom;.)
  2. Retourner l'objet courant : return *this; dans operator=, operator+=, pour permettre les appels en cascade (robot += moteur) += capteur;.
  3. Comparer des adresses : if (this != &autre) détecte l'auto-affectation a = a;.

Dans une méthode const, this est de type const Classe* : impossible de modifier *this.

2. Surcharge des opérateurs

Surcharger un opérateur, c'est définir une fonction nommée operatorop pour qu'une expression comme a + b ou cout << a ait un sens pour vos classes. Le compilateur traduit a + b en a.operator+(b) (fonction membre) ou operator+(a, b) (fonction libre).

Règles
  • On ne peut pas inventer de nouveaux opérateurs ni changer leur priorité, leur associativité ou leur nombre d'opérandes (un opérateur unaire agit sur l'objet courant seul : -a, ++a, !a ; un binaire a un opérande gauche et un droit).
  • Au moins un opérande doit être d'un type classe (on ne redéfinit pas int + int).
  • Non surchargeables : ::, ., .*, ?:, sizeof.
  • Une surcharge n'implique pas les autres : définir == ne donne pas !=, définir + ne donne pas +=.

Fonction membre ou fonction amie (friend) ?

Fonction membreFonction libre (friend si elle touche aux membres privés)
FormeType operatorop(const Droite& d) const; : l'opérande gauche est *thisfriend Type operatorop(const Gauche& g, const Droite& d); : les deux opérandes sont des paramètres
Quandl'opérande gauche est un objet de la classel'opérande gauche est d'un autre type (ostream, int), ou pour que l'opérateur soit commutatif (2 * v et v * 2)
Obligatoirement membre=, [], (), ->
Obligatoirement libre<< et >> sur les flux (l'opérande gauche est le flux, pas notre objet)

Une fonction friend est une fonction globale déclarée dans la classe avec le mot-clé friend : elle n'est pas membre (pas de this, pas de Classe:: dans sa définition), mais elle a accès aux membres privés. Sans friend, la fonction libre doit passer par les accesseurs publics.

Les signatures à connaître par cœur

Comment choisir le type de retour
  • +, -, * produisent une nouvelle valeur : retour par valeur, méthode const.
  • =, +=, -=, ++ préfixe modifient l'objet : retour Classe& avec return *this;, jamais const.
  • ==, <, != : bool, const.
  • << : ostream& (le même flux), pour enchaîner cout << a << b << endl;.
  • [] : une référence (double&) pour permettre v[0] = 5;, plus une version const qui retourne const double& ou une copie. Le TP2 retourne const shared_ptr<Moteur>& : on peut régler le moteur pointé, pas remplacer le pointeur.

Pourquoi les appels en cascade fonctionnent

cout << "x = " << x << endl; s'évalue de gauche à droite : operator<<(cout, "x = ") retourne cout (une référence vers le même flux), qui devient l'opérande gauche de l'appel suivant. C'est exactement comme écrire les trois instructions l'une après l'autre, en plus lisible. Si operator<< retournait void, le deuxième << n'aurait pas de flux à gauche : erreur de compilation. Même principe pour (robot += moteur) += capteur; et a = b = c;.

Pourquoi un paramètre int fictif pour le ++ suffixe ?

++mc et mc++ appellent tous deux un opérateur à un seul opérande : impossible de les distinguer par les arguments. Le langage impose donc que la version suffixe déclare un paramètre int jamais utilisé : mc++ est traduit en mc.operator++(0), ++mc en mc.operator++(). La version préfixe incrémente et retourne l'objet lui-même (par référence) ; la version suffixe sauvegarde une copie, incrémente via la préfixe, et retourne la copie (par valeur : l'ancienne valeur).

Trois écritures équivalentes

Surcharger << sans friend (intra H16)

Quand la classe n'a pas déclaré la fonction friend, on écrit une fonction libre qui n'utilise que l'interface publique :

3. Constructeur de copie et opérateur d'affectation

Le compilateur fournit par défaut un constructeur de copie et un opérateur d'affectation qui copient membre à membre. Pour une classe faite de string, vector, double et shared_ptr, c'est exactement ce qu'il faut (chaque membre sait se copier correctement : un vecteur copie ses éléments). Le problème surgit avec un pointeur brut vers une ressource possédée (composition par pointeur) : la copie membre à membre copie l'adresse, pas l'objet pointé.

Copie superficielle ou profonde ?

La classe Tableau possède un bloc new int[3]. Comparez la copie par défaut et un vrai constructeur de copie, puis détruisez les objets.

Copie superficielle (shallow copy) : les trois symptômes
  1. Modifier l'un des objets modifie l'autre (ils partagent le même bloc).
  2. La destruction du premier objet laisse le second avec un pointeur pendant.
  3. La destruction du second fait un double delete : plantage.

Les deux fonctions à écrire

Règle des trois

Si une classe a besoin de l'un des trois (destructeur, constructeur de copie, opérateur d'affectation), elle a presque sûrement besoin des trois. Le marqueur : un new dont la classe est responsable. Si la copie n'a pas de sens (TP1), on l'interdit : Robot(const Robot&) = delete; Robot& operator=(const Robot&) = delete;. Si la copie par défaut convient, on peut l'écrire explicitement : = default. (En C++ moderne s'ajoutent le constructeur et l'affectation de déplacement, T(T&&) : règle des cinq. Le cours s'en tient aux trois.)

Quand le constructeur de copie est-il appelé ?

b = a; entre deux objets déjà construits appelle operator=, jamais le constructeur de copie. La ligne Robot b = a; est trompeuse : c'est une initialisation (constructeur de copie), pas une affectation.

Devinez l'affichage : constructeur, copie, affectation, destructeur

Explication (compilé en C++17 ou plus, comme aux TP)
  1. P a(1) : ctor 1. P b = a : initialisation, donc copie 1. P c(2) : ctor 2.
  2. b = c : objets existants, affect 2 (b vaut maintenant 2).
  3. f(b) : le paramètre p est construit par copie (copie 2). return p construit la valeur de retour par copie du paramètre (copie 2). À la sortie de f, p est détruit (dtor 2). La valeur de retour devient directement d (élision garantie) : pas de copie supplémentaire.
  4. Fin de main : destruction dans l'ordre inverse de déclaration : d (2), c (2), b (2), a (1).

Copie et affectation avec des pointeurs intelligents (TP2)

*autre.batterie_ déréférence le unique_ptr pour obtenir la Batterie, dont le constructeur de copie reproduit tous les attributs, y compris l'énergie restante (copier une batterie déchargée ne la remplit pas). Copier un robot ne duplique donc que ce qu'il possède en exclusivité ; les pièces partagées voient leur use_count augmenter.

4. À retenir

Quiz rapide - Module 4

Qu'est-ce que this ?

  • Une référence vers la classe
  • Une copie de l'objet courant
  • Un pointeur vers l'objet sur lequel la méthode est appelée
  • Un pointeur vers le premier attribut
Dans robot.afficher(), this == &robot à l'intérieur d'afficher. *this est l'objet lui-même.

Peut-on surcharger operator<< (flux de sortie) comme fonction membre de la classe AA ?

  • Oui, c'est même recommandé
  • Non : l'opérande gauche est le flux, pas un AA ; il faut une fonction libre (friend ou non)
  • Oui, si AA hérite de ostream
  • Oui, mais seulement pour cout
Une fonction membre AA::operator<<(ostream&) correspondrait à aa << cout. Pour cout << aa, la fonction doit être libre. Question vrai/faux de l'intra H16 : FAUX.

Les surcharges de « objet AA + int » et « int + objet AA » sont toutes deux des fonctions membres de AA.

  • Vrai
  • Vrai si AA définit un constructeur à partir d'un int
  • Faux : aucune des deux ne peut être membre
  • Faux : seule « AA + int » peut être membre ; « int + AA » doit être une fonction libre (friend)
Une fonction membre a toujours l'objet courant comme opérande gauche. Quand l'opérande gauche est un int, il faut une fonction libre.

Quelle signature est correcte pour l'opérateur += de la classe Complexe, fonction membre ?

  • Complexe& operator+=(const Complexe& c);
  • Complexe operator+=(const Complexe& c) const;
  • void operator+=(Complexe c);
  • friend Complexe& operator+=(Complexe& a, const Complexe& b);
Modifie l'objet (pas const), retourne une référence pour le chaînage. La quatrième compile mais contredit l'énoncé (« doit être une méthode »). Question de l'intra H15.

Pourquoi operator<< retourne-t-il une référence vers le flux ?

  • Pour éviter de copier l'objet affiché
  • Pour permettre les appels en cascade : le résultat de cout << a sert d'opérande gauche à << b
  • Parce que ostream n'a pas de destructeur
  • Pour que l'opérateur soit commutatif
Il s'agit de l'objet de classe ostream. Retourner void interdirait cout << a << b.

Quelle est la différence entre T& operator++() et T operator++(int) ?

  • La première incrémente de 1, la seconde de la valeur de l'int
  • La première est friend, la seconde membre
  • La première est le ++ préfixe (retourne l'objet incrémenté par référence), la seconde le ++ suffixe (l'int fictif sert seulement à les distinguer ; retourne l'ancienne valeur par valeur)
  • Aucune, ce sont des synonymes
mc++ devient mc.operator++(0). Le paramètre n'est jamais utilisé.

Dans operator=, comment vérifier qu'on n'affecte pas un objet à lui-même ?

  • if (this != &autre) : comparer l'adresse de l'objet courant à celle du paramètre
  • if (*this != autre) : comparer les contenus
  • if (this != autre)
  • Ce n'est jamais nécessaire
Comparer les contenus (b) appellerait operator!= et laisserait passer deux objets distincts de même valeur. Sans ce test, t = t; ferait delete[] sur les données avant de les copier : lecture d'une mémoire libérée.

Quelles fonctions membres manquent à cette classe pour éviter la copie superficielle de attribut_ ?

  • Un constructeur par défaut
  • Un accesseur et un mutateur
  • Seulement le constructeur de copie
  • Le constructeur de copie et l'opérateur d'affectation
Règle des trois : le destructeur est là, il manque AA(const AA&) (appelé par uneFonction(deux), passage par valeur, et par les retours par valeur) et AA& operator=(const AA&) (appelé par un = deuxFonction();). Question de l'intra H16.

Laquelle de ces lignes appelle l'opérateur d'affectation plutôt que le constructeur de copie ?

  • Robot b = a;
  • b = a; (b déjà déclaré)
  • Robot b(a);
  • f(a); avec void f(Robot r)
Seule l'affectation entre deux objets existants appelle operator=. Robot b = a; est une initialisation malgré le signe =.

La classe Achats n'a qu'un attribut vector<Item> mesAchats_. Pour permettre liste2 = liste;, faut-il écrire un opérateur d'affectation ?

  • Oui, obligatoirement, sinon erreur de compilation
  • Oui, pour éviter la copie superficielle du vecteur
  • Non : l'opérateur par défaut appelle operator= de vector, qui copie les éléments en profondeur
  • Non, car un vecteur ne peut pas être copié
Pas d'allocation dynamique gérée à la main : l'affectation membre à membre suffit (réponse attendue à l'intra H16).

Pourquoi operator+ devrait-il être const et retourner par valeur ?

  • Il crée un nouvel objet sans modifier ses opérandes ; retourner une référence pointerait vers une variable locale détruite
  • Pour qu'il soit commutatif
  • Parce que le compilateur l'exige
  • Pour pouvoir l'enchaîner avec <<
a + b ne change ni a ni b : const. Le résultat est un objet neuf, construit dans la fonction : par valeur.

Que se passe-t-il lors de la copie d'un Robot du TP2 (batterie en unique_ptr, moteurs en vector de shared_ptr) avec le constructeur de copie décrit dans l'énoncé ?

  • Les deux robots partagent la même batterie
  • Les moteurs sont dupliqués
  • Rien n'est copié, c'est une erreur
  • La batterie est dupliquée (objet distinct) ; les moteurs sont partagés (mêmes objets, use_count augmente)
C'est précisément ce que la démonstration du TP2 met en évidence : la mission de la copie ne décharge pas la batterie du prototype, mais régler le moteur partagé change les deux.

Quel est l'intérêt de l'idiome copie-et-échange (copy-and-swap) dans operator= ?

  • Il évite d'écrire un constructeur de copie
  • Il réutilise le constructeur de copie et le destructeur ; si la copie échoue, l'objet d'origine n'est pas modifié à moitié
  • Il est plus rapide que la copie membre à membre
  • Il rend l'opérateur commutatif
On construit une copie temporaire, on échange, et la temporaire (avec les anciennes ressources) se détruit seule.

Quel doit être le type de retour de operator[] pour que v[0] = 5; compile ?

  • double
  • const double&
  • double&
  • bool
Pour écrire dans l'élément, il faut une référence non constante. On ajoute une surcharge const pour les objets constants. Si on refuse l'écriture (intra H16 : retourner un Item par défaut hors bornes), retourner par valeur est un choix de conception valable.

Une fonction friend d'une classe :

  • N'est pas membre de la classe (pas de this) mais a accès à ses membres privés
  • Est une méthode statique
  • Doit être définie dans le .cpp avec Classe::
  • Ne peut accéder qu'aux membres publics
Elle est déclarée dans la classe avec le mot-clé friend, définie comme une fonction globale (sans Classe::).

Exercices à développement

Intra H16Opérateurs

Achats : operator[], operator+ et operator<<

Avec les classes Item et Achats du module 2 (attribut vector<Item> mesAchats_, accesseur public getItems()), donnez uniquement l'implémentation de :

  1. la surcharge de l'opérateur d'indexation [], qui retourne l'item à l'indice demandé ; vous devez vérifier les bornes et retourner un Item par défaut si l'indice sort des bornes ;
  2. la surcharge de l'opérateur +, fonction membre, qui combine deux listes d'achats en une nouvelle liste ;
  3. sans fonction amie, la surcharge de << qui affiche les items d'un Achats.
Solution

Avec un indice int, il faudrait tester index >= 0 && index < mesAchats_.size() (et convertir pour éviter -Wsign-compare). Déclarations correspondantes dans la classe : Item operator[](size_t index) const; et Achats operator+(const Achats& autre) const;.

Intra H16Règle des trois

AA : éviter la copie superficielle

Pour la classe AA du quiz (attribut string* attribut_, destructeur qui fait delete attribut_), écrivez les deux fonctions membres manquantes. Attention : attribut_ peut être nullptr.

Solution

Dans le main de l'énoncé, uneFonction(deux) (paramètre par valeur) et AA deuxFonction() (retour par valeur) appellent le constructeur de copie ; un = deuxFonction(); appelle l'opérateur d'affectation. Sans ces fonctions : double delete à la sortie de uneFonction.

Intra H15Opérateurs

Classe Complexe

Écrivez la classe Complexe (partie réelle et imaginaire, double) avec : constructeur (valeurs par défaut 0), += (méthode), + entre deux complexes (méthode const), * entre un double et un complexe dans les deux sens, ==, - unaire, et << au format 3 + 2i.

Solution

Variante sans friend pour + : Complexe operator+(const Complexe& a, const Complexe& b) { Complexe r(a); r += b; return r; } réutilise +=.

TP2Copie

Copie d'un robot avec pointeurs intelligents

La classe Robot possède std::string nom_, std::unique_ptr<Batterie> batterie_, std::unique_ptr<Chassis> chassis_ et std::vector<std::shared_ptr<Moteur>> moteurs_. Écrivez le constructeur de copie (batterie et châssis dupliqués s'ils existent, moteurs partagés) et l'opérateur d'affectation avec l'idiome copie-et-échange. Pourquoi ne peut-on pas simplement écrire batterie_(autre.batterie_) ?

Solution

batterie_(autre.batterie_) tenterait de copier un unique_ptr : constructeur supprimé, erreur de compilation. Il faut construire une nouvelle batterie à partir de l'objet pointé, *autre.batterie_, avec le constructeur de copie de Batterie. L'opérateur ternaire garde nullptr si l'original n'a pas de batterie.

AffichageCopie

Qui est appelé ?

La classe Z affiche « ctor », « copie », « affect » ou « dtor » dans chacune de ses quatre fonctions spéciales (comme la classe P plus haut). Donnez l'affichage de :

Indice

Comptez les objets créés (chacun finira par un « dtor ») et distinguez initialisation (constructeur de copie) et affectation entre objets existants (operator=, même quand l'objet est affecté à lui-même).

Solution
  1. Z a; ctor. Z b(a); copie. Z c = b; copie (initialisation, pas affectation).
  2. a = b; affect.
  3. parValeur(a) : copie du paramètre, puis dtor à la sortie de la fonction. parRef(a) : rien du tout.
  4. new Z(b) : copie sur le tas ; delete p : dtor.
  5. c = c; : l'opérateur d'affectation est appelé (affect) ; c'est le test this != &autre à l'intérieur qui évite de casser l'objet.
  6. Fin de main : dtor c, dtor b, dtor a (ordre inverse de déclaration). Au total 5 objets créés, 5 détruits.

Pour l'examen, l'analyse attendue : une copie par passage par valeur, une par new Z(objet) ou Z x = objet, aucune par référence ; une affectation par = entre objets existants.

Révision H2019Opérateurs

Personnage : égalité, affichage, incrémentation

La classe Personnage a string nom_, int niveau_, int experience_. Écrivez : operator== (même nom et même niveau), operator!= en réutilisant ==, operator<< (friend) qui affiche « nom (niveau N) », ++ préfixe (monte d'un niveau) et ++ suffixe, puis operator+=(int xp) qui ajoute de l'expérience et monte d'un niveau par tranche de 100 points. Montrez ensuite un appel en cascade.

Solution