← C++, End to End

C++, End to End

Object Relationships

Classes rarely exist in isolation. How one class relates to another (does it own it, share it, or just know about it briefly) is a design decision with real consequences for lifetime management and coupling. C++ doesn't enforce these distinctions in the language; they're patterns you choose.

Composition: strict ownership, matching lifetime

Composition means an object owns another object outright: the contained object is created when the container is, destroyed when the container is, and has no independent existence. A Car has an Engine; when the Car is destroyed, its Engine goes with it. The clearest signal in code: the member is a value (or a unique_ptr), not a pointer or reference to something that might outlive or be shared.

c++ · live, editable, runnableOpen in Compiler Explorer ↗

Aggregation: a looser, shared or independent lifetime

Aggregation means one object has-a reference to another, but the two don't share a lifetime. A Department has Employees, but an Employee exists independently of any one Department (they can transfer, or the object modeling the department can be deleted without deleting the person). The signal in code: the member is a pointer or reference to something owned elsewhere, often a shared_ptr if genuinely shared, or a raw observing pointer if the aggregating object doesn't own it.

c++ · live, editable, runnableOpen in Compiler Explorer ↗

Association: the loosest relationship

Association is even looser than aggregation: two objects just know about or use each other temporarily, with no ownership implication either direction. A Person uses a Doctor for an appointment; neither owns the other, and the relationship might not even be permanently stored, just a parameter passed for the duration of one call.

Has-a versus is-a

Composition and aggregation are both "has-a" relationships. Inheritance (a full chapter on its own) models "is-a": a Circle is-a Shape. Mixing these up is one of the most common real design mistakes in object-oriented code. If you catch yourself inheriting from a class purely to reuse its code, and the derived class doesn't actually behave like a specialized version of the base in every context, that's a has-a relationship wearing an is-a costume, and composition would be the more honest design.

A quick sanity check: can you truthfully finish the sentence "a [Derived] is a [Base]" and have it hold in every situation the code will actually be used? If it only sort of holds, or holds with exceptions, you probably want composition instead.

Why composition is usually the safer default

Inheritance creates a tight coupling: a change to the base class can silently break every derived class, and the is-a contract has to hold in all circumstances or callers relying on polymorphism get surprised. Composition is more flexible: you can swap out the contained object, change its type, or refactor the relationship without touching an entire class hierarchy. "Prefer composition over inheritance" is a genuinely useful default, not just a slogan; reach for inheritance specifically when you need runtime polymorphism (treating different derived types uniformly through a base pointer/reference), not just to avoid retyping a few member functions.

Try it yourself: composition vs a bad inheritance shortcut

Model a Playlist that contains Songs using composition (a std::vector<Song> member), and compare the design mentally to what it would look like if you (wrongly) made Playlist inherit from a hypothetical std::vector<Song>. Notice how composition lets you control exactly what operations the Playlist class exposes, while inheriting from a container would expose every container operation whether it made sense for a playlist or not.

c++ · live, editable, runnableOpen in Compiler Explorer ↗