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
Enginector/dtor (composition part). ImplementEngine::Engine(model): initialisem_modeland append"Engine built: " + modeltotrace::log(). ImplementEngine::~Engine(): append"Engine destroyed: " + m_model. ImplementEngine::model()to returnm_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.Driverctor/dtor (aggregated entity). ImplementDriver::Driver(name): initialisem_nameand append"Driver built: " + name. ImplementDriver::~Driver(): append"Driver destroyed: " + m_name. ImplementDriver::name()to returnm_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.Mechanicctor/dtor and helpers (dependency entity). ImplementMechanic::Mechanic(name)(trace:"Mechanic on duty: " + name),Mechanic::~Mechanic()(trace:"Mechanic off duty: " + m_name),Mechanic::name(),Mechanic::tuneUpCount(), andMechanic::recordTuneUp()(incrementsm_tuneUpCount). The test callstuneUp()three times through two different Cars and checks the counter accumulates correctly.Carctor/dtor and allCarmember functions (the composite whole). This is the central TASK — all three relationships meet here:- Constructor: initialise
m_engine{engineModel}andm_make{make}in the member-init list (declaration order); in the body, append"Car built: " + make. Becausem_engineis a value member declared beforem_make, its constructor runs first — but both run before your body code. - Destructor: append
"Car destroyed: " + m_make. Do NOT calldelete m_driver— the Car does not own the Driver (aggregation rule). After your destructor body,m_enginedestructs automatically. setDriver/driver(): store/return the pointer; no ownership transfer.tuneUp(Mechanic&): callmechanic.recordTuneUp(), append"Tuned up by: " + mechanic.name(). Do NOT store the mechanic in any field.make()/engineModel(): returnm_makeandm_engine.model().
- Constructor: initialise
RouteListinitializer_list constructor (container class). ImplementRouteList::RouteList(std::initializer_list<std::string_view> waypoints): iterate the list with a range-for loop andpush_backeach waypoint (as astd::string) intom_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"andtrace[1] == "Car built: Sedan"— the Engine must trace BEFORE the Car body. On destruction:trace[2] == "Car destroyed: Sedan"THENtrace[3] == "Engine destroyed: V8-Turbo"— the Car dtor body fires FIRST. Swapping any pair fails this scenario.Scenario B (aggregation isolation): after
innerCargoes out of scope,alice.name() == "Alice"must still hold. IfCar::~Car()mistakenly callsdelete m_driver, the program crashes oraliceis corrupted. The test also confirmsDriver destroyed: Aliceappears in the trace AFTER allCar destroyedevents, proving the Driver's lifetime extends past the Car's.Scenario D (dependency reuse):
bob.tuneUpCount() == 3after threetuneUp()calls across two Cars. IftuneUp()does not callmechanic.recordTuneUp(), the count stays at 0.Scenario E (initializer_list ctor):
route.size() == 4forRouteList route { "Home", "Gas station", "Highway", "Destination" }. If the initializer_list constructor stub is left empty, size stays 0 and everyat()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
seconddestructs beforefirst, and each Engine destructs immediately after its own Car body.
Concepts practiced
- Composition (
EngineinsideCar— 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*insideCar— non-owning part): whole references external object; destroyingCarmust NOT destroyDriver; the Driver must outlive the Car in a correct program (notes 23.3) - Dependency (
Mechanic¶meter inCar::tuneUp()— temporary borrow): the mechanic is used for one call and not stored; contrast with an association which would storeMechanic*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),assertfor bounds checks (Ch 9), pointers andnullptr(Ch 12),constmember 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}) inCar's constructor — do not assign inside the body, and list members in their declaration order (notes 23.2;-Wextrawarns about re-ordering). - Never
delete m_driverinCar::~Car()— this is the aggregation rule made mandatory. - Never store
mechanicin anyCardata member — the dependency must be temporary. - Use a range-for loop to iterate
std::initializer_list(it has nooperator[]).
Build & run locally
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 artifactsmake 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:
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:
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
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:
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):
- Construction: non-static data members are initialised in their declaration order (not init-list order), before the constructor body runs.
- Destruction: the destructor body runs first; then members are destroyed in reverse declaration order.
So for Car:
m_engineis declared beforem_make, soEngine::Enginefires first in the init list, before the Car body'strace::log().push_back("Car built: ...").- On destruction,
Car::~Car()'s body runs (appending "Car destroyed"), and thenm_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
Garageclass that storesstd::vector<Car*>(an aggregation container of non-owned Cars) and verify that destroying theGaragedoes not destroy any Cars (the same aggregation lifetime rule at container scale). - Add a
Garage::addCar(Car&)andGarage::removeCar(Car&)— bidirectional association where eachCarcould also hold aGarage*. - Make
Drivermove-constructible and store the driver bystd::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 storedMechanic*— 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 internalstd::vector<std::string>with a manually managednew[]/delete[]array (Ch 19 tools) and implement a proper copy constructor and assignment operator to avoid shallow-copy bugs (notes 23.7'sTinyArrayexample).
// ============================================================================
// 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();
}
Try the lab first — the learning is in the attempt.
// ============================================================================
// solution/garage.cpp — REFERENCE IMPLEMENTATION (Chapter 23)
// ----------------------------------------------------------------------------
// This is the COMPLETE, correct implementation of the Garage Simulation.
// Read it after you have attempted the exercise yourself — or consult it if
// you are stuck. Every TASK is richly commented with the WHY, not just the how.
//
// CENTRAL LESSON MADE PHYSICAL:
// The trace log in every constructor and destructor below is not decoration —
// it is *evidence*. When the tests check trace::log() contents and ORDER,
// they are asserting C++ lifetime rules that you cannot see in static code:
//
// (a) COMPOSITION (23.2):
// Engine constructs BEFORE the Car constructor body runs, and destroys
// AFTER the Car destructor body. This is the standard's member-init-order
// rule. The trace makes the ordering physical and testable.
//
// (b) AGGREGATION (23.3):
// Destroying a Car does NOT destroy the Driver. After the Car goes out of
// scope, the Driver still exists. The test verifies this by reading the
// Driver's name after the Car is gone.
//
// (c) DEPENDENCY (23.5):
// Mechanic is used inside tuneUp() but NOT stored. The Mechanic's lifetime
// is controlled by the caller, not by the Car.
//
// (d) CONTAINER + std::initializer_list (23.6, 23.7):
// RouteList is a value container — it owns its waypoints. The braced-init
// syntax {A, B, C} works because of the initializer_list constructor.
//
// CS6340 / LLVM tie-in:
// A typical analysis pass uses all four relationships in one file:
// std::string m_passName; // composition
// std::vector<llvm::Instruction*> m_candidates; // aggregation (ptrs)
// void analyze(const llvm::Function& F); // dependency (parameter)
// llvm::Module* m_module; // association (non-owning ptr)
// Recognizing the pattern behind the syntax is what lets you read and
// write LLVM code without making ownership mistakes.
// ============================================================================
#include "../garage.h" // the shared contract — found via -I. in the Makefile
#include <cassert> // assert — bounds checks (Ch 9)
#include <string>
#include <string_view>
#include <vector>
#include <initializer_list>
// ─── trace::log() / trace::clear() ───────────────────────────────────────────
// We return a reference to a FUNCTION-LOCAL STATIC vector (Ch 7 "static
// duration" — a preview; provided scaffolding). One canonical copy lives in
// this translation unit; every call returns the same object. This is the
// Meyers-singleton idiom — simple, thread-safe since C++11, no header-only ODR
// risk. (Provided as scaffolding — NOT a learner task.)
namespace trace {
std::vector<std::string>& log()
{
static std::vector<std::string> s_log;
return s_log;
}
void clear()
{
log().clear();
}
}
// ─── Engine implementation ─────────────────────────────────────────────────────
// TASK 1 in the starter.
//
// Engine is the COMPOSED PART of Car. Its ctor/dtor append to the trace log so
// that the tests can assert the exact order relative to Car's own ctor/dtor.
//
// KEY RULE (23.2): Because m_engine is declared BEFORE m_make in Car's class
// definition, Engine constructs first and destroys LAST (among Car's members).
// The Car constructor body runs AFTER all members are constructed; the Car
// destructor body runs BEFORE members are destroyed. So the trace for one
// Car creation/deletion reads:
//
// "Engine built" ← member init (before Car ctor body)
// "Car built: Sedan" ← Car ctor body
// [in-use events]
// "Car destroyed: Sedan" ← Car dtor body (before member dtors)
// "Engine destroyed" ← member dtor (after Car dtor body)
//
// Getting this order wrong is the canonical Chapter-23 trap. The tests check it.
Engine::Engine(std::string_view model)
: m_model { model } // owned copy of the model string
{
// COMPOSITION trace: append to the shared log so tests can assert order.
trace::log().push_back("Engine built: " + std::string(model));
}
Engine::~Engine()
{
// Destruction trace: see the note above about order relative to Car's dtor body.
trace::log().push_back("Engine destroyed: " + std::string(m_model));
}
std::string_view Engine::model() const
{
return m_model;
}
// ─── Driver implementation ─────────────────────────────────────────────────────
// TASK 2 in the starter.
//
// Driver is an INDEPENDENT object. It exists on its own; Car can REFERENCE it
// (aggregation) without owning it. The trace lets tests verify that a Driver
// survives after a Car that held it is destroyed.
Driver::Driver(std::string_view name)
: m_name { name }
{
trace::log().push_back("Driver built: " + std::string(name));
}
Driver::~Driver()
{
// If this ever ran WHILE the Car that referenced it still existed, that would
// mean the Car outlived its aggregated Driver — a lifetime bug (notes 23.3).
trace::log().push_back("Driver destroyed: " + std::string(m_name));
}
std::string_view Driver::name() const
{
return m_name;
}
// ─── Mechanic implementation ───────────────────────────────────────────────────
// TASK 3 in the starter.
//
// Mechanic is a DEPENDENCY — it is passed by reference to Car::tuneUp() and used
// for that call only. Mechanic is NOT stored in Car. If you stored it, the
// relationship would become an association (notes 23.5 — "is the relationship
// stored?"). The test verifies that tuneUpCount() increments correctly through
// the dependency relationship.
Mechanic::Mechanic(std::string_view name)
: m_name { name }
{
trace::log().push_back("Mechanic on duty: " + std::string(name));
}
Mechanic::~Mechanic()
{
trace::log().push_back("Mechanic off duty: " + std::string(m_name));
}
std::string_view Mechanic::name() const
{
return m_name;
}
int Mechanic::tuneUpCount() const
{
return m_tuneUpCount;
}
void Mechanic::recordTuneUp()
{
++m_tuneUpCount;
}
// ─── Car implementation ────────────────────────────────────────────────────────
// TASK 4 in the starter.
//
// Car is the WHOLE that owns Engine (composition) and references Driver
// (aggregation). It also demonstrates the dependency relationship through tuneUp.
//
// MEMBER-INIT-LIST ORDER:
// m_engine is declared FIRST in the class, so it is initialized FIRST.
// Even if you listed m_make before m_engine in the init list, the compiler
// still initialises in declaration order (and -Wextra warns about reorder).
// We list them in declaration order to be explicit.
Car::Car(std::string_view make, std::string_view engineModel)
: m_engine { engineModel } // COMPOSITION: Engine built before Car body runs
, m_make { make } // owned value member
// m_driver default-initialised to nullptr (set in class definition)
{
// This line executes AFTER m_engine's constructor has already run.
// The trace order: "Engine built" appears BEFORE "Car built".
trace::log().push_back("Car built: " + std::string(make));
}
Car::~Car()
{
// This line executes BEFORE m_engine's destructor runs.
// The trace order: "Car destroyed" appears BEFORE "Engine destroyed".
trace::log().push_back("Car destroyed: " + std::string(m_make));
// CRITICAL: do NOT call "delete m_driver" here!
// m_driver is an AGGREGATION — Car does not own the Driver.
// The Driver must outlive the Car in a correct program (notes 23.3).
// Deleting it here would be undefined behaviour if the caller still holds
// a reference to the Driver object.
}
// ── Aggregation: assign / query the (non-owned) driver ──────────────────────
// setDriver stores a NON-OWNING pointer. The contract (from the header comment)
// states that the pointed-to Driver must remain alive as long as this Car uses
// it. Car does nothing to enforce that — it is the CALLER's responsibility.
// This is the aggregation lifetime risk described in notes 23.3.
void Car::setDriver(Driver* driver)
{
m_driver = driver; // store the pointer; do NOT allocate or take ownership
}
Driver* Car::driver() const
{
return m_driver;
}
// ── Dependency: use a Mechanic for one call only ─────────────────────────────
// tuneUp() is the DEPENDENCY relationship in action:
// - Mechanic& is a PARAMETER (not stored in any member field).
// - The mechanic is used (recordTuneUp() called) and then the reference goes
// out of scope when tuneUp() returns.
// Compare this to an ASSOCIATION, which would store `Mechanic* m_lastMechanic`
// as a field. That would be a stored relationship — a dependency is temporary
// (notes 23.5 distinction: "is the relationship stored?").
void Car::tuneUp(Mechanic& mechanic)
{
mechanic.recordTuneUp(); // USE the mechanic (dependency: call through param)
trace::log().push_back("Tuned up by: " + std::string(mechanic.name()));
// mechanic is NOT stored. When this function returns, the dependency is over.
}
std::string_view Car::make() const
{
return m_make;
}
std::string_view Car::engineModel() const
{
return m_engine.model(); // delegate to the COMPOSED part
}
// ─── RouteList implementation ──────────────────────────────────────────────────
// TASK 5 in the starter.
//
// RouteList is a VALUE CONTAINER (23.6): it owns its waypoint strings.
// The initializer_list constructor enables brace-init syntax (23.7):
// RouteList r { "A", "B", "C" };
//
// NOTE on std::initializer_list (23.7):
// - The list is passed by VALUE (it is a lightweight view, like string_view).
// - Iterating with a range-for loop or begin()/end() is the standard approach
// (operator[] is NOT provided by initializer_list).
// - Brace-init prefers the initializer_list ctor when it matches, so
// `RouteList r { "A", "B" }` calls THIS ctor, not the default ctor.
//
// CS6340 analogy:
// std::vector<std::string> generatedInputs { "seed1", "seed2", "seed3" };
// That is exactly RouteList's pattern applied to a standard container.
RouteList::RouteList(std::initializer_list<std::string_view> waypoints)
{
// Iterate the lightweight view and copy each name into our owned vector.
// We cannot use `m_waypoints { waypoints }` directly because waypoints is
// initializer_list<string_view> but m_waypoints is vector<string> — a range
// loop with explicit conversion is the clearest approach.
for (std::string_view wp : waypoints)
{
m_waypoints.push_back(std::string(wp));
}
}
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()); // bounds guard (Ch 9)
return m_waypoints[index];
}
void RouteList::add(std::string_view waypoint)
{
m_waypoints.push_back(std::string(waypoint));
}
void RouteList::clear()
{
m_waypoints.clear();
}
// ============================================================================
// tests/tests.cpp — Automated grader for the Garage Simulation (Chapter 23)
// ----------------------------------------------------------------------------
// A tiny no-framework unit-test harness (same style as drills/CLAUDE.md).
// The Makefile compiles this file against EITHER starter/garage.cpp (`make test`)
// OR solution/garage.cpp (`make test-solution`). The shared contract is the
// header at ../garage.h.
//
// WHAT THESE TESTS ASSERT:
//
// The TRACE LOG is the spine of this grader. By appending events in ctors/dtors,
// the code makes C++ lifetime rules OBSERVABLE at runtime. The tests check:
//
// 1. COMPOSITION order (23.2):
// Engine constructs BEFORE the Car body, and destroys AFTER. The trace
// sequence proves that the standard's member-init-order rule holds.
//
// 2. AGGREGATION isolation (23.3):
// Destroying a Car does NOT destroy the Driver. After the Car scope ends,
// the Driver is still alive (its name() is still readable).
//
// 3. DEPENDENCY scope (23.5):
// tuneUp() increments Mechanic::tuneUpCount() via a temporary borrow.
// The Mechanic is not stored; its lifetime is the caller's responsibility.
//
// 4. CONTAINER + std::initializer_list (23.6 / 23.7):
// RouteList can be brace-initialised and supports add/at/size/clear.
//
// EDGE CASES:
// - Car with no driver (m_driver == nullptr).
// - RouteList constructed empty vs. brace-initialised.
// - Multiple tune-ups accumulate on one Mechanic (the dependency is reusable).
// - Trace order across two Cars in the same scope (stack-like LIFO destruction).
// ============================================================================
#include <iostream>
#include <string>
#include <vector>
#include "../garage.h"
static int fails = 0;
// ── CHECK helpers ──────────────────────────────────────────────────────────────
#define CHECK(cond) \
do { if(!(cond)){ std::cerr << "FAIL: " #cond " @line " << __LINE__ << "\n"; ++fails; } } while(0)
// Check that the Nth event in trace::log() equals `expected`.
#define CHECK_TRACE(n, expected) \
do { \
auto& t = trace::log(); \
if ((n) >= (int)t.size()) { \
std::cerr << "FAIL: trace event " << (n) << " missing (log has " \
<< t.size() << " entries) @line " << __LINE__ << "\n"; \
++fails; \
} else if (t[(n)] != (expected)) { \
std::cerr << "FAIL: trace[" << (n) << "] == \"" << t[(n)] \
<< "\" want \"" << (expected) << "\" @line " << __LINE__ << "\n"; \
++fails; \
} \
} while(0)
// Check trace log has exactly `n` events.
#define CHECK_TRACE_SIZE(n) \
do { \
if ((int)trace::log().size() != (n)) { \
std::cerr << "FAIL: trace has " << trace::log().size() \
<< " events, want " << (n) << " @line " << __LINE__ << "\n"; \
++fails; \
} \
} while(0)
// ── Helper: dump the trace for debugging failed tests ──────────────────────────
static void dumpTrace()
{
const auto& t = trace::log();
std::cerr << " [trace dump — " << t.size() << " events]\n";
for (std::size_t i = 0; i < t.size(); ++i)
std::cerr << " [" << i << "] \"" << t[i] << "\"\n";
}
int main()
{
// ══════════════════════════════════════════════════════════════════════════
// SCENARIO A — Composition: member init/destroy ORDER
// ══════════════════════════════════════════════════════════════════════════
// This is the CRITICAL test for Task 1 + Task 4.
//
// Rule (23.2): members construct BEFORE the ctor body; destroy AFTER the dtor
// body, in REVERSE declaration order. Engine is declared FIRST in Car, so:
//
// construct phase: Engine ctor → Car ctor body
// destruct phase: Car dtor body → Engine dtor
//
// We wrap the Car in a scope block to force its destructor to fire before we
// check the trace.
{
trace::clear();
{
Car car { "Sedan", "V8-Turbo" };
// After construction: engine first, then car body.
CHECK_TRACE(0, "Engine built: V8-Turbo"); // member init runs first
CHECK_TRACE(1, "Car built: Sedan"); // ctor body runs after
CHECK_TRACE_SIZE(2);
}
// Car went out of scope — destructor fired.
// Car dtor body runs BEFORE m_engine's dtor.
CHECK_TRACE(2, "Car destroyed: Sedan"); // dtor body fires first
CHECK_TRACE(3, "Engine destroyed: V8-Turbo"); // member dtor fires after
CHECK_TRACE_SIZE(4);
}
// ══════════════════════════════════════════════════════════════════════════
// SCENARIO B — Aggregation: Driver survives Car destruction (Task 2 + Task 4)
// ══════════════════════════════════════════════════════════════════════════
// KEY PROPERTY (23.3): The whole (Car) does NOT own the part (Driver).
// After the Car is destroyed, the Driver must still be usable.
// If Car's destructor called `delete m_driver`, this test would either
// crash or report use-after-free under a sanitizer.
{
trace::clear();
{
Driver alice { "Alice" };
Car car { "Hatchback", "I4-Electric" };
car.setDriver(&alice);
// Verify aggregation is set up correctly.
CHECK(car.driver() == &alice);
CHECK(car.driver()->name() == "Alice");
// Inner scope ends — Car destroyed BEFORE Alice goes out of scope.
{
// A tighter scope so Car dies while alice is still alive.
Car innerCar { "Coupe", "V6-Hybrid" };
innerCar.setDriver(&alice);
// innerCar destroyed here:
// "Engine built: V6-Hybrid" / "Car built: Coupe" happened;
// now "Car destroyed: Coupe" / "Engine destroyed: V6-Hybrid"
}
// Alice is still alive — her destructor has NOT run yet.
CHECK(alice.name() == "Alice"); // driver survives car destruction
}
// Now alice goes out of scope — Driver destroyed AFTER Car.
// The trace should contain "Driver destroyed: Alice" at some point,
// but NOT paired with a Car destruction (no delete m_driver in Car::~Car).
bool carDestroyedCoupeSeen { false };
bool driverDestroyedAfterCar { false };
bool carDestroyedAtAll { false };
for (std::size_t i = 0; i < trace::log().size(); ++i)
{
if (trace::log()[i] == "Car destroyed: Coupe")
{
carDestroyedCoupeSeen = true;
carDestroyedAtAll = true;
}
if (trace::log()[i] == "Driver destroyed: Alice" && carDestroyedCoupeSeen)
driverDestroyedAfterCar = true;
}
CHECK(carDestroyedAtAll); // Car was definitely destroyed
CHECK(driverDestroyedAfterCar); // Driver lived on past the Car
}
// ══════════════════════════════════════════════════════════════════════════
// SCENARIO C — Null driver (edge case for aggregation)
// ══════════════════════════════════════════════════════════════════════════
// A Car with no driver set should return nullptr from driver().
// This verifies that m_driver is default-initialised to nullptr, not garbage.
{
trace::clear();
Car loner { "Truck", "Diesel-V10" };
CHECK(loner.driver() == nullptr); // no driver assigned -> nullptr
}
// ══════════════════════════════════════════════════════════════════════════
// SCENARIO D — Dependency: tuneUp() borrows and uses Mechanic (Task 3 + Task 4)
// ══════════════════════════════════════════════════════════════════════════
// WHAT WE'RE TESTING (23.5):
// - Mechanic::tuneUpCount() increments each time Car::tuneUp() is called.
// - The Mechanic is NOT stored in Car — each call is an independent borrow.
// - Multiple Cars can use the same Mechanic (the dependency is re-entrant).
// - Trace contains "Tuned up by: <name>" entries in order.
{
trace::clear();
Mechanic bob { "Bob" };
CHECK(bob.tuneUpCount() == 0); // fresh mechanic, no work done yet
Car car1 { "Roadster", "EV-Motor" };
Car car2 { "Minivan", "V6-Base" };
car1.tuneUp(bob);
CHECK(bob.tuneUpCount() == 1); // one tune-up recorded
car2.tuneUp(bob);
CHECK(bob.tuneUpCount() == 2); // second tune-up on same mechanic
car1.tuneUp(bob);
CHECK(bob.tuneUpCount() == 3); // third — dependency is reusable
// Verify the trace contains the expected "Tuned up by" events in order.
// Count them (exact positions vary because of ctor events interleaved).
int tuneUpCount { 0 };
for (const auto& event : trace::log())
if (event == "Tuned up by: Bob")
++tuneUpCount;
CHECK(tuneUpCount == 3);
// Mechanic is NOT stored in any Car — after tuneUp() returns, Car has
// no reference to Bob. This is confirmed by the fact that Bob's
// tuneUpCount() is only accessible through the original `bob` variable.
CHECK(car1.driver() == nullptr); // tuneUp does not set driver
}
// ══════════════════════════════════════════════════════════════════════════
// SCENARIO E — RouteList: container + std::initializer_list (Task 5)
// ══════════════════════════════════════════════════════════════════════════
{
// Default-constructed: empty.
RouteList empty;
CHECK(empty.size() == 0);
// Brace-initialised: four waypoints (23.7 initializer_list ctor).
// Guard at() calls with a size check so the starter does not abort.
RouteList route { "Home", "Gas station", "Highway", "Destination" };
CHECK(route.size() == 4);
if (route.size() >= 4)
{
CHECK(route.at(0) == "Home");
CHECK(route.at(1) == "Gas station");
CHECK(route.at(2) == "Highway");
CHECK(route.at(3) == "Destination");
}
// Single-element list.
RouteList solo { "Airport" };
CHECK(solo.size() == 1);
if (solo.size() >= 1)
CHECK(solo.at(0) == "Airport");
// Add appends to an existing list.
route.add("Parking lot");
CHECK(route.size() == 5);
if (route.size() >= 5)
CHECK(route.at(4) == "Parking lot");
// Clear empties the container.
route.clear();
CHECK(route.size() == 0);
// Add to a cleared list still works.
route.add("Restart");
CHECK(route.size() == 1);
if (route.size() >= 1)
CHECK(route.at(0) == "Restart");
// Empty-brace list: 0 elements (edge case — braces with zero args).
// Note: RouteList r {}; would call the DEFAULT ctor, not initializer_list,
// because the empty list matches the default ctor (23.7 brace-init rules).
// But an explicit empty initializer_list<string_view> call works:
RouteList emptyBrace(std::initializer_list<std::string_view>{});
CHECK(emptyBrace.size() == 0);
}
// ══════════════════════════════════════════════════════════════════════════
// SCENARIO F — Composition detail: engine model is accessible after build
// ══════════════════════════════════════════════════════════════════════════
{
trace::clear();
Car sports { "SportsCar", "Twin-Turbo-V12" };
CHECK(sports.make() == "SportsCar");
CHECK(sports.engineModel() == "Twin-Turbo-V12");
}
// ══════════════════════════════════════════════════════════════════════════
// SCENARIO G — Two Cars: LIFO stack destruction order
// ══════════════════════════════════════════════════════════════════════════
// When two Cars are declared in the same scope, they destroy in REVERSE
// order (stack / LIFO discipline). Each Car still has its Engine destroy
// AFTER the Car dtor body. Test the combined trace.
{
trace::clear();
{
Car first { "First", "Engine-A" };
Car second { "Second", "Engine-B" };
// Destruction order on scope exit: second then first (LIFO).
}
// Expected trace order:
// [0] "Engine built: Engine-A" — first Car member init
// [1] "Car built: First" — first Car ctor body
// [2] "Engine built: Engine-B" — second Car member init
// [3] "Car built: Second" — second Car ctor body
// [4] "Car destroyed: Second" — second Car dtor body (LIFO)
// [5] "Engine destroyed: Engine-B"— second Car member dtor
// [6] "Car destroyed: First" — first Car dtor body
// [7] "Engine destroyed: Engine-A"— first Car member dtor
CHECK_TRACE(0, "Engine built: Engine-A");
CHECK_TRACE(1, "Car built: First");
CHECK_TRACE(2, "Engine built: Engine-B");
CHECK_TRACE(3, "Car built: Second");
CHECK_TRACE(4, "Car destroyed: Second");
CHECK_TRACE(5, "Engine destroyed: Engine-B");
CHECK_TRACE(6, "Car destroyed: First");
CHECK_TRACE(7, "Engine destroyed: Engine-A");
CHECK_TRACE_SIZE(8);
}
// ══════════════════════════════════════════════════════════════════════════
// SCENARIO H — Driver trace integrity (Task 2 constructor/destructor strings)
// ══════════════════════════════════════════════════════════════════════════
{
trace::clear();
{
Driver charlie { "Charlie" };
}
CHECK_TRACE(0, "Driver built: Charlie");
CHECK_TRACE(1, "Driver destroyed: Charlie");
CHECK_TRACE_SIZE(2);
}
// ══════════════════════════════════════════════════════════════════════════
// SCENARIO I — Mechanic trace (Task 3 constructor/destructor strings)
// ══════════════════════════════════════════════════════════════════════════
{
trace::clear();
{
Mechanic dave { "Dave" };
CHECK_TRACE(0, "Mechanic on duty: Dave");
}
CHECK_TRACE(1, "Mechanic off duty: Dave");
CHECK_TRACE_SIZE(2);
}
// ══════════════════════════════════════════════════════════════════════════
// Summary
// ══════════════════════════════════════════════════════════════════════════
if (!fails)
std::cout << "PASS \xe2\x9c\x85 all garage checks passed.\n";
else
{
if (fails > 0) dumpTrace();
std::cerr << "\nFAIL \xe2\x9d\x8c " << fails
<< " check(s) failed — fix the TASK blocks in garage.cpp.\n";
}
return fails ? 1 : 0;
}
// ============================================================================
// garage.h — PUBLIC INTERFACE for the Garage Simulation (Chapter 23)
// ----------------------------------------------------------------------------
// This header is COMPLETE and PROVIDED. You do NOT edit it. Read it carefully
// before you implement anything in starter/garage.cpp — a header is the
// CONTRACT your implementation must honour.
//
// WHAT THIS FILE DEMONSTRATES (while you read it):
//
// — OBJECT RELATIONSHIP VOCABULARY (23.1):
// Every class below models a distinct relationship; the in-class comment
// above each member calls it out by name. Identify them before coding.
//
// — LIFETIME TRACING (23.2 / 23.3):
// A global Trace log collects "Engine built", "Car destroyed" etc. at
// runtime, making construction/destruction ORDER physically visible.
// The tests assert the exact trace contents — you must get the order right.
//
// — MEMBER CONSTRUCTION/DESTRUCTION ORDER (23.2):
// C++ standard rule: data members construct BEFORE the constructor body
// runs (in DECLARATION ORDER), and destroy AFTER the destructor body
// (in REVERSE declaration order). The trace proves this — be precise.
//
// — SCOPE DISCIPLINE (23.1 — "covers everything except inheritance"):
// Only Chapter ≤ 23 features appear here. Inheritance (Ch 24) and
// virtual (Ch 25) are forbidden.
//
// CS6340/LLVM tie-in: every analysis pass you write in CS6340 mixes these
// relationships exactly:
// std::string name — composition/owned value
// llvm::Function& F — dependency (parameter, borrowed)
// llvm::Instruction* — aggregation/association (not owned)
// Reading the *ownership story* behind a signature is the skill this lab builds.
// ============================================================================
#ifndef GARAGE_H
#define GARAGE_H
#include <string> // std::string — owned, mutable text (Ch 5)
#include <string_view> // std::string_view — cheap, non-owning view (Ch 5)
#include <vector> // std::vector — dynamic sequence container (Ch 16)
#include <initializer_list> // std::initializer_list — brace-init support (23.7)
// ─── Trace Log ───────────────────────────────────────────────────────────────
// A shared append-only log that constructors and destructors write to.
// Using a function returning a static reference lets the log exist exactly
// once across all translation units without a separate global definition.
// (A preview of Ch 7 linkage rules — provided scaffolding, not your task.)
namespace trace {
// Returns the one shared log vector. Append events here.
std::vector<std::string>& log();
// Clears all recorded events — call before each test scenario.
void clear();
}
// ─── class Engine ─────────────────────────────────────────────────────────────
// Engine is a COMPOSED PART of Car (23.2): it is built when Car is built and
// destroyed when Car is destroyed. It has no knowledge of the Car it belongs to.
//
// Relationship: COMPOSITION — Engine is "part of" Car; Car OWNS it exclusively.
// C++ form: Engine is a direct VALUE MEMBER inside Car (no pointer, no new/delete).
class Engine
{
public:
// Constructor: records "Engine built" in trace::log().
explicit Engine(std::string_view model);
// Destructor: records "Engine destroyed" in trace::log().
~Engine();
// Returns the model string passed at construction.
std::string_view model() const;
// Engine is not copyable (it belongs to one Car at a time — composition rule).
Engine(const Engine&) = delete;
Engine& operator=(const Engine&) = delete;
private:
std::string m_model;
};
// ─── class Driver ─────────────────────────────────────────────────────────────
// Driver exists independently of any Car. Multiple Cars could share a Driver.
//
// Relationship: INDEPENDENT ENTITY — aggregated by Car (Car holds a Driver* but
// does NOT own the Driver's lifetime). Destroying a Car must not destroy the Driver.
class Driver
{
public:
// Constructor: records "Driver built: <name>" in trace::log().
explicit Driver(std::string_view name);
// Destructor: records "Driver destroyed: <name>" in trace::log().
~Driver();
std::string_view name() const;
private:
std::string m_name;
};
// ─── class Mechanic ───────────────────────────────────────────────────────────
// Mechanic is never stored — it is only passed to Car::tuneUp() for that call.
//
// Relationship: DEPENDENCY — Car *uses* a Mechanic temporarily (via parameter).
// C++ form: passed by reference to the function that needs it; not stored anywhere.
class Mechanic
{
public:
// Constructor: records "Mechanic on duty: <name>" in trace::log().
explicit Mechanic(std::string_view name);
// Destructor: records "Mechanic off duty: <name>" in trace::log().
~Mechanic();
std::string_view name() const;
// Returns the number of times this mechanic has been asked to tune up a car.
int tuneUpCount() const;
// Called by Car::tuneUp to increment this mechanic's work counter.
void recordTuneUp();
private:
std::string m_name;
int m_tuneUpCount { 0 };
};
// ─── class Car ────────────────────────────────────────────────────────────────
// Car demonstrates THREE relationships at once:
//
// 1. COMPOSITION with Engine:
// m_engine is a VALUE MEMBER (not a pointer). The Engine is built before
// Car's constructor body runs and destroyed after Car's destructor body.
// The trace ORDER proves this — the standard guarantees it (23.2).
//
// 2. AGGREGATION with Driver:
// m_driver is a POINTER. Car holds a reference to an external Driver but
// does NOT create or destroy it. If m_driver is nullptr, the car is
// unoccupied. Destroying a Car must NOT call delete on m_driver (23.3).
//
// 3. DEPENDENCY on Mechanic (via tuneUp):
// Car borrows a Mechanic for one function call only; the Mechanic is NOT
// stored as a member (23.5).
class Car
{
public:
// Build a Car with a given make and engine model.
// Records "Car built: <make>" in trace::log() — AFTER Engine already built.
// m_driver starts as nullptr (no driver yet).
Car(std::string_view make, std::string_view engineModel);
// Records "Car destroyed: <make>" in trace::log() — BEFORE Engine destroys.
// DOES NOT delete m_driver — Car does not own the Driver.
~Car();
// ── Aggregation: assign / query the (non-owned) driver ────────────────────
// setDriver: store the pointer (not a copy, not a new allocation).
// The pointed-to Driver must remain alive as long as this Car uses it.
void setDriver(Driver* driver);
// Returns the current driver pointer (may be nullptr).
Driver* driver() const;
// ── Dependency: borrow a Mechanic for one function call ────────────────────
// Calls mechanic.recordTuneUp() (uses the mechanic), then records
// "Tuned up by: <mechanic.name()>" in trace::log().
// The Mechanic is NOT stored — this is a DEPENDENCY, not an association (23.5).
void tuneUp(Mechanic& mechanic);
// ── Queries ───────────────────────────────────────────────────────────────
std::string_view make() const;
std::string_view engineModel() const; // delegates to m_engine
private:
// ── COMPOSITION member (owns lifetime) ────────────────────────────────────
// Declared FIRST: Engine constructs first, destroys last.
// (C++ constructs members in DECLARATION ORDER; destroys in REVERSE order.)
Engine m_engine;
// ── Owned data ────────────────────────────────────────────────────────────
std::string m_make;
// ── AGGREGATION member (does NOT own lifetime) ─────────────────────────────
// Pointer: may be null; NEVER call delete on this.
Driver* m_driver { nullptr };
};
// ─── class RouteList ──────────────────────────────────────────────────────────
// A value container for a sequence of named waypoints (strings).
// Demonstrates: container class (23.6) + std::initializer_list ctor (23.7).
//
// Relationship: RouteList OWNS its waypoints (VALUE container — copies each
// string in). Destroying RouteList destroys all waypoints automatically (via
// std::vector).
//
// Usage:
// RouteList r { "Home", "Gas station", "Highway", "Destination" };
// r.size(); // 4
// r.at(0); // "Home"
class RouteList
{
public:
// Default: empty route.
RouteList() = default;
// ── TASK 5: implement this constructor (23.7) ──────────────────────────────
// Accepts a brace-list of waypoint names:
// RouteList r { "A", "B", "C" };
// Stores each name in m_waypoints (in order).
explicit RouteList(std::initializer_list<std::string_view> waypoints);
// Number of waypoints.
std::size_t size() const;
// Access the Nth waypoint (0-based). Asserts index < size().
std::string_view at(std::size_t index) const;
// Append one waypoint to the end.
void add(std::string_view waypoint);
// Remove all waypoints.
void clear();
private:
// VALUE container: RouteList owns these strings.
std::vector<std::string> m_waypoints;
};
#endif // GARAGE_H
# Chapter 23 — Object Relationships · Garage Simulation · unit-test grader (Style B).
# Targets follow drills/CLAUDE.md Makefile contract. TABS, not spaces.
#
# The learner implements ../garage.h in starter/garage.cpp (five TASK blocks).
# The grader (tests/tests.cpp) supplies main() and checks trace-log ORDER,
# aggregation isolation, dependency counts, and RouteList initializer_list.
#
# -I. puts the chapter root on the include path so both garage.cpp files can
# write: #include "../garage.h" (same convention as chapter-02 and chapter-08).
CXX := clang++
CXXFLAGS := -std=c++17 -Wall -Wextra -I.
.PHONY: all build run test solution test-solution clean
all: build
# ── build — compile-check the LEARNER's starter (warning-clean) ───────────────
build:
$(CXX) $(CXXFLAGS) -c starter/garage.cpp -o starter/garage.o
@echo "OK \xe2\x9c\x85 starter/garage.cpp compiles. Now run: make test"
# ── run — run the starter's compiled object against a minimal driver ──────────
# (no separate main.cpp needed — the tests binary doubles as the driver)
run: tests/run
./tests/run
tests/run: tests/tests.cpp garage.h starter/garage.cpp
$(CXX) $(CXXFLAGS) tests/tests.cpp starter/garage.cpp -o tests/run
# ── test — grade the LEARNER's code (RED until TASK blocks filled in) ─────────
test:
$(CXX) $(CXXFLAGS) tests/tests.cpp starter/garage.cpp -o tests/run
@./tests/run || echo "FAIL \xe2\x9d\x8c fill in the TASK blocks in starter/garage.cpp until every check passes."
# ── solution — run the REFERENCE solution ─────────────────────────────────────
solution: solution/app
./solution/app
solution/app: tests/tests.cpp garage.h solution/garage.cpp
$(CXX) $(CXXFLAGS) tests/tests.cpp solution/garage.cpp -o solution/app
# ── test-solution — proof the exercise is solvable (MUST be green) ────────────
test-solution:
$(CXX) $(CXXFLAGS) tests/tests.cpp solution/garage.cpp -o tests/run
@./tests/run
# ── clean — remove all build artifacts ───────────────────────────────────────
clean:
rm -f starter/garage.o solution/app tests/run
rm -rf starter/garage.o.dSYM solution/app.dSYM tests/run.dSYM
make test locally
(see “Build & run locally” above).