Chapter 23 · Object Relationships
Exercise · Chapter 23

Chapter 23 — Object Relationships: Garage Simulation

You build a tiny Garage Simulation whose constructors and destructors write to a shared trace log. That log is the whole point: every time an Engine is created, a Car is created, or a Mechanic leaves, a string like "Engine built: V8-Turbo" or "Car destroyed: Sedan" is appended to trace::log(). The automated tests then assert the exact contents and order of the log — so you cannot fake the result. You have to understand, and correctly implement, the C++ lifetime rules that produce that specific sequence.

The four classes each model a different ownership relationship from notes 23.1–23.7: Engine is a composed value-member of Car (it constructs before and destroys after Car's own body — the standard's member-init-order rule made observable); Driver is aggregated by Car (held as a Driver*, never deleted — the Car has no authority over the Driver's lifetime); Mechanic is a dependency (passed by reference to Car::tuneUp() and not stored — when the function returns, the relationship is over); and RouteList is a container class supporting std::initializer_list construction so you can write RouteList r { "Home", "Gas station", "Destination" }.

The combination of live trace checks and scope blocks makes the abstract vocabulary — composition, aggregation, dependency — physical: you write the code, run the tests, and either the log says the right thing in the right order, or it does not.

Your tasks

  1. Engine ctor/dtor (composition part). Implement Engine::Engine(model): initialise m_model and append "Engine built: " + model to trace::log(). Implement Engine::~Engine(): append "Engine destroyed: " + m_model. Implement Engine::model() to return m_model. This task practices the composition lifecycle: Engine is the owned part, and the trace sequence proves that it constructs/destroys around the Car's own ctor/dtor body.

  2. Driver ctor/dtor (aggregated entity). Implement Driver::Driver(name): initialise m_name and append "Driver built: " + name. Implement Driver::~Driver(): append "Driver destroyed: " + m_name. Implement Driver::name() to return m_name. This is the entity that Car will reference but not own. The test verifies the Driver is alive after the Car that referenced it is destroyed.

  3. Mechanic ctor/dtor and helpers (dependency entity). Implement Mechanic::Mechanic(name) (trace: "Mechanic on duty: " + name), Mechanic::~Mechanic() (trace: "Mechanic off duty: " + m_name), Mechanic::name(), Mechanic::tuneUpCount(), and Mechanic::recordTuneUp() (increments m_tuneUpCount). The test calls tuneUp() three times through two different Cars and checks the counter accumulates correctly.

  4. Car ctor/dtor and all Car member functions (the composite whole). This is the central TASK — all three relationships meet here:

    • Constructor: initialise m_engine{engineModel} and m_make{make} in the member-init list (declaration order); in the body, append "Car built: " + make. Because m_engine is a value member declared before m_make, its constructor runs first — but both run before your body code.
    • Destructor: append "Car destroyed: " + m_make. Do NOT call delete m_driver — the Car does not own the Driver (aggregation rule). After your destructor body, m_engine destructs automatically.
    • setDriver / driver(): store/return the pointer; no ownership transfer.
    • tuneUp(Mechanic&): call mechanic.recordTuneUp(), append "Tuned up by: " + mechanic.name(). Do NOT store the mechanic in any field.
    • make() / engineModel(): return m_make and m_engine.model().
  5. RouteList initializer_list constructor (container class). Implement RouteList::RouteList(std::initializer_list<std::string_view> waypoints): iterate the list with a range-for loop and push_back each waypoint (as a std::string) into m_waypoints. The other five member functions (size, at, add, clear) are already provided. This task proves that custom classes can support { "A", "B", "C" } brace-init syntax (notes 23.7).

Success criteria

  • Scenario A (composition order): trace[0] == "Engine built: V8-Turbo" and trace[1] == "Car built: Sedan" — the Engine must trace BEFORE the Car body. On destruction: trace[2] == "Car destroyed: Sedan" THEN trace[3] == "Engine destroyed: V8-Turbo" — the Car dtor body fires FIRST. Swapping any pair fails this scenario.

  • Scenario B (aggregation isolation): after innerCar goes out of scope, alice.name() == "Alice" must still hold. If Car::~Car() mistakenly calls delete m_driver, the program crashes or alice is corrupted. The test also confirms Driver destroyed: Alice appears in the trace AFTER all Car destroyed events, proving the Driver's lifetime extends past the Car's.

  • Scenario D (dependency reuse): bob.tuneUpCount() == 3 after three tuneUp() calls across two Cars. If tuneUp() does not call mechanic.recordTuneUp(), the count stays at 0.

  • Scenario E (initializer_list ctor): route.size() == 4 for RouteList route { "Home", "Gas station", "Highway", "Destination" }. If the initializer_list constructor stub is left empty, size stays 0 and every at() check reports the wrong result.

  • Scenario G (LIFO destruction order): two Cars in the same scope destroy in reverse declaration order. The trace must show second destructs before first, and each Engine destructs immediately after its own Car body.

Concepts practiced
  • Composition (Engine inside Car — owned part, value member): members initialise (in declaration order) BEFORE the constructor body runs, and destroy (in REVERSE declaration order) AFTER the destructor body runs (notes 23.2)
  • Aggregation (Driver* inside Car — non-owning part): whole references external object; destroying Car must NOT destroy Driver; the Driver must outlive the Car in a correct program (notes 23.3)
  • Dependency (Mechanic& parameter in Car::tuneUp() — temporary borrow): the mechanic is used for one call and not stored; contrast with an association which would store Mechanic* as a field (notes 23.5)
  • Container class with std::initializer_list<T> constructor, enabling brace-init syntax for custom classes (notes 23.6, 23.7)
  • Trace-log technique for asserting construction/destruction order (a pattern you reuse whenever lifetime bugs are hard to spot statically)
  • Member-init-list to initialise composed members before the constructor body (notes 23.2 — the standard rule proven by the trace)
  • Reused from earlier chapters: classes, access specifiers, constructors, destructors (Ch 14–15), std::string / std::string_view (Ch 5), std::vector (Ch 16), assert for bounds checks (Ch 9), pointers and nullptr (Ch 12), const member functions (Ch 14)
Constraints

Allowed: class, constructors with member-init lists, destructors, this (implicit), std::string, std::string_view, std::vector, std::initializer_list, assert, raw pointers (Driver*, nullptr), range-for loops, push_back, and anything from Chapters 1–22.

Forbidden (not yet taught): virtual, override, dynamic_cast (Ch 25); inheritance : public Base (Ch 24); std::weak_ptr / std::shared_ptr ownership tricks beyond Ch 22 coverage; goto; raw new/delete inside your task code (the scaffolding already manages ownership correctly with value members and vector).

Required idioms:

  • Use a member-init list (: m_engine{engineModel}, m_make{make}) in Car's constructor — do not assign inside the body, and list members in their declaration order (notes 23.2; -Wextra warns about re-ordering).
  • Never delete m_driver in Car::~Car() — this is the aggregation rule made mandatory.
  • Never store mechanic in any Car data member — the dependency must be temporary.
  • Use a range-for loop to iterate std::initializer_list (it has no operator[]).
Build & run locally
shell
make            # compile-check starter/garage.cpp (warning-clean)
make test       # grade your code  ->  RED until the TASK blocks are filled in
make solution   # run the reference solution (peek if stuck)
make clean      # remove build artifacts

make test is the grader. Each FAIL: line names the broken condition and its line number in tests/tests.cpp. A [trace dump] section is printed when checks fail, showing the actual log contents to help diagnose ordering mistakes.

Hints
Task 1 — Engine ctor: the member-init trap

You cannot call trace::log().push_back(...) inside the member-init expression itself (:m_model{model}, <can't call trace here>). Write it in the constructor body instead:

C++
Engine::Engine(std::string_view model)
    : m_model { model }
{
    trace::log().push_back("Engine built: " + std::string(model));
}

std::string(model) converts string_view to string for the + operator.

Task 4 — Car ctor: member-init order rule

Declare the init list in the same order as the class member declarations in garage.h (m_engine first, then m_make). C++ always constructs in declaration order regardless of init-list order, but -Wextra warns (and some compilers error) when the list order doesn't match. The trace will show Engine first regardless:

C++
Car::Car(std::string_view make, std::string_view engineModel)
    : m_engine { engineModel }   // constructed FIRST (declared first in header)
    , m_make   { make }
{
    trace::log().push_back("Car built: " + std::string(make));
}

Note: m_driver is not listed — it is default-initialised to nullptr by the Driver* m_driver { nullptr }; in-class member initialiser.

Task 4 — Car dtor: the aggregation rule
C++
Car::~Car()
{
    trace::log().push_back("Car destroyed: " + std::string(m_make));
    // m_driver is NOT deleted — Car does not own the Driver.
    // After this body, m_engine's destructor fires automatically.
}

If you write delete m_driver; here, Scenario B crashes because the Driver is a local variable in the test, not heap-allocated. Even if it were heap-allocated, deleting it here would be wrong: the caller still holds a reference to it. Aggregation means: the whole observes the part, but does not control its fate.

Task 5 — RouteList initializer_list ctor

std::initializer_list<T> supports range-for and iterators, but not operator[]. The simplest body:

C++
RouteList::RouteList(std::initializer_list<std::string_view> waypoints)
{
    for (std::string_view wp : waypoints)
        m_waypoints.push_back(std::string(wp));
}

std::string_view is a lightweight view — push_back(std::string(wp)) copies the characters into an owned std::string stored in m_waypoints. Storing the string_view directly would be a lifetime bug (the temporary brace list's storage may not last past the constructor call).

Why is the trace order exactly what it is? (the deep answer)

C++ standard rules for object lifetime (23.2):

  1. Construction: non-static data members are initialised in their declaration order (not init-list order), before the constructor body runs.
  2. Destruction: the destructor body runs first; then members are destroyed in reverse declaration order.

So for Car:

  • m_engine is declared before m_make, so Engine::Engine fires first in the init list, before the Car body's trace::log().push_back("Car built: ...").
  • On destruction, Car::~Car()'s body runs (appending "Car destroyed"), and then m_engine's destructor fires (appending "Engine destroyed"). Hence the Car dtor body trace comes BEFORE the Engine dtor trace.

The test asserts this order exactly — not because of coincidence, but because it is what the standard mandates. Understanding this rule is essential for writing RAII types and composed objects correctly in real C++ code.

Stretch goals
  • Add a Garage class that stores std::vector<Car*> (an aggregation container of non-owned Cars) and verify that destroying the Garage does not destroy any Cars (the same aggregation lifetime rule at container scale).
  • Add a Garage::addCar(Car&) and Garage::removeCar(Car&) — bidirectional association where each Car could also hold a Garage*.
  • Make Driver move-constructible and store the driver by std::unique_ptr<Driver> in a separate "driver registry" class — an example of composition through owning smart pointer (notes 23.2 variant, Ch 22 tools).
  • Add Car::lastMechanic() returning a stored Mechanic* — observe how storing the mechanic transforms the dependency into an association (notes 23.5). Then remove the storage to put it back to a dependency and notice the design difference.
  • Replace RouteList's internal std::vector<std::string> with a manually managed new[]/delete[] array (Ch 19 tools) and implement a proper copy constructor and assignment operator to avoid shallow-copy bugs (notes 23.7's TinyArray example).
starter/garage.cpp C++
// ============================================================================
//  starter/garage.cpp  —  YOUR WORKSPACE  (Chapter 23)
// ----------------------------------------------------------------------------
//  Fill in the five TASK blocks below. Each maps 1:1 to a numbered task in
//  the README and to a declaration in ../garage.h. The file compiles right now
//  (stubs return placeholders), but `make test` is RED. Your goal: GREEN.
//
//      make build         compile your code (should already work)
//      make test          grade it  (RED until you fill these in)
//      make solution      run the grader against the reference if you get stuck
//
//  BEFORE YOU CODE — read these four things in order:
//    1.  ../garage.h          — the contract (all class declarations + comments)
//    2.  The README (tasks)   — what each TASK asks for
//    3.  The notes            — ../../notes/chapter-23.md
//    4.  The tests            — tests/tests.cpp  (so you know what is checked)
//
//  CENTRAL IDEA:
//    The trace::log() calls inside every constructor and destructor you write
//    are NOT optional decoration — the TESTS CHECK THEM. The trace makes
//    C++ lifetime rules PHYSICAL: you can't guess the construction/destruction
//    order; you have to know the rule and get the strings exactly right.
//
//    Key rule (23.2): data MEMBERS construct BEFORE the constructor body runs
//    (in declaration order) and destroy AFTER the destructor body (in reverse
//    declaration order). Engine is declared first in Car, so Engine always
//    constructs before and destroys after the Car's own ctor/dtor body.
//
//  RELATIONSHIP VOCABULARY (fill in from notes 23.1 as you read):
//    — Engine inside Car:           COMPOSITION  (owns lifetime, value member)
//    — Driver* inside Car:          AGGREGATION  (does NOT own, pointer)
//    — Mechanic& in tuneUp():       DEPENDENCY   (temporary borrow, not stored)
//    — RouteList owns its strings:  CONTAINER    (value container, 23.6 / 23.7)
// ============================================================================

#include "../garage.h"   // the shared contract — found via -I. in the Makefile

#include <cassert>
#include <string>
#include <string_view>
#include <vector>
#include <initializer_list>

// ─── trace::log() / trace::clear() ───────────────────────────────────────────
//  PROVIDED — do NOT modify. A function-static vector persists for the program's
//  lifetime. (This uses Ch 7 "static duration" — a preview; provided scaffolding
//  so you don't need to know it yet.) Your ctors/dtors call trace::log().push_back().

namespace trace {
    std::vector<std::string>& log()
    {
        static std::vector<std::string> s_log;
        return s_log;
    }

    void clear()
    {
        log().clear();
    }
}

// ─── TASK 1: Engine constructor and destructor ────────────────────────────────
//
//  Engine is the COMPOSITION part of Car (notes 23.2). It is created and
//  destroyed together with the Car that owns it — a value member.
//
//  Implement:
//    Engine::Engine(std::string_view model)
//        — Store `model` in m_model (use std::string(model) to convert).
//        — Append exactly this string to trace::log():
//              "Engine built: " + model
//          e.g. for model "V8-Turbo" → trace gets "Engine built: V8-Turbo".
//
//    Engine::~Engine()
//        — Append to trace::log():
//              "Engine destroyed: " + m_model
//
//    Engine::model() const   → return m_model
//
//  TRAP: do NOT call trace from inside m_model's init — it is not yet populated
//  when the member-init runs. Append to the log INSIDE the constructor body {}.
//
//   >>> YOUR CODE HERE <<<
//

Engine::Engine(std::string_view /*model*/)
    // Hint: initialise m_model here with : m_model { model }
{
    // TODO: push "Engine built: <model>" onto trace::log()
}

Engine::~Engine()
{
    // TODO: push "Engine destroyed: <m_model>" onto trace::log()
}

std::string_view Engine::model() const
{
    return m_model;   // placeholder: returns m_model (which is empty until Task 1 done)
}
// ─────────────────────────────────────────────────────────────────────────────


// ─── TASK 2: Driver constructor and destructor ────────────────────────────────
//
//  Driver is an INDEPENDENT entity that Car AGGREGATES (notes 23.3).
//  Car holds a Driver* but NEVER deletes it.
//
//  Implement:
//    Driver::Driver(std::string_view name)
//        — Store name in m_name.
//        — Append to trace::log():
//              "Driver built: " + name
//
//    Driver::~Driver()
//        — Append to trace::log():
//              "Driver destroyed: " + m_name
//
//    Driver::name() const  → return m_name
//
//   >>> YOUR CODE HERE <<<
//

Driver::Driver(std::string_view /*name*/)
{
    // TODO: store name, push "Driver built: <name>"
}

Driver::~Driver()
{
    // TODO: push "Driver destroyed: <m_name>"
}

std::string_view Driver::name() const
{
    return m_name;   // placeholder: returns empty string until Task 2 done
}
// ─────────────────────────────────────────────────────────────────────────────


// ─── TASK 3: Mechanic constructor, destructor, and helpers ───────────────────
//
//  Mechanic is used as a DEPENDENCY — passed by reference to Car::tuneUp(),
//  NEVER stored inside Car (notes 23.5).
//
//  Implement:
//    Mechanic::Mechanic(std::string_view name)
//        — Store name in m_name.
//        — Append to trace::log():
//              "Mechanic on duty: " + name
//
//    Mechanic::~Mechanic()
//        — Append to trace::log():
//              "Mechanic off duty: " + m_name
//
//    Mechanic::name() const        → return m_name
//    Mechanic::tuneUpCount() const → return m_tuneUpCount
//    Mechanic::recordTuneUp()      → increment m_tuneUpCount by 1
//
//   >>> YOUR CODE HERE <<<
//

Mechanic::Mechanic(std::string_view /*name*/)
{
    // TODO: store name, push "Mechanic on duty: <name>"
}

Mechanic::~Mechanic()
{
    // TODO: push "Mechanic off duty: <m_name>"
}

std::string_view Mechanic::name() const
{
    return m_name;   // placeholder: returns empty string until Task 3 done
}

int Mechanic::tuneUpCount() const
{
    return m_tuneUpCount;   // placeholder: always 0 until recordTuneUp implemented
}

void Mechanic::recordTuneUp()
{
    // TODO: increment m_tuneUpCount
}
// ─────────────────────────────────────────────────────────────────────────────


// ─── TASK 4: Car constructor, destructor, and all Car member functions ────────
//
//  Car is the composite WHOLE that demonstrates three relationships at once:
//    1. COMPOSITION with Engine (m_engine — value member; Engine is built first,
//       destroyed last among Car's members, notes 23.2).
//    2. AGGREGATION with Driver (m_driver — pointer; Car does NOT delete it,
//       notes 23.3). The Car can have nullptr driver (unoccupied).
//    3. DEPENDENCY on Mechanic (parameter in tuneUp — NOT stored, notes 23.5).
//
//  Implement:
//
//    Car::Car(std::string_view make, std::string_view engineModel)
//        — Use a MEMBER-INIT LIST to construct m_engine{engineModel} and m_make{make}.
//          (m_driver default-initialises to nullptr from the class definition.)
//        — In the constructor BODY, append to trace::log():
//              "Car built: " + make
//          NOTE: m_engine's constructor already fired BEFORE this body runs.
//          The trace will read: "Engine built: ..." then "Car built: ...".
//
//    Car::~Car()
//        — Append to trace::log():
//              "Car destroyed: " + m_make
//        — DO NOT call delete on m_driver — Car does not own the Driver.
//          NOTE: after this body runs, m_engine's destructor will fire next.
//          The trace will read: "Car destroyed: ..." then "Engine destroyed: ...".
//
//    Car::setDriver(Driver* driver) — store the pointer (no ownership transfer)
//    Car::driver() const            — return m_driver
//    Car::tuneUp(Mechanic& mechanic)
//        — Call mechanic.recordTuneUp() (the dependency use).
//        — Append to trace::log():
//              "Tuned up by: " + mechanic.name()
//        — Do NOT store the mechanic anywhere.
//    Car::make() const              — return m_make
//    Car::engineModel() const       — delegate to m_engine.model()
//
//   >>> YOUR CODE HERE <<<
//

Car::Car(std::string_view /*make*/, std::string_view /*engineModel*/)
    : m_engine { "" }   // placeholder: passes empty string — TASK is to fix this
    // Hint: also initialise m_make here
{
    // TODO: push "Car built: <make>" onto trace::log()
}

Car::~Car()
{
    // TODO: push "Car destroyed: <m_make>"
    // DO NOT delete m_driver — aggregation means Car does not own the Driver.
}

void Car::setDriver(Driver* driver)
{
    m_driver = driver;   // placeholder already correct — store pointer, no ownership
}

Driver* Car::driver() const
{
    return m_driver;   // placeholder already correct
}

void Car::tuneUp(Mechanic& mechanic)
{
    // TODO: call mechanic.recordTuneUp() and push "Tuned up by: <name>" onto trace
    (void)mechanic;   // suppress unused-parameter warning until Task 4 is filled in
}

std::string_view Car::make() const
{
    return m_make;   // placeholder: returns empty until Task 4 done
}

std::string_view Car::engineModel() const
{
    return m_engine.model();   // delegation to COMPOSED part (already correct shape)
}
// ─────────────────────────────────────────────────────────────────────────────


// ─── TASK 5: RouteList initializer_list constructor ──────────────────────────
//
//  RouteList is a VALUE CONTAINER (notes 23.6): it owns its waypoint strings.
//  The initializer_list constructor lets callers write:
//      RouteList r { "A", "B", "C" };
//  which is more readable than calling add() three times.
//
//  Implement:
//    RouteList::RouteList(std::initializer_list<std::string_view> waypoints)
//        — For each std::string_view in waypoints, push a std::string copy into
//          m_waypoints. (Range-for loop is cleanest — initializer_list does not
//          provide operator[], but it does support begin()/end() and range-for.)
//        — Notes 23.7: "Accessing initializer_list elements — use a range-for loop."
//
//  The other five member functions (size, at, add, clear) are provided below
//  and already correct — you only implement the constructor.
//
//   >>> YOUR CODE HERE <<<
//

RouteList::RouteList(std::initializer_list<std::string_view> /*waypoints*/)
{
    // TODO: iterate over waypoints and push each one into m_waypoints
    // Hint:  for (std::string_view wp : waypoints)
    //            m_waypoints.push_back(std::string(wp));
}
// ─────────────────────────────────────────────────────────────────────────────

// ── Provided: the rest of RouteList is already implemented ──────────────────
//  These bodies are CORRECT and should NOT be modified.

std::size_t RouteList::size() const
{
    return m_waypoints.size();
}

std::string_view RouteList::at(std::size_t index) const
{
    assert(index < m_waypoints.size());
    return m_waypoints[index];
}

void RouteList::add(std::string_view waypoint)
{
    m_waypoints.push_back(std::string(waypoint));
}

void RouteList::clear()
{
    m_waypoints.clear();
}
Run
Submit
Run in your browser — coming soon For now: copy or download the files and use make test locally (see “Build & run locally” above).