ProgrammationArchitecte logiciel C++

Qu'est-ce que l'agrégation (aggregation) et la composition (composition) en C++ ? Comment diffèrent-elles et quand utiliser quelle approche ?

Réussissez les entretiens avec l'assistant IA Hintsage

Réponse.

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 :

  • Composition — c'est une relation où un objet possède un objet-partie et est responsable de sa création/destruction. En C++, cela s'exprime généralement par un membre de classe par valeur ou via unique_ptr.
  • Agrégation — c'est un lien plus faible, l'objet-partie existe en dehors du "tout", et la responsabilité de son cycle de vie ne repose pas sur le propriétaire. Cela se réalise généralement par un lien non possédant (pointeur/référence).

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 :

  • Composition — lien fort (part-of), possède
  • Agrégation — lien faible (utilise), ne possède pas
  • La composition automatise la gestion de la mémoire, l'agrégation nécessite prudence et accords sur les propriétaires

Questions piégées.

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.

Erreurs courantes et antipatterns

  • Mélanger les concepts d'agrégation et de composition (par exemple, conserver des pointeurs bruts inutiles, mais essayer de les détruire dans le destructeur du propriétaire)
  • Utiliser l'agrégation là où un cycle de vie strict est nécessaire (par exemple, les détails d'un objet complexe)
  • Ne pas libérer des pointeurs non possédants

Exemple de la vie réelle

Cas négatif

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 :

  • Flexibilité de l'architecture pour certains cas (par exemple, relations flottantes entre les objets)

Inconvénients :

  • Haut risque d'erreurs de gestion de la mémoire
  • Difficile à maintenir

Cas positif

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 :

  • Relations de possession transparentes et compréhensibles
  • Pas de fuites ni de double libérations

Inconvénients :

  • La composition cyclique n'est pas toujours possible
  • Parfois, des protocoles de communication entre objets doivent être retravaillés