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.

Key Concept
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.

Best Practice
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.

Important
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.

Key Concept
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 with get()
  • 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::vector cannot store plain references, because its elements must be assignable. std::reference_wrapper from <functional> is the copyable stand-in, and get() 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.