Module 3 - cours du 4 septembre (quiz 2)

Pointeurs intelligents, composition et agrégation

Leçons Moodle « Pointeurs intelligents », « Composition et agrégation » | Mis en pratique au TP1 (composition et agrégation par pointeurs bruts) et au TP2 (unique_ptr pour la batterie et le châssis, shared_ptr pour les pièces partagées)

Composition (a un, possède)Agrégation (utilise)Propriété et durée de vieunique_ptrshared_ptrmake_unique / make_sharedstd::move

Question centrale de ce module, et de beaucoup de questions d'intra : qui possède l'objet, et donc qui est responsable de le détruire ? La composition répond « le contenant », l'agrégation répond « quelqu'un d'autre », et les pointeurs intelligents encodent cette réponse dans le type.

1. Les trois relations entre classes

CompositionAgrégationHéritage (modules 5 et 6)
Phrase cléA possède un B (« a un », partie de)A utilise un B (« a un », mais sans le posséder)A est un B
Exempleune voiture a 4 roues ; une roue appartient à une seule voitureune compagnie emploie des employés ; un employé peut travailler pour plusieurs compagniesun gérant est un employé
Durée de vieliée : le B est créé et détruit avec le Aindépendante : le B existe avant et après le Aun seul objet
Partagenon : un seul propriétaireoui : plusieurs A peuvent utiliser le même Bsans objet
UMLlosange plein du côté du contenantlosange videflèche à pointe vide vers la classe de base
Dans le TP1batterie et châssis du robot (le robot les reçoit, les libère, les remplace)moteurs, capteurs, pinces (le robot ne fait que mémoriser leur adresse)TP3 : Moteur : public Composante

Reconnaître la relation dans le code

Attribut dans la classe AInitialisationRelationQui détruit le B
B b_; (objet)liste d'initialisation, copie de l'argumentcomposition (par valeur)automatique, avec A
B* b_; avec new dans Ab_(new B(...)) ou b_(new B(*autre))composition (par pointeur)le destructeur de A (delete b_;)
B* b_; reçu de l'extérieurb_(ptr) ou b_(&objet)agrégation (par pointeur)l'extérieur, jamais A
B& b_;obligatoirement dans la liste d'initialisation (une référence ne peut pas rester vide ni changer de cible)agrégation (par référence)l'extérieur ; l'objet doit vivre plus longtemps que A
unique_ptr<B> b_;make_unique<B>(...) ou std::move(ptr)composition (propriété exclusive)automatique, avec A
shared_ptr<B> b_;copie d'un shared_ptr reçupartage (agrégation avec propriété partagée)automatique, quand le dernier propriétaire disparaît
Intra H15, exercice 3 : composition ou agrégation ?

Composition : attribut objet, copie privée.

Agrégation : le pointeur vient de l'extérieur, qui le détruit.

Composition par pointeur : BB fait le new (et devra faire le delete).

Agrégation par référence : BB désigne l'objet du main.

Agrégation : l'objet est une variable locale du main, BB ne doit surtout pas le détruire.

Méthode

Cherchez le new. S'il est dans la classe (constructeur ou méthode d'installation qui prend la propriété), c'est une composition et le destructeur de la classe doit faire le delete. Si l'objet vient de l'extérieur et que la classe n'en fait que mémoriser l'adresse, c'est une agrégation : pas de delete dans la classe. Un attribut objet (sans pointeur) est toujours une composition.

Composition et agrégation par pointeurs bruts (TP1)

Conséquences pour l'appelant (énoncé du TP1)
  • Ne jamais passer l'adresse d'un objet local à installerBatterie : le robot tenterait un delete sur la pile.
  • Ne pas faire delete sur une batterie installée, ni la confier à deux robots (double libération).
  • Garder les moteurs vivants tant qu'un robot les référence ; les supprimer après leur retrait de tous les robots, exactement une fois.
  • Ne pas copier un robot : la copie membre à membre dupliquerait batterie_ et les deux destructeurs feraient delete sur le même objet. Le TP1 interdit la copie avec Robot(const Robot&) = delete;.

2. Pointeurs intelligents

Un pointeur intelligent est un petit objet qui possède un pointeur brut et appelle delete dans son propre destructeur. Comme il est une variable automatique (ou un attribut), la libération suit automatiquement la fin de portée ou la destruction du contenant : plus de fuite, plus de double libération, et les fonctions qui quittent par exception restent sûres. En-tête <memory>.

unique_ptr : propriété exclusive

Pas à pas : transfert de propriété avec unique_ptr

Suivez qui possède l'objet Batterie, et quand son destructeur s'exécute.

shared_ptr : propriété partagée

Erreurs fatales avec les pointeurs intelligents
  • Créer deux pointeurs intelligents à partir du même pointeur brut : Moteur* p = new Moteur(...); shared_ptr<Moteur> a(p); shared_ptr<Moteur> b(p); → deux compteurs indépendants, double delete. Toujours copier le shared_ptr, jamais reconstruire depuis .get().
  • Faire delete ptr.get() : le pointeur intelligent fera un second delete.
  • Conserver un pointeur brut obtenu par get() après que le propriétaire a été remplacé ou détruit : pointeur pendant (règle énoncée dans les TP pour getBatterie()).
  • Oublier std::move pour un unique_ptr : erreur de compilation « call to deleted constructor ».

Simulateur de shared_ptr

Trois variables a, b, c de type shared_ptr<Objet>. Exécutez des instructions et suivez le compteur de références et les destructions.

Devinez l'affichage : use_count et destructions

Explication

b = a partage A (compteur 2). b = c : b lâche A (compteur 1) et partage C (compteur 2). À la fin du bloc, c disparaît mais C survit grâce à b (compteur 1). a.reset() fait tomber A à 0 : « -A » immédiatement. « fin », puis la destruction de b à la fin de main détruit C.

Le modèle du TP2 : unique_ptr pour posséder, shared_ptr pour partager

Plus aucun new ni delete explicite : la destruction du robot libère la batterie (unique_ptr) et décrémente le compteur des moteurs (détruits seulement si personne d'autre ne les partage).

3. À retenir

Quiz rapide - Module 3

Dans class Voiture { Moteur moteur_; };, la relation entre Voiture et Moteur est :

  • Une composition : le moteur est créé et détruit avec la voiture
  • Une agrégation : le moteur peut être partagé
  • Un héritage
  • Indéterminée sans voir le constructeur
Un attribut objet (par valeur) est toujours une composition : il fait physiquement partie de l'objet contenant.

Dans class Equipe { Entraineur* coach_; }; avec Equipe(Entraineur* c) : coach_(c) {} et aucun delete dans Equipe :

  • Composition par pointeur
  • Agrégation : l'entraîneur vient de l'extérieur, qui reste responsable de sa destruction
  • Erreur de compilation
  • Composition, car un pointeur implique toujours la possession
Pas de new dans la classe : elle ne fait que mémoriser une adresse. Un entraîneur pourrait d'ailleurs être agrégé par plusieurs équipes.

Pour un attribut Licorne& licorne_;, quelle affirmation est vraie ?

  • Il peut être affecté dans le corps du constructeur
  • Il peut être changé de cible plus tard par licorne_ = autre;
  • Il doit être initialisé dans la liste d'initialisation et désigne toujours le même objet
  • Il représente une composition
Une référence doit être liée à sa création et ne peut pas être réassignée (licorne_ = autre copierait le contenu d'autre dans l'objet référencé). C'est une agrégation : l'objet appartient à l'extérieur.

Quelle est la différence essentielle entre unique_ptr et shared_ptr ?

  • shared_ptr ne libère jamais l'objet
  • unique_ptr est plus lent
  • unique_ptr ne peut pointer qu'un seul type
  • unique_ptr a un seul propriétaire (non copiable) ; shared_ptr compte ses propriétaires et détruit l'objet quand le dernier disparaît
Un seul propriétaire = composition ; propriété partagée = compteur de références.

Que se passe-t-il ici ?

  • b et a pointent le même objet
  • Erreur de compilation : le constructeur de copie de unique_ptr est supprimé
  • b reçoit l'objet et a devient nul
  • L'objet est dupliqué
Pour transférer, il faut écrire explicitement unique_ptr<Batterie> b = std::move(a);.

Après auto p = make_shared<Moteur>("m", 1, 10); auto q = p; auto r = q; q.reset();, que vaut p.use_count() ?

  • 1
  • 3
  • 2
  • 0
p, q, r partagent le moteur (3), puis q lâche sa part (2). L'objet n'est pas détruit.

Pourquoi ce code est-il faux ?

  • a et b ont chacun leur compteur : l'objet sera détruit deux fois
  • Il ne compile pas
  • Il crée une fuite de mémoire
  • Rien : a et b partagent correctement l'objet
Le partage ne fonctionne qu'en copiant un shared_ptr existant (b = a). Construire deux shared_ptr depuis le même pointeur brut est une double libération garantie. Utilisez make_shared.

Que fait installerBatterie(std::move(batterie)) au unique_ptr nommé batterie de l'appelant ?

  • Il le copie
  • Rien, il reste utilisable
  • Il détruit l'objet Batterie
  • Il le vide : après l'appel, batterie vaut nullptr et le robot possède l'objet
C'est le transfert de propriété. Énoncé du TP2 : « Après le transfert d'un unique_ptr nommé, le pointeur de l'appelant est nul. »

Un vector<shared_ptr<Capteur>> est détruit alors qu'un autre shared_ptr vers l'un des capteurs existe ailleurs. Que se passe-t-il ?

  • Tous les capteurs sont détruits
  • Les capteurs partagés ailleurs survivent ; les autres sont détruits
  • Aucun capteur n'est détruit
  • Le programme plante
Chaque compteur est décrémenté ; seuls ceux qui tombent à 0 entraînent une destruction.

Quelle est la bonne manière de créer un objet géré par shared_ptr ?

  • shared_ptr<Moteur> m = new Moteur("m", 1, 10);
  • shared_ptr<Moteur> m; m = Moteur("m", 1, 10);
  • auto m = make_shared<Moteur>("m", 1, 10);
  • shared_ptr<Moteur> m = &Moteur("m", 1, 10);
La première ne compile pas (conversion implicite interdite, constructeur explicit) ; make_shared alloue l'objet et son compteur en une fois.

Dans le TP1, pourquoi la copie du robot est-elle interdite (= delete) ?

  • Parce que la copie par défaut dupliquerait les pointeurs propriétaires, et les deux destructeurs libéreraient la même batterie
  • Parce qu'un robot est trop gros pour être copié
  • Parce que les vecteurs ne sont pas copiables
  • Parce que le nom doit être unique
C'est le problème de la copie superficielle, résolu au TP2 par un constructeur de copie qui duplique la batterie (module 4).

Quel est le rôle de batterie_.get() dans const Batterie* getBatterie() const ?

  • Transférer la propriété à l'appelant
  • Créer une copie de la batterie
  • Libérer la batterie
  • Donner un pointeur d'observation, sans propriété, valide tant que le robot garde cette batterie
L'appelant ne doit ni faire delete ni construire un pointeur intelligent à partir de cette adresse. Il devient invalide si la batterie est remplacée ou le robot détruit.

Deux robots du TP2 partagent le même shared_ptr<Moteur>. On règle la charge du moteur depuis le premier robot. Que voit le second ?

  • L'ancienne charge : chaque robot a sa copie
  • La nouvelle charge : c'est le même objet
  • Une erreur d'accès
  • Cela dépend de l'ordre de création
Copier un shared_ptr partage l'objet. Le TP2 utilise cela pour montrer la différence avec les batteries, dupliquées par le constructeur de copie.

Quand faut-il écrire un destructeur explicite dans une classe ?

  • Toujours
  • Jamais depuis C++11
  • Quand la classe possède une ressource acquise manuellement (pointeur brut issu de new, fichier) : composition par pointeur brut
  • Quand la classe a des attributs de type vector
Marqueur : s'il y a un new dont la classe est responsable, il faut un delete dans le destructeur. Avec des unique_ptr, vector et string, le destructeur implicite suffit.

À quoi sert weak_ptr ?

  • À observer un objet géré par shared_ptr sans en être propriétaire (ne compte pas), par exemple pour éviter les cycles
  • À posséder un objet de façon exclusive
  • À remplacer les pointeurs bruts dans les tableaux
  • À convertir un unique_ptr en shared_ptr
Un cycle de shared_ptr (A pointe B, B pointe A) ne tombe jamais à 0 ; un des deux liens doit être un weak_ptr.

Exercices à développement

Intra H16Relations

Composition ou agrégation ? (PaysDesMerveilles)

Pour chacune des trois versions, dites s'il s'agit de composition ou d'agrégation pour les attributs lutin_ et licorne_, et justifiez.

Solution
  • Version 1 : lutin_ est un objet → composition (le constructeur copiera *lutin dedans). licorne_ est une référence → agrégation (une référence vers un objet extérieur ; une composition n'aurait pas de sens par référence). Remarque : le constructeur reçoit licorne par valeur, donc licorne_ référencerait un paramètre détruit à la fin du constructeur : erreur de conception.
  • Version 2 : les deux attributs sont alloués avec new dans la classe (copie de *licorne, nouveau Lutin) → composition par pointeur pour les deux ; la classe doit les libérer dans son destructeur.
  • Version 3 : lutin_ reçoit un pointeur venu de l'extérieur sans new → agrégation par pointeur. licorne_ est un objet → composition (copie du paramètre).
TP1Composition par pointeur brut

installerChassis et le destructeur du robot

Avec les attributs Chassis* chassis_ et Batterie* batterie_ (pointeurs bruts), écrivez void Robot::installerChassis(Chassis* chassis) selon le contrat du TP1 : le robot reçoit la propriété d'un objet alloué avec new ; lors d'un remplacement, l'ancienne pièce est libérée ; installer nullptr retire et libère la pièce ; réinstaller exactement le même pointeur est sans effet. Écrivez aussi le destructeur.

Solution

Le test chassis == chassis_ en premier est indispensable : sans lui, réinstaller le même pointeur ferait delete puis stockerait une adresse libérée.

TP2unique_ptr et shared_ptr

Retirer une pièce partagée et transférer une batterie

Avec std::vector<std::shared_ptr<Capteur>> capteurs_ et std::unique_ptr<Batterie> batterie_, écrivez :

  1. Robot& Robot::operator-=(const std::shared_ptr<Capteur>& capteur) : retire le pointeur s'il est présent, en conservant l'ordre des autres ; retirer un capteur absent ne fait rien ; retourne le robot pour permettre le chaînage.
  2. std::unique_ptr<Batterie> Robot::retirerBatterie() : rend la batterie à l'appelant (le robot n'en a plus).
Indice

Comparer deux shared_ptr avec == compare les adresses pointées (même objet). Pour rendre un unique_ptr, retournez std::move(batterie_) : l'attribut devient nul.

Solution
Simulateur de hockeyFuite de mémoire

Corriger une fuite avec des pointeurs intelligents

Ce simulateur souffre d'une fuite de mémoire. Identifiez-la et corrigez-la en utilisant des pointeurs intelligents, sans changer le comportement.

Solution

Les joueurs sont alloués avec new et jamais libérés : ni delete dans un destructeur de Match, ni dans le main. Deux objets Joueur fuient. Correction : le match possède ses joueurs, donc unique_ptr (ou shared_ptr si on veut un jour les partager entre matchs).

Autre correction acceptable sans pointeurs intelligents : un destructeur ~Match() { for (Joueur* j : joueurs_) delete j; }, et alors interdire ou écrire correctement la copie de Match (module 4).

DébogagePointeurs intelligents

Trouver les erreurs

Chaque extrait contient une erreur (compilation ou exécution). Laquelle ?

Solution
  • a) Erreur de compilation : passer un unique_ptr par valeur exige un transfert explicite, r.installerMoteur(std::move(m));.
  • b) Double libération : le shared_ptr détruira encore le capteur à la fin. Ne jamais faire delete sur get().
  • c) Le unique_ptr possède l'adresse d'une variable locale : à la destruction du robot, delete sur la pile, plantage. Il faut make_unique.
  • d) Après std::move(b), b est nul : b->getNom() déréférence nullptr. Utiliser r.getBatterie()->getNom().
ConceptionUML et code

Modéliser un atelier

Un atelier contient des établis (créés et détruits avec l'atelier, jamais partagés). Chaque établi utilise un outil pris dans l'armoire de l'atelier ; un même outil peut être utilisé par plusieurs établis au fil de la journée, et il existe même quand aucun établi ne l'utilise. Proposez les attributs (avec le bon type de pointeur) des classes Atelier et Etabli, et dites quelle relation lie chaque paire de classes.

Solution

Atelier - Etabli : composition (losange plein) ; Etabli - Outil : agrégation (losange vide) ; Atelier - Outil : composition de l'armoire, mais comme les outils sont partagés avec les établis, shared_ptr garantit qu'un outil n'est détruit que lorsqu'il n'est plus ni dans l'armoire ni utilisé. Un Outil* brut dans Etabli serait aussi acceptable (observation) tant que l'armoire survit aux établis.