En programmation C++, on utilise souvent deux façons de combiner des objets : l'agrégation et la composition. Ces concepts reflètent différentes relations entre les classes et influencent le cycle de vie et la responsabilité de la destruction des objets associés.
Historique de la question :
Dans la conception orientée objet, il a toujours été important de séparer les dépendances entre les objets. Avec l'avènement des langages objets (Smalltalk, C++, Java), la question s'est posée : comment mieux modéliser les relations "partie – tout". En C++, cela est devenu particulièrement pertinent en raison de la gestion manuelle de la mémoire et du cycle de vie des objets.
Problème :
Un choix erroné entre agrégation et composition entraîne soit des fuites de mémoire, soit des duplications de ressources, soit des erreurs dans la destruction des objets. Ces concepts sont également souvent confondus.
Solution :
Exemple de code :
// Composition : class Engine {}; class Car { Engine engine; // Engine est créé et détruit avec Car }; // Agrégation : class Person {}; class Team { std::vector<Person*> members; // Pointeurs vers des objets Person, ne les possède pas };
Caractéristiques principales :
Si un membre de classe contient un pointeur vers un objet, est-ce toujours de l'agrégation ?
Non ! Si la classe possède ce pointeur (par exemple, via std::unique_ptr), c'est toujours de la composition. Le type de relation est déterminé non pas par le type du champ, mais par la responsabilité du cycle de vie.
class House { std::unique_ptr<Room> room; // composition, House possède Room };
La composition peut-elle être réalisée par référence ou pointeur brut ?
Oui — mais seulement si l'objet est créé et détruit par le propriétaire, et que la référence ou le pointeur est utilisé pour l'optimisation. Cependant, il est beaucoup mieux d'utiliser des objets par valeur ou des pointeurs intelligents pour exprimer explicitement la possession.
Que se passe-t-il si dans une composition, un objet-part est créé en dehors du propriétaire et lui est transmis ?
Dans ce cas, le risque de violation des invariants de la composition apparaît : si un objet créé extérieurement est transmis au propriétaire, et que celui-ci le détruit, alors que quelque part d'autre il reste une référence — un pointeur suspendu apparaîtra. Il est essentiel de définir strictement les droits de propriété et la responsabilité de destruction dans le projet.
Une équipe a décidé de conserver tous les objets imbriqués via des pointeurs bruts dans un conteneur et de les détruire manuellement dans le destructeur. Tout fonctionnait, jusqu'à ce que le schéma de possession change. En conséquence, le pointeur a été libéré deux fois, provoquant un crash.
Avantages :
Inconvénients :
Une autre équipe est passée à std::unique_ptr pour toutes les liaisons réellement possédantes, et a utilisé des références non possédantes uniquement sous forme de références temporaires. Cela a clairement exprimé l'architecture.
Avantages :
Inconvénients :