In C++ programmeer je vaak met twee manieren om objecten te combineren: aggregatie en compositie. Deze concepten weerspiegelen verschillende relaties tussen klassen en beïnvloeden de levenscyclus en de verantwoordelijkheid voor het vernietigen van gerelateerde objecten.
Geschiedenis van de vraag:
In objectgeoriënteerd ontwerp was het altijd belangrijk om de afhankelijkheden tussen objecten te scheiden. Met de opkomst van objectgeoriënteerde talen (Smalltalk, C++, Java) werd de vraag gesteld: hoe modelleer je het beste de relaties "deel - geheel"? In C++ werd dit bijzonder relevant vanwege het handmatige geheugensbeheer en de levenscyclus van objecten.
Probleem:
Een foutieve keuze tussen aggregatie en compositie leidt tot geheugenlekken, duplicatie van middelen of fouten bij het vernietigen van objecten. Bovendien worden deze concepten vaak verward.
Oplossing:
Codevoorbeeld:
// Compositie: class Engine {}; class Car { Engine engine; // Engine wordt samen met Car gemaakt en vernietigd }; // Aggregatie: class Person {}; class Team { std::vector<Person*> members; // Wijst naar Person-objecten, heeft geen eigendom van hen };
Belangrijke kenmerken:
Als er een pointer naar een object in de lidklasse ligt, is dat altijd aggregatie?
Nee! Als de klasse deze pointer bezit (bijvoorbeeld via std::unique_ptr), is het nog steeds compositie. Het type verbinding wordt niet bepaald door het type veld, maar door de verantwoordelijkheid voor de levenscyclus.
class House { std::unique_ptr<Room> room; // compositie, House is eigenaar van Room };
Kan compositie worden geïmplementeerd via een referentie of raw pointer?
Ja — maar alleen als het object wordt gemaakt en vernietigd door de eigenaar, en de referentie of pointer wordt gebruikt voor optimalisatie. Het is echter veel beter om objecten naar waarde of smart pointers te gebruiken voor een duidelijke uitdrukking van eigendom.
Wat gebeurt er als in de compositie het deel-object buiten de eigenaar wordt aangemaakt en aan hem wordt doorgegeven?
In dat geval bestaat het risico dat de invarianties van compositie worden geschonden: als een extern aangemaakt object aan de eigenaar is doorgegeven en deze het vernietigt, terwijl er elders nog een referentie blijft, ontstaat er een dangling pointer. Het is essentieel om eigendomsrechten en verantwoordelijkheden voor vernietiging duidelijk te definiëren in het project.
Een team besloot om alle geneste objecten via raw pointers in een container op te slaan en deze handmatig te vernietigen in de destructor. Het werkte goed totdat de eigendomsschema's werden gewijzigd. Het resultaat was dat de pointer twee keer werd vrijgegeven, wat leidde tot een crash.
Voordelen:
Nadelen:
Een ander team schakelde over op std::unique_ptr voor alle echte eigendomsrelaties, en gebruikte niet-eigendoms alleen in de vorm van tijdelijke referenties. Dit maakte de architectuur duidelijker.
Voordelen:
Nadelen: