What Is a Dependency?

A dependency is what is left when one type needs a second type only while a single call is running. Nothing is stored, nothing is owned, and once the call returns neither side holds anything to show the two ever met.

That makes it the weakest of the four relationships in this chapter, and the last one left to cover. Composition owns a part. Aggregation refers to a part it does not own. Association keeps a two-way-capable link to something that is not a part at all. A dependency keeps nothing: the collaborator arrives, does its job, and is gone.

Everyday dependencies have the same shape. You depend on a taxi to reach the airport, and once you are there the relationship is over. A courier depends on a lift to reach the ninth floor, and neither the courier nor the lift retains anything about the other afterwards. Dependencies are unidirectional in every case: the caller knows about the thing it uses, and the thing being used has no idea who called it.

Key Concept
Weak does not mean harmless. Your code still breaks if the type you depend on changes its interface. What makes a dependency weak is its lifetime, not its consequences.

std::ostream Is the Dependency You Already Use

You have written dozens of dependencies without naming one. Every class you have given an output operator depends on std::ostream.

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

class Ticket
{
private:
    std::string m_route{};
    int m_coach{};
    int m_seat{};

public:
    Ticket(std::string_view route, int coach, int seat)
        : m_route{route}, m_coach{coach}, m_seat{seat}
    {
    }

    friend std::ostream& operator<<(std::ostream& stream, const Ticket& ticket);
};

std::ostream& operator<<(std::ostream& stream, const Ticket& ticket)
{
    stream << ticket.m_route << " [coach " << ticket.m_coach
           << " seat " << ticket.m_seat << ']';

    return stream;
}

int main()
{
    const Ticket commuter{"Leeds to York", 3, 42};

    std::cout << commuter << '\n';

    return 0;
}
Leeds to York [coach 3 seat 42]

Ticket has no std::ostream member. It has no pointer to one, and if you printed a Ticket a thousand times it would still hold nothing connecting it to any stream. What it has is a function that borrows a stream for the duration of one call in order to accomplish the task of printing. That is a dependency, and std::ostream is far and away the one you will meet most often.

The main function has a dependency of its own, on std::cout. It reaches for the console stream at the moment it wants output and abandons it immediately afterwards.

The Three Shapes a Dependency Takes

Dependencies show up in source code in three recognisable forms. The common thread is that the depended-upon object is never a data member.

Shape How the collaborator arrives Example
Parameter Handed in by the caller, one call at a time std::ostream& in an overloaded operator<<
Local object Constructed inside the function that needs it, destroyed at the closing brace A lookup table built to answer one question
Global facility Reached for by name wherever it is needed std::cout

The middle shape is worth seeing, because it is the one people misread as ownership. Turnstile below builds a FareTable, uses it, and lets it die at the end of the function.

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

class FareTable
{
private:
    int m_peakPence{};
    int m_offPeakPence{};

public:
    FareTable(int peakPence, int offPeakPence)
        : m_peakPence{peakPence}, m_offPeakPence{offPeakPence}
    {
    }

    int priceFor(bool peak) const
    {
        return peak ? m_peakPence : m_offPeakPence;
    }
};

class Turnstile
{
private:
    std::string m_station{};

public:
    explicit Turnstile(std::string_view station)
        : m_station{station}
    {
    }

    void admit(bool peak) const
    {
        const FareTable table{980, 640};

        std::cout << m_station << " charged " << table.priceFor(peak) << " pence\n";
    }
};

int main()
{
    const Turnstile northGate{"York"};

    northGate.admit(true);
    northGate.admit(false);

    return 0;
}
York charged 980 pence
York charged 640 pence

Turnstile constructs a FareTable, so it might look like composition. It is not, because the table is not part of a Turnstile. A Turnstile between calls to admit contains a station name and nothing else, and each call builds a fresh table. Composition requires the part to be contained in the whole; a local variable is contained in a function.

Dependency or Association? Ask What the Object Remembers

This is the distinction that trips people up, so here is a single question that settles it: after the function returns, can you still ask the object about its collaborator?

If yes, the class stored a link, and a stored link is an association. If no, the class used something and let it go, and that is a dependency. A dependency is the weaker of the two, because an association survives between calls and a dependency does not survive the call it was created for.

Association Dependency
Stored as a member Yes, a pointer, reference, or identifier No
Reachable after the call Yes, ask the object No, nothing was kept
Lifetime of the relationship As long as the member holds the link The duration of one operation
Direction May be one-way or two-way Always one-way
Visible in the object's size Yes, the member occupies storage No

The last row is not a metaphor. A Conductor assigned to a coach stores a pointer to it and pays for that pointer in every Conductor object ever created. An Inspector who walks whichever coach is handed to it stores nothing and pays nothing.

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

class Coach
{
private:
    int m_number{};

public:
    explicit Coach(int number)
        : m_number{number}
    {
    }

    int getNumber() const { return m_number; }
};

