ProgrammingC++ Software Architect

What are aggregation and composition in C++? How do they differ from each other and when to use which approach?

Pass interviews with Hintsage AI assistant

Answer.

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:

  • Composition is a relationship where an object owns a part-object and is responsible for its creation/destruction. In C++, this is usually expressed through a member class by value or via unique_ptr.
  • Aggregation is a weaker relationship; the part-object exists outside of the "whole", and the responsibility for its lifecycle does not lie with the owner. It is usually implemented via a non-owning reference (pointer/reference).

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:

  • Composition — strong relationship (part-of), owns
  • Aggregation — weak relationship (uses), does not own
  • Composition automates memory management, aggregation requires caution and agreements on ownership

Tricky Questions.

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.

Typical Errors and Anti-patterns

  • Mixing the concepts of aggregation and composition (e.g., storing unnecessary raw pointers but trying to destroy them in the owner's destructor)
  • Using aggregation where strict lifecycle is needed (e.g., details of a complex object)
  • Not releasing non-owning pointers

Real-life Example

Negative Case

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:

  • Flexibility of architecture for some scenarios (e.g., floating relationships between objects)

Cons:

  • High risk of memory management errors
  • Difficult to maintain

Positive Case

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:

  • Transparent and understandable ownership relationships
  • No leaks or double frees

Cons:

  • Cyclic composition is not always possible
  • Sometimes it requires refining communication protocols between objects