What Is Object Composition?

Object composition is the practice of assembling a complicated type out of simpler types by making those simpler types its data members. The assembled type is called the whole (or parent), and each simpler type it contains is a part (or child, or component).

You have been doing this since the first struct you wrote. A type with an int member and a std::string member is already a complex object built from two simpler ones. Structs and classes carry the standard label composite type on exactly this basis: composing is the whole job.

Spoken aloud, the pattern is always "has-a": an espresso machine has-a boiler, a paperback has-a spine, a route has-a distance. Read in the other direction, the part is described as being part-of the whole, which is the more precise phrasing and the one worth remembering, because it rules out things a loose "has-a" would let through. A boiler is part-of an espresso machine. A page is part-of a book.

The payoff is not decorative. Each part is a small type that can be written once, tested once, and then reused everywhere without being re-reasoned about. The whole spends its code coordinating parts rather than reimplementing what they already do, so it stays short, and short types have fewer places for bugs to hide.

Two Subtypes, One Idea

Object composition splits into two subtypes that differ on exactly one question: who is responsible for the part's lifetime?

Composition Aggregation
Who creates the part the whole somebody else, before the whole exists
Who destroys the part the whole somebody else, after the whole is gone
Can one part serve several wholes at once no yes
Typical member a value member a reference or pointer to an outside object

This lesson covers composition. The next lesson covers aggregation.

The vocabulary here is unfortunate, because "composition" names both the umbrella and one of the two things under it. This course writes object composition when it means the umbrella and plain composition when it means the lifetime-owning subtype below.

Four Conditions a Composition Must Meet

A relationship qualifies as a composition only when all four of these hold:

# Condition What it rules out
1 The part is contained within the whole Two unrelated objects that merely talk to each other
2 The part belongs to exactly one whole at a time A part shared between two wholes simultaneously
3 The whole manages the part's existence A part the caller has to create and clean up by hand
4 The part does not know the whole exists A part that reaches back up to its owner

Take a boiler inside an espresso machine. The boiler sits inside the machine, so condition 1 holds. One boiler cannot be plumbed into two machines at the same time, so condition 2 holds. Assembling the machine installs the boiler and scrapping the machine takes it out with it, so condition 3 holds. The boiler heats water without any notion of what it is bolted into, so condition 4 holds. Four for four, so this is a composition.

Condition 4 is what makes composition a unidirectional relationship: knowledge runs from whole to part and never back. The machine can ask the boiler its temperature. The boiler cannot ask the machine anything, because it has no idea a machine is there.

Key Concept
Composition is a claim about lifetime and containment, not about physical objects. A std::string member inside a class is a composition every bit as much as a boiler inside a machine.

The Smallest Composition Is a Data Member

Compositions are the easiest relationship in this chapter to write, because C++ gives you one for free every time you declare a normal data member. A member declared by value lives inside its enclosing object, so its lifetime is welded to the enclosing object's without you writing a line to arrange it.

class Dose
{
private:
    int m_grams{};
    int m_clicks{};

public:
    Dose(int grams, int clicks)
        : m_grams{grams}, m_clicks{clicks}
    {
    }

    int getGrams() const { return m_grams; }
    int getClicks() const { return m_clicks; }
};

Run the two integers through the four conditions. They live inside Dose, they cannot simultaneously belong to a second Dose, they come into being when a Dose is constructed and cease to exist when it is destroyed, and neither of them knows it is part of anything. Dose composes its members, and the entire implementation of that fact is the two member declarations.

When a part has to be allocated at runtime, a pointer member can hold it instead of a value member. That is still a composition, but only if the class itself does every allocation and every matching deallocation. The moment the caller has to remember to free something, condition 3 has been broken and you no longer have a composition.

Best Practice
If a class can be designed with composition, design it with composition. Compositions are the simplest relationship to reason about and the only one that cleans up after itself with no help.

Watching a Whole Build and Unbuild Its Parts

Condition 3 is easy to accept in the abstract and much more convincing when you watch the destructors fire. The program below prints a line from every constructor and every destructor, and puts the machine in an inner scope so it dies before main ends.

#include <iostream>
#include <string>
#include <string_view>

class Boiler
{
private:
    int m_celsius{};

public:
    explicit Boiler(int celsius)
        : m_celsius{celsius}
    {
        std::cout << "  Boiler filled to " << m_celsius << "C\n";
    }

    ~Boiler()
    {
        std::cout << "  Boiler drained\n";
    }

    int getCelsius() const { return m_celsius; }
};

class Grinder
{
private:
    int m_clicks{};

public:
    explicit Grinder(int clicks)
        : m_clicks{clicks}
    {
        std::cout << "  Grinder set to click " << m_clicks << '\n';
    }

