Modeling Has-A Relationships
Reference objects that exist independently of the container.
What is Aggregation?
Aggregation is the subtype of object composition in which the whole holds on to parts it did not create and will not destroy. The part is reachable through the whole, but the part's lifetime is somebody else's responsibility.
That one sentence carries the entire distinction. Composition, from the previous lesson, welds the part's lifetime to the whole: build the whole and the part appears, destroy the whole and the part goes with it. Aggregation cuts that weld. The part is built somewhere else, handed to the whole, and typically outlives it.
Both subtypes model "has-a" relationships. A playlist has-a track. A bus route has-a stop. What separates them is not the shape of the diagram, it is who runs the constructor and who runs the destructor.
Two everyday examples make the split concrete:
- A track and the playlists it appears on. The same recording can sit in your morning list and a friend's workout list at the same time. It existed before either list was made and it survives both being deleted. The playlists know which tracks they contain; the track has no idea it is on a list at all.
- A bus stop and the routes that serve it. Several routes call at the same stop. The stop was there before the route was drawn and stays there after the route is discontinued. The timetable knows its stops; the stop knows nothing about timetables.
An outside force can of course destroy an aggregated part while the whole is still running. The claim is narrower than that: the whole itself never does the destroying.
Four Questions That Separate the Two Subtypes
Run any candidate relationship through these four questions. Only the middle two answers change between the subtypes, and both of them are about ownership.
| Question | Composition | Aggregation |
|---|---|---|
| Is the part contained within the whole? | Yes | Yes |
| Can the part belong to several wholes at the same time? | No | Yes |
| Does the whole manage the part's existence? | Yes | No |
| Does the part know the whole exists? | No | No |
Both subtypes are unidirectional: knowledge flows from whole to part and never back. Both allow one part or many parts. When a relationship answers "yes, yes, no, no" down that column, you are looking at an aggregation.
What the Member Declaration Looks Like
The two subtypes are implemented so similarly that a reader has to look at the member types to tell them apart. This is where the difference actually shows up in code.
| Composition | Aggregation | |
|---|---|---|
| Typical member | a value member such as Track m_opener{};, or a pointer the class allocates and frees itself |
a reference or pointer to an object built elsewhere, such as const Track& m_opener; |
| Who builds the part | the whole, during its own construction | the caller, before the whole exists |
| How the part arrives | through the member initializer list | as a constructor argument, or later through a member function that takes a reference |
| What the whole's destructor does to it | destroys the part | nothing; the reference or pointer member simply ceases to exist |
Because an aggregate's member is a handle rather than the object itself, destroying the whole destroys only the handle. The object on the other end is untouched.
A class with a reference member cannot be copy-assigned, because a reference can never be rebound after initialization. If you need assignable aggregate objects, store a pointer instead of a reference, or use the wrapper introduced later in this lesson.
Watching the Lifetimes Come Apart
Talk about lifetimes is easy to nod along to and hard to believe until you see the destructors fire. This program prints a line every time a Track or a Playlist is built or torn down, and puts the Playlist in an inner scope so it dies first.
#include <iostream>
#include <string>
#include <string_view>
class Track
{
private:
std::string m_title{};
int m_seconds{};
public:
Track(std::string_view title, int seconds)
: m_title{title}, m_seconds{seconds}
{
std::cout << "Track built: " << m_title << '\n';
}
~Track()
{
std::cout << "Track torn down: " << m_title << '\n';
}
const std::string& getTitle() const { return m_title; }
int getSeconds() const { return m_seconds; }
};
class Playlist
{
private:
std::string m_label{};
const Track& m_opener; // aggregated: an object somebody else made
public:
Playlist(std::string_view label, const Track& opener)
: m_label{label}, m_opener{opener}
{
std::cout << "Playlist built: " << m_label << '\n';
}
~Playlist()
{
std::cout << "Playlist torn down: " << m_label << '\n';
}
void printOpener() const
{
std::cout << m_label << " opens with " << m_opener.getTitle()
<< " (" << m_opener.getSeconds() << "s)\n";
}
};
int main()
{
Track harbour{"Harbour Lights", 214};
{
Playlist morning{"Slow Start", harbour};
morning.printOpener();
}
std::cout << "Back in main: " << harbour.getTitle() << " is untouched\n";
return 0;
}
The output:
Track built: Harbour Lights
Playlist built: Slow Start
Slow Start opens with Harbour Lights (214s)
Playlist torn down: Slow Start
Back in main: Harbour Lights is untouched
Track torn down: Harbour Lights
Read the order carefully. harbour is constructed first because it has to exist before it can be passed to a constructor. The Playlist is destroyed at the closing brace of the inner scope, and its destructor runs without touching the Track. Only when main itself ends does the Track destructor run, triggered by harbour going out of scope rather than by anything the Playlist did.
Change m_opener to a value member (Track m_opener;) and the Playlist gets a Track of its own, copy-constructed from harbour. An extra Track torn down: Harbour Lights line then appears immediately after the playlist is torn down, because that copy is a part the Playlist owns. One extra destructor line is the entire difference between the two subtypes.
Why a Container Cannot Hold Plain References
A single reference member handles a playlist with one opener. Real playlists have many tracks, so the obvious next step is a vector of them. This does not compile:
std::vector<const Track&> m_lineup{};
Vector elements have to be assignable and have to have addresses you can take. A reference is neither an object with an address of its own nor something you can point at a new target after it is initialized. Raw pointers would compile, but then the vector could hold a null pointer, and every read would need a check that the design was supposed to make unnecessary.
std::reference_wrapper, declared in the <functional> header, fills that gap. It is a small object that stores the address of whatever you hand it and gives it back through the get() member function. Unlike a reference, it can be copied and reassigned, which is exactly what containers require.
#include <functional>
#include <iostream>
#include <string>
int main()
{
std::string takeOne{"rough mix"};
std::string takeTwo{"studio mix"};
std::reference_wrapper<std::string> slot{takeOne};
std::cout << "slot refers to: " << slot.get() << '\n';
slot = takeTwo; // rebinds; assignment moves a wrapper to a different object
std::cout << "slot refers to: " << slot.get() << '\n';
slot.get() += ", approved";
std::cout << "takeTwo is now: " << takeTwo << '\n';
std::cout << "takeOne is now: " << takeOne << '\n';
return 0;
}
slot refers to: rough mix
slot refers to: studio mix
takeTwo is now: studio mix, approved
takeOne is now: rough mix
Assigning to slot moved the wrapper to a different string rather than overwriting the first one. Assigning through slot.get() wrote to the referenced string itself, which is why takeTwo changed and takeOne did not.
| Detail worth remembering | Why |
|---|---|
It lives in <functional> |
Not in <memory> or <vector>, which is where most people look first |
get() retrieves the referent |
The wrapper is not a pointer, so * and -> are not how you reach through it |
| Never wrap a temporary | A temporary dies at the end of the full expression, leaving the wrapper referring to nothing |
std::reference_wrapper<const T> for read-only |
Drop the const only when the whole is meant to modify the parts |
One Part, Several Wholes
With the wrapper available, a Playlist can hold as many tracks as it likes, and the same Track can appear in more than one Playlist at once. That is the property no composition can offer.
#include <functional> // std::reference_wrapper
#include <iostream>
#include <string>
#include <string_view>
#include <vector>
class Track
{
private:
std::string m_title{};
int m_seconds{};
public:
Track(std::string_view title, int seconds)
: m_title{title}, m_seconds{seconds}
{
}
const std::string& getTitle() const { return m_title; }
int getSeconds() const { return m_seconds; }
};
class Playlist
{
private:
std::string m_label{}; // composed: this Playlist owns its label
std::vector<std::reference_wrapper<const Track>> m_lineup{}; // aggregated: owned elsewhere
public:
Playlist(std::string_view label)
: m_label{label}
{
}
void append(const Track& track)
{
m_lineup.push_back(track);
}
void printLineup() const
{
int total{0};
std::cout << m_label << '\n';
for (const auto& slot : m_lineup)
{
const Track& track{slot.get()};
std::cout << " " << track.getTitle() << '\n';
total += track.getSeconds();
}
std::cout << " total " << total << "s\n";
}
};
int main()
{
Track harbour{"Harbour Lights", 214};
Track copper{"Copper Hour", 187};
Track ninth{"Ninth Street", 243};
Playlist morning{"Slow Start"};
morning.append(harbour);
morning.append(copper);
Playlist evening{"Late Shift"};
evening.append(copper);
evening.append(ninth);
morning.printLineup();
evening.printLineup();
return 0;
}
Slow Start
Harbour Lights
Copper Hour
total 401s
Late Shift
Copper Hour
Ninth Street
total 430s
There is exactly one Copper Hour object in this program, and both playlists refer to it. Neither playlist copied it, neither will free it, and adding it to a second playlist did not remove it from the first.
Notice also that Playlist uses both relationships at once. m_label is a plain std::string member: composed, created with the Playlist, destroyed with it. m_lineup holds handles to objects that come and go on their own schedule. A class is free to mix the two, member by member.
Choosing a Relationship for Your Program
The same pair of real-world things can be modelled either way, and the correct choice depends on your program rather than on the world.
- A streaming player whose library is shared across many playlists wants aggregation. Tracks live in one place; playlists refer to them.
- An offline export bundle that must remain playable after the library is deleted wants composition. The bundle copies the audio it needs and owns those copies outright.
Neither answer is more faithful to reality. They are answers to different questions about lifetime.
Pick whichever relationship your program actually depends on, rather than the one that mirrors the world most faithfully. When no code anywhere relies on a part outliving its whole, composition is the smaller and safer model.
What Aggregation Costs You
Aggregation buys sharing, and pays for it with two problems that composition does not have.
An aggregate does nothing to keep its parts alive. If a part is destroyed while a whole still refers to it, every later access through that reference or pointer is undefined behaviour. Make sure the parts outlive every whole that refers to them, which usually means declaring the parts in an enclosing scope, as the examples above do.
The second problem is the mirror image. Because the aggregate frees nothing, something outside it has to. When the parts were allocated dynamically and the only handles to them lived inside an aggregate that has just been destroyed, that memory is leaked. A composition would have cleaned up on its way out.
For both reasons, prefer composition when the choice is genuinely open, and reach for aggregation when sharing is a requirement rather than a convenience.
If several objects genuinely need to keep a part alive between them, that is shared ownership, and
std::shared_ptr is the tool for it. Aggregation is the case where the whole has no ownership stake at all, so a non-owning handle is the honest way to say so.
Aggregate Class Means Something Else
Two unrelated pieces of C++ vocabulary sit uncomfortably close together here.
Aggregation is the relationship this lesson describes. Aggregate, or aggregate class, is a completely separate language rule: a struct or class with no user-declared or inherited constructors, no private or protected non-static data members, no virtual functions, and no virtual, private, or protected base classes. Satisfying those conditions is what lets you brace-initialize such a type member by member. A plain data-holding struct is an aggregate. The two terms share a root and nothing else.
One more caveat: unlike composition, the definition of aggregation is not standardised across the industry. Other books and modelling tools draw the line in slightly different places. The four questions above are the version used throughout this course.
Looking Forward
Aggregation is the second of several relationship types in this chapter. The next lessons cover association, where two objects are aware of each other but neither is part of the other, and dependencies, where one class merely uses another to get a job done without storing it at all. Container classes then apply these ideas to types whose whole purpose is holding parts.
Key Terminology
- Aggregation: A part-whole relationship where the whole does not manage the part's lifetime and the part may belong to several wholes at once
- Composition: A part-whole relationship where the whole creates and destroys the part
- Whole (parent): The containing object in a part-whole relationship
- Part (child, component): The contained object
- Unidirectional relationship: One where the whole knows about the part but not the reverse
std::reference_wrapper: A copyable, reassignable stand-in for a reference, from<functional>, retrieved withget()- Dangling reference: A reference whose referent has already been destroyed; reading it is undefined behaviour
- Aggregate class: An unrelated term meaning a class simple enough to be brace-initialized member by member
Summary
- Aggregation is object composition without ownership: the whole refers to parts it did not build and will not destroy.
- The four tests are containment (yes), shared membership across wholes (yes), lifetime management by the whole (no), and knowledge of the whole by the part (no).
- Aggregation models a "has-a" relationship, just as composition does. The difference is lifetime, not shape.
- In code, an aggregate stores references or pointers to objects created outside the class, usually accepted as constructor parameters or added later through a member function.
- Destroying the aggregate destroys only the handle. The part carries on until whatever created it destroys it.
std::vectorcannot store plain references, because its elements must be assignable.std::reference_wrapperfrom<functional>is the copyable stand-in, andget()retrieves the referenced object.- One part can be referenced by several wholes at the same time, which is the capability aggregation exists to provide.
- A single class may compose some members and aggregate others.
- Choose the simplest relationship that satisfies the program, not the one that best matches the real world.
- Prefer composition where the choice is open: aggregates leave both dangling references and leaked parts as possibilities that a composition rules out.
- If several objects must genuinely keep a part alive, that is shared ownership and calls for
std::shared_ptr, not aggregation.
How did you find this lesson?
Your rating helps us improve the content.
Create an account to track your progress and access interactive exercises. Already have one? Sign in.
Modeling Has-A Relationships - Quiz
Test your understanding of the lesson.
Practice Exercises
Build a Team with Player Aggregation
Create a Team class that aggregates Player objects using std::reference_wrapper. Players exist independently and can be part of multiple teams, demonstrating the aggregation relationship where the whole doesn't manage the part's lifetime.
Lesson Discussion
Share your thoughts and questions
No comments yet. Be the first to share your thoughts!