What Are Object Relationships?

An object relationship is the connection between two types in a program: which one owns the other, which one merely borrows it, and which one needs it only for the length of a single call. C++ has no owns or borrows keyword, so you never declare a relationship outright. You express it by choosing where the second object is stored, and that storage choice is what the rest of this chapter is about.

That makes relationships unusually concrete in C++. Look at nothing but the data members of a class and you can already recover most of the designer's intent:

class Production
{
    // public interface omitted

private:
    Playbill m_playbill;        // stored by value: dies when the Production dies
    const Performer& m_lead;    // stored by reference: outlives the Production
    const BoxOffice* m_desk{};  // stored by pointer: may not be attached at all
};

Three members, three different lifetime stories. A reader who knows the vocabulary in this chapter can name each one without reading a single member function.

The Vocabulary: Six Relation Words

Designers describe connections with a small set of relation words, and each one has a standard C++ spelling. The table below is the map for this chapter and the two that follow it.

Relation word The question it answers How C++ usually spells it Covered in
"part-of" Does this object come and go with the other? a value member Composition
"has-a" Does the whole hold a part it did not create? a reference or pointer member Aggregation
"uses-a" Do two independent objects need each other over time? a pointer or reference member, or a stored id Association
"depends-on" Is the other object needed for one job and then forgotten? a function parameter or a local variable Dependencies
"member-of" Is this object one of many held in a collection? an element of a container class Container Classes
"is-a" Is this type a kind of the other type? inheritance and virtual functions the next two chapters

Five of these are the subject of this chapter. The sixth, "is-a", is large enough to need chapters of its own, so we set it aside until we reach inheritance and virtual functions.

Why the words matter
The compiler accepts a value member, a reference member, and a pointer member equally happily. Nothing in the language stops you from storing a part by reference when it should have been owned outright. The relation words are how you decide which one is correct before the code is written.

Object Composition: Building a Whole Out of Parts

Object composition is the process of building a complex object out of simpler ones. It is the workhorse relationship of C++ design, and you have been using it since your first struct.

Every time you declare a class with data members, you are composing: a std::string member, an int member and a bool member together form a new type that is more than any of them individually. This is exactly why structs and classes are called composite types. They compose several simpler types into one larger type.

In general terms, object composition models a "has-a" relationship. A playhouse has a marquee. A ticket has a barcode. A production has a running time. The complex object is the whole, and the simpler objects it is built from are its parts.

Object composition splits into two subtypes, and the whole of this chapter's design advice comes down to telling them apart:

  • Composition, where the whole is responsible for its part's existence
  • Aggregation, where the part exists on its own and the whole merely holds it

They look nearly identical in a class diagram and very different in memory. The deciding question is always about lifetime: if the whole is destroyed, does the part go with it?

Subtype One: Composition, Where the Whole Owns the Part

In a composition, the part is created when the whole is created and destroyed when the whole is destroyed. The part belongs to exactly one whole, and it has no idea the whole exists. Because that is precisely the behavior of an ordinary value member, composition needs no special machinery at all.

A theatre building and the illuminated sign over its entrance work this way. The marquee is put up when the building is fitted out, and it comes down when the building does. Nobody moves it to another theatre.

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

class Marquee
{
public:
    explicit Marquee(int bulbs)
        : m_bulbs{ bulbs }
    {
        std::cout << "Marquee wired up with " << m_bulbs << " bulbs\n";
    }

    ~Marquee()
    {
        std::cout << "Marquee taken down\n";
    }

    int getBulbs() const { return m_bulbs; }

private:
    int m_bulbs{};
};

class Playhouse
{
public:
    Playhouse(std::string_view name, int bulbs)
        : m_name{ name }, m_sign{ bulbs }
    {
    }

    void printFrontage() const
    {
        std::cout << m_name << " glows with " << m_sign.getBulbs() << " bulbs\n";
    }

private:
    std::string m_name{};
    Marquee m_sign;
};

int main()
{
    Playhouse riverside{ "Riverside Playhouse", 96 };
    riverside.printFrontage();

    return 0;
}
Marquee wired up with 96 bulbs
Riverside Playhouse glows with 96 bulbs
Marquee taken down