class Conductor
{
private:
    std::string m_badge{};
    const Coach* m_assigned{};

public:
    Conductor(std::string_view badge, const Coach& assigned)
        : m_badge{badge}, m_assigned{&assigned}
    {
    }

    void announce() const
    {
        std::cout << m_badge << " is working coach " << m_assigned->getNumber() << '\n';
    }
};

class Inspector
{
private:
    std::string m_badge{};

public:
    explicit Inspector(std::string_view badge)
        : m_badge{badge}
    {
    }

    void inspect(const Coach& coach) const
    {
        std::cout << m_badge << " inspected coach " << coach.getNumber() << '\n';
    }
};

int main()
{
    const Coach quietCoach{7};

    const Conductor conductor{"C-118", quietCoach};
    const Inspector inspector{"D-204"};

    conductor.announce();
    inspector.inspect(quietCoach);

    std::cout << "Conductor carries " << sizeof(Conductor) - sizeof(std::string)
              << " bytes beyond its badge\n";
    std::cout << "Inspector carries " << sizeof(Inspector) - sizeof(std::string)
              << " bytes beyond its badge\n";

    return 0;
}
C-118 is working coach 7
D-204 inspected coach 7
Conductor carries 8 bytes beyond its badge
Inspector carries 0 bytes beyond its badge

Those eight bytes are the association. They are the storage the Conductor spends on remembering, and they are exactly what an association buys you: ask a Conductor which coach it works and it can answer. Ask an Inspector the same question and there is nobody to ask, because the coach was never anything more than an argument. The Inspector still depends on Coach, though. Rename getNumber and inspect stops compiling.

Important
Do not read the exact byte counts as a rule of the language. The pointer costs eight bytes on this platform and could differ elsewhere. What generalises is the direction: a stored link adds to an object's size, and a dependency adds nothing.

Why the Weakest Relationship Still Matters

A dependency creates no lifetime obligations, which is precisely why it is easy to acquire without noticing. What it does create is coupling, and coupling is the thing that decides how expensive tomorrow's change is.

Every type your code names is a type whose interface your code has agreed to. Change FareTable::priceFor to take a string instead of a bool and Turnstile stops compiling, even though Turnstile never stored a FareTable and never will. Tracing dependencies is how you answer the question that comes up before every refactor: if I change this, what breaks?

That is the practical use of naming this relationship at all. Dependencies are the edges of your codebase's change-propagation graph, and they are the ones the compiler will not draw for you the way it draws inheritance or membership.

Best Practice
When a class only needs to use another type briefly, take it as a function parameter rather than storing it as a member. The parameter is the honest declaration that the relationship lasts for one call, and it keeps the class smaller and easier to test.

The Four Relationships Side by Side

With dependencies covered, the whole chapter fits in one table. Read it top to bottom as a ladder from strongest to weakest.

Relationship Other object is a part This object manages its lifetime Several users at once Stored as a member Direction
Composition Yes Yes No Yes, by value One-way
Aggregation Yes No Yes Yes, by reference or pointer One-way
Association No No Yes Yes, by any handle One-way or two-way
Dependency No No Yes No One-way only

Every row down the ladder gives something up. Composition owns and manages. Aggregation keeps the containment and drops the ownership. Association keeps the link and drops the containment. Dependency drops the link too, which leaves nothing but the fact that one type calls another.

Looking Forward

That completes the four object relationships. The rest of the chapter puts them to work: container classes are composition applied deliberately, since a container exists to own the elements you hand it, and std::initializer_list is the mechanism that lets such a container be filled with brace syntax. Later chapters revisit dependencies constantly, because every standard library facility you call is one.

Key Terminology

  • Dependency: The weakest of the four relationships, in which a type reaches for a collaborator during a call and keeps no member pointing at it afterwards
  • Coupling: The degree to which one piece of code must change when another changes
  • Unidirectional relationship: One where the user knows about the used type and the used type knows nothing of the user
  • Association: A relationship in which the object keeps a stored link to a collaborator that is not part of it
  • std::ostream: The output stream type, and the dependency you will encounter more than any other

Summary

  • Dependencies are the weakest of the four relationships and always run one way: a collaborator is borrowed for the length of one operation and never recorded as a member.
  • The relationship is temporary. It exists for the duration of one operation and ends when that operation ends.
  • Weak lifetime does not mean weak consequences: a change to the type you depend on can still break your code.
  • std::ostream is the dependency you have used most. Every overloaded operator<< borrows a stream to print with and stores nothing.
  • Dependencies are not data members. The collaborator is passed in as a parameter, constructed locally when it is needed, or reached for as a global facility such as std::cout.
  • An association stores a link and can be asked about its collaborator afterwards. A dependency stores nothing, so there is nothing to ask. The stored link also shows up in the object's size.
  • Constructing an object locally inside a function is a dependency, not composition. The object is contained in the function, not in the class.
  • Dependencies are worth tracking because they map the coupling in a codebase, which is what tells you the blast radius of a change.
  • Prefer taking a briefly used type as a function parameter over storing it as a member, so the code states the true length of the relationship.