    ~Grinder()
    {
        std::cout << "  Grinder emptied\n";
    }

    int getClicks() const { return m_clicks; }
};

class EspressoMachine
{
private:
    std::string m_serial{};
    Boiler m_boiler;
    Grinder m_grinder;

public:
    EspressoMachine(std::string_view serial, int celsius, int clicks)
        : m_serial{serial}, m_boiler{celsius}, m_grinder{clicks}
    {
        std::cout << "Machine " << m_serial << " assembled\n";
    }

    ~EspressoMachine()
    {
        std::cout << "Machine " << m_serial << " scrapped\n";
    }
};

int main()
{
    std::cout << "Workshop opens\n";

    {
        EspressoMachine machine{"LM-402", 93, 14};
    }

    std::cout << "Workshop closes\n";

    return 0;
}
Workshop opens
  Boiler filled to 93C
  Grinder set to click 14
Machine LM-402 assembled
Machine LM-402 scrapped
  Grinder emptied
  Boiler drained
Workshop closes

Three things in that ordering are worth naming.

  • Parts are built first. Every member is fully constructed before the whole's constructor body starts running, which is what lets that body use its parts immediately.
  • Members are built in declaration order. m_boiler prints before m_grinder because it is declared first, not because it appears first in the member initializer list. Reordering the initializer list would change nothing; reordering the declarations would.
  • Destruction is the exact reverse. The whole's destructor body runs first, then the members are destroyed last-declared-first.

Nothing in main created the boiler and nothing in main destroyed it. That is condition 3 doing its work. The label the literature attaches is blunt: a death relationship, since the parts have no way to outlive the whole and no say in the matter.

Transfer Does Not Break the Relationship

Condition 2 says one whole at a time. It does not say the part is stuck where it started. A boiler can be pulled out of one machine and fitted into another; move semantics does the same thing to a std::string member every time you move a class that owns one. After the transfer the part is owned by its new whole, belongs to that whole alone, and will be destroyed by it. Four conditions, still satisfied.

The word that matters in condition 2 is simultaneously. Composition forbids a part serving two wholes at the same moment, and says nothing at all about serving a different whole later.

Compositions That Bend the Timing

Most compositions build every part during construction and destroy every part during destruction. A few loosen that timing without ceasing to be compositions, because condition 3 is about who is responsible, not about when the work happens.

Variation Example Still a composition?
The part is created lazily, only once something needs it A string type that allocates no character buffer until data is assigned Yes, the class still allocates it
The part is copied in from something the caller supplied A constructor taking a value and storing its own copy as a member Yes, the stored copy is the class's own
Destruction is handed off to another mechanism A part registered with a cleanup routine that the class arranges Yes, the class arranged the handoff
The caller must delete the part afterwards A class handing back a raw owning pointer and washing its hands of it No, condition 3 is broken

The line runs through the caller. If somebody using your class has to remember to do anything about a part's lifetime, it is not a composition any more.

Delegating Work to the Parts

Here is the question that separates composition from merely bundling data together: why write a Boiler class at all, instead of putting an int m_celsius straight into EspressoMachine?

The single integer is less code today. It is also a decision that does not scale, and it costs three things worth having.

  1. Focus. Boiler worries about temperature and nothing else, so it is small enough to hold in your head and small enough to test exhaustively.
  2. Reuse. A self-contained Boiler drops into a kettle or a steam wand with no edits. An integer buried in EspressoMachine drops into nothing.
  3. Delegation. EspressoMachine does not have to know how a temperature is validated or stored. It asks the boiler, and the boiler already knows.

EspressoMachine.h:

#pragma once

#include <iostream>
#include <string>
#include <string_view>

class Boiler
{
private:
    int m_celsius{};

public:
    explicit Boiler(int celsius)
        : m_celsius{celsius}
    {
    }

    void setCelsius(int celsius) { m_celsius = celsius; }
    int getCelsius() const { return m_celsius; }
};

class Grinder
{
private:
    int m_clicks{};

public:
    explicit Grinder(int clicks)
        : m_clicks{clicks}
    {
    }

    void setClicks(int clicks) { m_clicks = clicks; }
    int getClicks() const { return m_clicks; }
};

class EspressoMachine
{
private:
    std::string m_serial{};
    Boiler m_boiler;
    Grinder m_grinder;

public:
    EspressoMachine(std::string_view serial, int celsius, int clicks)
        : m_serial{serial}, m_boiler{celsius}, m_grinder{clicks}
    {
    }

    void dialIn(int celsius, int clicks)
    {
        m_boiler.setCelsius(celsius);
        m_grinder.setClicks(clicks);
    }