Notice that main never mentions Marquee. The constructor message appears because building a Playhouse builds its sign, and the destructor message appears because riverside reaching the end of main takes the sign with it. That automatic bracketing of lifetimes is the entire point of composition, and it is why "part-of" is the more precise relation word for this subtype: the marquee is not merely something the playhouse has, it is part of the playhouse.

Subtype Two: Aggregation, Where the Part Is Only Borrowed

An aggregation is still a whole and its parts, but the whole does not manage the part's lifetime. The part was created elsewhere, it may be attached to several wholes at once, and it carries on existing after any particular whole is gone. Since a value member would copy and co-own the part, aggregations are stored as references or pointers instead.

A production and its lead performer fit this shape. The performer existed before the production was cast and will be working long after it closes, and there is nothing to stop two productions from booking the same performer.

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

class Performer
{
public:
    explicit Performer(std::string_view name)
        : m_name{ name }
    {
    }

    const std::string& getName() const { return m_name; }

private:
    std::string m_name{};
};

class Production
{
public:
    Production(std::string_view title, const Performer& lead)
        : m_title{ title }, m_lead{ lead }
    {
    }

    void printCallSheet() const
    {
        std::cout << m_title << " leads with " << m_lead.getName() << '\n';
    }

private:
    std::string m_title{};
    const Performer& m_lead;
};

int main()
{
    Performer marlowe{ "Ada Marlowe" };

    {
        Production springRun{ "The Long Interval", marlowe };
        Production autumnRun{ "Nine Winters", marlowe };

        springRun.printCallSheet();
        autumnRun.printCallSheet();
    }

    std::cout << marlowe.getName() << " is still on the books\n";

    return 0;
}
The Long Interval leads with Ada Marlowe
Nine Winters leads with Ada Marlowe
Ada Marlowe is still on the books

The inner block ends, both productions are destroyed, and marlowe is untouched. One performer served two wholes at the same time and outlived both, which is exactly what a value member could not have done.

Warning
Borrowing cuts the other way too. If marlowe had been declared inside the inner block and a production had been declared outside it, the production would be left holding a reference to a destroyed object. Using it afterwards is undefined behavior. Aggregation buys you sharing at the cost of having to guarantee that the part outlives every whole pointing at it.

When There Is No Whole and No Part

Composition and aggregation both assume a part-whole shape. Plenty of connections do not have one, and forcing them into it produces strange designs.

An association joins two objects that are simply peers. Neither is part of the other, neither owns the other, and the connection may run in one direction or both. A box office and a production know about each other for as long as the show is running, but a box office is not made of productions.

A dependency is weaker still. One object reaches for another to finish a single task and then forgets about it. The dependency shows up as a parameter or a local variable rather than as a data member, which is the cleanest signal there is: nothing is stored, so nothing is being kept.

Finally, "member-of" describes an element inside a collection rather than a designed part of a whole. A single ticket is a member of tonight's sales, not a component of them. That is the job of container classes, which we come to later in the chapter.

Reading a Class as a Relationship Map

Put all four kinds together and one class declaration tells the whole story. The program below wires up a production with a playbill it owns, a performer it borrows, a box office it collaborates with, and a lighting rig it uses once and never stores.

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

class Playbill
{
public:
    explicit Playbill(int pages)
        : m_pages{ pages }
    {
    }

    int getPages() const { return m_pages; }

private:
    int m_pages{};
};

class Performer
{
public:
    explicit Performer(std::string_view name)
        : m_name{ name }
    {
    }

    const std::string& getName() const { return m_name; }

private:
    std::string m_name{};
};

class BoxOffice
{
public:
    explicit BoxOffice(std::string_view venue)
        : m_venue{ venue }
    {
    }

    void reportSales(int tickets) const
    {
        std::cout << m_venue << " reports " << tickets << " tickets sold\n";
    }

private:
    std::string m_venue{};
};

class LightingRig
{
public:
    explicit LightingRig(int channels)
        : m_channels{ channels }
    {
    }

