In C++ programming, two ways to combine objects are often used: aggregation and composition. These concepts reflect different relationships between classes and impact the lifecycle and responsibility for the destruction of related objects.
Background:
In object-oriented design, it has always been important to separate dependencies between objects. With the advent of object-oriented languages (Smalltalk, C++, Java), the question arose: how to best model "part-whole" relationships. In C++, this became particularly relevant due to manual memory management and the lifecycle of objects.
Problem:
A wrong choice between aggregation and composition leads to memory leaks, resource duplication, or destruction errors of objects. These concepts are often confused.
Solution:
Code example:
// Composition: class Engine {}; class Car { Engine engine; // Engine is created and destroyed with Car }; // Aggregation: class Person {}; class Team { std::vector<Person*> members; // Points to Person objects, does not own them };
Key features:
If a member class holds a pointer to an object, is it always aggregation?
No! If the class owns this pointer (for example, via std::unique_ptr), it is still composition. The type of relationship is determined not by the type of field, but by the responsibility for the lifecycle.
class House { std::unique_ptr<Room> room; // composition, House owns Room };
Can composition be implemented via a reference or raw pointer?
It can — but only if the object is created and destroyed by the owner, and the reference or pointer is used for optimization. However, it is much better to use objects by value or smart pointers to clearly express ownership.
What happens if in composition the part-object is created outside the owner and passed to it?
In that case, there is a risk of violating composition invariants: if an externally created object is passed to the owner and the owner destroys it, while somewhere else there is a reference left — a dangling pointer will appear. Ownership rights and responsibilities for destruction need to be strictly defined in the project.
One team decided to store all nested objects via raw pointers in a container and manually destroy them in the destructor. Everything worked until they changed the ownership scheme. As a result, the pointer was freed twice, leading to a crash.
Pros:
Cons:
Another team switched to std::unique_ptr for all truly owning relationships, and non-owning was used only as temporary references. This clearly expressed the architecture.
Pros:
Cons: