Nella programmazione in C++, spesso si utilizzano due modi per unire oggetti: aggregazione e composizione. Queste concetti riflettono diverse relazioni tra le classi e influenzano il ciclo di vita e la responsabilità per la distruzione degli oggetti correlati.
Storia della Questione:
Nella progettazione orientata agli oggetti, è sempre stato importante separare le dipendenze tra gli oggetti. Con l'avvento dei linguaggi orientati agli oggetti (Smalltalk, C++, Java), sorge la questione di come modellare al meglio le relazioni "parte - tutto". In C++ questo è diventato particolarmente rilevante a causa della gestione manuale della memoria e del ciclo di vita degli oggetti.
Problema:
Una scelta errata tra aggregazione e composizione porta a perdite di memoria, duplicazione delle risorse o errori di distruzione degli oggetti. Inoltre, questi concetti vengono spesso confusi.
Soluzione:
Esempio di codice:
// Composizione: class Engine {}; class Car { Engine engine; // Engine è creato e distrutto insieme a Car }; // Aggregazione: class Person {}; class Team { std::vector<Person*> members; // Punta a oggetti Person, non li possiede };
Caratteristiche chiave:
Se in un membro-classe c'è un puntatore a un oggetto, è sempre aggregazione?
No! Se la classe possiede questo puntatore (ad esempio, tramite std::unique_ptr), è comunque composizione. Il tipo di legame è definito non dal tipo del campo, ma dalla responsabilità per il ciclo di vita.
class House { std::unique_ptr<Room> room; // composizione, House possiede Room };
Può la composizione essere implementata tramite un riferimento o raw pointer?
Può — ma solo se l'oggetto è creato e distrutto dal proprietario, e il riferimento o il puntatore è utilizzato per ottimizzazione. Tuttavia, è molto meglio utilizzare oggetti per valore o smart pointers per esprimere esplicitamente la proprietà.
Cosa succede se, nella composizione, l'oggetto-parte è creato al di fuori del proprietario e gli viene passato?
In tal caso si corre il rischio di violare gli invarianti della composizione: se l'oggetto creato esternamente è passato al proprietario e questo lo distrugge, mentre altrove esiste ancora un riferimento — si avrà un dangling pointer. È necessario definire rigorosamente i diritti di proprietà e la responsabilità per la distruzione nel progetto.
Un team ha deciso di memorizzare tutti gli oggetti nidificati tramite puntatori raw in un contenitore e distruggerli manualmente nel distruttore. Tutto ha funzionato finché non hanno cambiato lo schema di proprietà. Di conseguenza, il puntatore è stato liberato due volte, causando un crash.
Pro:
Contro:
Un altro team ha adottato std::unique_ptr per tutte le relazioni realmente proprietarie e ha utilizzato non-owning solo sotto forma di riferimenti temporanei. Questo ha espresso chiaramente l'architettura.
Pro:
Contro: