Pointeurs intelligents, composition et agrégation
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
| Composition | Agrégation | Hé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 |
| Exemple | une voiture a 4 roues ; une roue appartient à une seule voiture | une compagnie emploie des employés ; un employé peut travailler pour plusieurs compagnies | un gérant est un employé |
| Durée de vie | liée : le B est créé et détruit avec le A | indépendante : le B existe avant et après le A | un seul objet |
| Partage | non : un seul propriétaire | oui : plusieurs A peuvent utiliser le même B | sans objet |
| UML | losange plein du côté du contenant | losange vide | flèche à pointe vide vers la classe de base |
| Dans le TP1 | batterie 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 A | Initialisation | Relation | Qui détruit le B |
|---|---|---|---|
B b_; (objet) | liste d'initialisation, copie de l'argument | composition (par valeur) | automatique, avec A |
B* b_; avec new dans A | b_(new B(...)) ou b_(new B(*autre)) | composition (par pointeur) | le destructeur de A (delete b_;) |
B* b_; reçu de l'extérieur | b_(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çu | partage (agrégation avec propriété partagée) | automatique, quand le dernier propriétaire disparaît |
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.
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)
- Ne jamais passer l'adresse d'un objet local à
installerBatterie: le robot tenterait undeletesur la pile. - Ne pas faire
deletesur 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 feraientdeletesur le même objet. Le TP1 interdit la copie avecRobot(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
- Non copiable, déplaçable :
std::movetransfère la propriété ; le pointeur source devientnullptr. Après le transfert, ne plus utiliser la source. - Passer un
unique_ptrpar valeur à une fonction = lui donner l'objet (il faut écriref(std::move(b))). Retourner ununique_ptrpar valeur = donner l'objet à l'appelant (pas besoin demoveaureturn). - Un attribut
unique_ptrrend la classe non copiable par défaut : il faut écrire soi-même le constructeur de copie (module 4) pour dupliquer l'objet possédé. - Taille et vitesse d'un pointeur brut : à utiliser systématiquement à la place de
new/deletepour une composition par pointeur.
shared_ptr : propriété partagée
- Chaque copie incrémente un compteur de références partagé ; chaque destruction ou
resetle décrémente. Quand il tombe à 0, l'objet est détruit. Il n'y a pas de « premier » propriétaire : le dernier à partir éteint la lumière. - Copier un
shared_ptrpartage l'objet (pas de copie de l'objet). C'est le mécanisme des pièces partagées du TP2 : modifier la charge d'un moteur est visible depuis toutes les configurations qui l'utilisent. - Un
vector<shared_ptr<Moteur>>détruit automatiquement les moteurs que plus personne ne partage. std::make_shared<T>(args)alloue l'objet et le compteur d'un coup. Préférable àshared_ptr<T>(new T(...)).weak_ptr: observe un objet partagé sans le posséder (ne compte pas) ; sert à casser les cycles (deux objets qui se pointent mutuellement ne seraient jamais détruits).
- 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 leshared_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 pourgetBatterie()). - Oublier
std::movepour ununique_ptr: erreur de compilation « call to deleted constructor ».
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
- Composition : « possède », durée de vie liée, un seul propriétaire, c'est le contenant qui détruit. Agrégation : « utilise », durée de vie indépendante, l'objet peut être partagé, c'est l'extérieur qui détruit.
- Indice décisif : où est le
new? Attribut objet ouunique_ptr= composition ; pointeur ou référence reçu de l'extérieur = agrégation ;shared_ptr= partage. unique_ptr: non copiable,std::movepour transférer,make_unique,get()pour observer,reset()pour détruire.shared_ptr: copie = partage,use_count(), destruction au dernier propriétaire,make_shared. Jamais deux pointeurs intelligents à partir du même pointeur brut.- Une référence attribut s'initialise obligatoirement dans la liste d'initialisation et ne change jamais de cible.
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
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
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
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_ptrne libère jamais l'objetunique_ptrest plus lentunique_ptrne peut pointer qu'un seul typeunique_ptra un seul propriétaire (non copiable) ;shared_ptrcompte ses propriétaires et détruit l'objet quand le dernier disparaît
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é
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
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
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,
batterievaut nullptr et le robot possède l'objet
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
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);
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
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
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
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
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
Exercices à développement
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*lutindedans).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çoitlicornepar valeur, donclicorne_référencerait un paramètre détruit à la fin du constructeur : erreur de conception. - Version 2 : les deux attributs sont alloués avec
newdans 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 sansnew→ agrégation par pointeur.licorne_est un objet → composition (copie du paramètre).
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.
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 :
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.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
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).
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
deletesurget(). - c) Le unique_ptr possède l'adresse d'une variable locale : à la destruction du robot,
deletesur la pile, plantage. Il fautmake_unique. - d) Après
std::move(b),best nul :b->getNom()déréférence nullptr. Utiliserr.getBatterie()->getNom().
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.