    void blackout() const
    {
        std::cout << "All " << m_channels << " lighting channels down\n";
    }

private:
    int m_channels{};
};

class Production
{
public:
    Production(std::string_view title, const Performer& lead, const BoxOffice* desk)
        : m_title{ title }, m_playbill{ 8 }, m_lead{ lead }, m_desk{ desk }
    {
    }

    void curtainDown(const LightingRig& rig) const
    {
        std::cout << m_title << " starring " << m_lead.getName()
                  << ", " << m_playbill.getPages() << "-page playbill\n";
        rig.blackout();
        m_desk->reportSales(389);
    }

private:
    std::string m_title{};
    Playbill m_playbill;
    const Performer& m_lead;
    const BoxOffice* m_desk{};
};

int main()
{
    Performer marlowe{ "Ada Marlowe" };
    BoxOffice riversideDesk{ "Riverside Playhouse" };
    LightingRig rig{ 24 };

    Production springRun{ "The Long Interval", marlowe, &riversideDesk };
    springRun.curtainDown(rig);

    return 0;
}
The Long Interval starring Ada Marlowe, 8-page playbill
All 24 lighting channels down
Riverside Playhouse reports 389 tickets sold

Four declarations, four relationships:

Declaration in Production Relationship What it commits you to
Playbill m_playbill; composition, "part-of" the playbill is printed and pulped with the run
const Performer& m_lead; aggregation, "has-a" someone else must keep the performer alive
const BoxOffice* m_desk{}; association, "uses-a" the link is optional and can be repointed
curtainDown(const LightingRig&) dependency, "depends-on" the rig is borrowed for one call only
Best Practice
Let the lifetime answer pick the mechanism rather than habit. If the part genuinely comes and goes with the whole, a value member is both the simplest thing to write and the safest, because nothing can dangle. If it does not, step down to a reference, a pointer, or a plain parameter, and accept that the design now owes a guarantee the compiler will not check for you.

Why Assembling Beats Growing One Big Class

It is always possible to skip composition and pour every field into a single class. Production could have carried a page count, a performer name, a venue name and a channel count directly, with no Playbill, Performer, BoxOffice or LightingRig in sight. Composition wins anyway, for three reasons.

Each class ends up with one job. LightingRig worries about lighting and nothing else, so it is short enough to read in one sitting and short enough to get right. A class that stores four unrelated groups of fields has four reasons to change.

Parts are reusable, so they get tested once. Performer was written for productions but nothing in it mentions productions, so a casting tool or a payroll report can reuse it unchanged. Every reuse is code you have already written, already tested and already verified, rather than code you write again slightly differently.

The whole becomes a coordinator. Production::curtainDown does not know how many bulbs a marquee has or how a venue records a sale. It asks its parts to do their own work and arranges the order. Delegating like that keeps the outer class thin even as the system grows.

The same idea, one level up
This is why the standard library is built the way it is. std::string does not reimplement dynamic memory, and your class does not reimplement string handling. Each layer composes the layer beneath it, and every layer above inherits work that is already correct.

Summary

A relationship is a storage decision. C++ has no syntax for ownership, so a value member, a reference member, a pointer member and a plain parameter are how designs express "owns", "borrows", "collaborates with" and "uses once".

Object composition is the process of building a complex object out of simpler ones, and it models a "has-a" relationship. Structs and classes are called composite types for exactly this reason: they bundle data members of assorted types into one new type.

Composition has two subtypes. In composition, the whole creates and destroys the part, the part belongs to one whole, and a value member says so. In aggregation, the part is created elsewhere, may be shared, and outlives the whole, so a reference or pointer member says so.

Not every connection is a part-whole one. Association links independent peers that use each other over time. Dependency is a one-off use that is never stored. Member-of describes an element inside a container class.

The relation words in this chapter are "part-of", "has-a", "uses-a", "depends-on" and "member-of". The remaining one, "is-a", is covered later through inheritance and virtual functions.

Getting these choices right is what makes a design hold up. Model a part as owned when it is shared and you get needless copies; model it as borrowed when it is owned and you get dangling references. Ask the lifetime question first, and the C++ spelling follows from the answer.