    void printRecipe() const
    {
        std::cout << m_serial << " brews at " << m_boiler.getCelsius()
                  << "C on click " << m_grinder.getClicks() << '\n';
    }
};

main.cpp:

#include "EspressoMachine.h"

int main()
{
    EspressoMachine machine{"LM-402", 93, 14};
    machine.printRecipe();

    machine.dialIn(90, 11);
    machine.printRecipe();

    return 0;
}
LM-402 brews at 93C on click 14
LM-402 brews at 90C on click 11

Look at what dialIn actually contains: two forwarding calls and no logic of its own. EspressoMachine decides what should happen and each part decides how. That division is the reason the outer class stays readable as the program grows, and it generalises into a rule about what any one class should be for.

Best Practice
Build each class to accomplish a single task: either the storage and manipulation of data (like Boiler or std::string), or the coordination of its members (like EspressoMachine). Ideally not both.

One Part or Many

Nothing in the four conditions mentions how many parts there are. A whole can compose one part, or a container of them. Every element of a std::vector member is destroyed when the vector is, and the vector is destroyed when its enclosing object is, so ownership passes cleanly down the chain without any extra work.

#include <iostream>
#include <string>
#include <string_view>
#include <vector>

class Portafilter
{
private:
    std::string m_basket{};

public:
    explicit Portafilter(std::string_view basket)
        : m_basket{basket}
    {
    }

    const std::string& getBasket() const { return m_basket; }
};

class EspressoMachine
{
private:
    std::string m_serial{};
    std::vector<Portafilter> m_filters{};

public:
    explicit EspressoMachine(std::string_view serial)
        : m_serial{serial}
    {
    }

    void fit(std::string_view basket)
    {
        m_filters.emplace_back(basket);
    }

    void printKit() const
    {
        std::cout << m_serial << " ships with " << m_filters.size() << " portafilters\n";

        for (const Portafilter& filter : m_filters)
        {
            std::cout << "  " << filter.getBasket() << '\n';
        }
    }
};

int main()
{
    EspressoMachine machine{"LM-402"};
    machine.fit("single spout");
    machine.fit("double spout");
    machine.fit("bottomless");

    machine.printKit();

    return 0;
}
LM-402 ships with 3 portafilters
  single spout
  double spout
  bottomless

The portafilters are created by the machine, are held by no other machine, and disappear when the machine does. Three parts instead of one changes the count and not the relationship.

Important
A composition owns its parts, so copying the whole copies every part. If your parts are expensive to copy, that cost is real and it is easy to trigger by accident when passing the whole by value. Pass wholes by reference unless you specifically want a copy.

Looking Forward

Composition is the first and strictest of this chapter's relationships. Aggregation comes next and relaxes exactly one thing: the whole stops owning the part, which is what allows one part to be shared between several wholes. Association then drops containment altogether, and dependencies drop even the stored link. Container classes later in the chapter are composition applied at scale, since a container's entire purpose is to own the elements handed to it.

Key Terminology

  • Object composition: Building a complex type out of simpler types held as data members
  • Composition: The subtype of object composition in which the whole creates and destroys its parts
  • Whole (parent): The containing object
  • Part (child, component): The contained object
  • Composite type: A type built from other types, which is what every struct and class is
  • Has-a relationship: The everyday phrasing for containment, as in a machine has-a boiler
  • Part-of relationship: The more precise reading of a composition, from the part's point of view
  • Unidirectional relationship: One where the whole knows about the part and the part knows nothing of the whole
  • Death relationship: A nickname for composition, from the parts being destroyed along with the whole

Summary

  • Object composition builds complex types from simpler ones held as data members, modelling a "has-a" relationship. Composition and aggregation are its two subtypes.
  • A composition requires all four of: the part is contained in the whole, belongs to one whole at a time, has its existence managed by the whole, and knows nothing about the whole.
  • The "part-of" phrasing is more precise than "has-a" and is the one to test a candidate relationship against.
  • Compositions are implemented with ordinary value data members in a struct or class. A pointer member also works, provided the class performs every allocation and deallocation itself.
  • Members are constructed in declaration order before the whole's constructor body runs, and destroyed in reverse order after its destructor body finishes. A part therefore has no way to outlive its whole, which is what the death relationship label records.
  • Parts may be transferred to a new whole. Condition 2 forbids serving two wholes at the same time, not serving a different one later.
  • Deferred creation, copying in a caller-supplied value, and delegated cleanup are all still compositions. Requiring the caller to clean up is not.
  • Giving a part its own class keeps each class focused, makes it reusable elsewhere, and lets the whole delegate rather than reimplement.
  • A whole may compose a single part or a container of them, and the ownership rules are identical either way.