Module 9 — Debugging¶
Module 8's technique audited an interface's promises. This module's technique audits something the compiler never checks: who was supposed to delete it.
The technique this module adds¶
The ownership map. Before you debug behaviour, draw the objects and mark every arrow with one of four facts: owns (filled diamond), borrows (hollow diamond or plain line), uses for one call (dashed arrow), and — in words — how long each one lives. Three lines per relationship:
Registration ──◆ StudentStore Registration owns it; created in its constructor,
destroyed with it.
Registration ──◇ Notifier borrows; supplied at construction; must outlive
Registration: the caller's job.
Report ┄┄> Formatter used during render(); nothing stored; no lifetime rule.
Then ask the two questions in this order, because the second is unanswerable without the first:
- For each non-owning member: what guarantees it is still alive when I use it? If the answer is "nothing, it is just a pointer I was given", the design has an unstated requirement, and you have found where the bug will be.
- For each owned member: is it destroyed after everything that refers to it? Members destruct in reverse declaration order, so a member that refers to a later member is destroyed after the thing it refers to — which is fine for destruction, and a real bug for any use that happens later in the outer destructor body.
The map takes five minutes and it is the only debugging technique in this course that works before a symptom appears: an unmarked arrow is a question not yet answered, and the lifetime line is the answer somebody has to write.
A worked diagnosis¶
Symptom. A term report prints an empty attendance table. The roster has six students when the report is constructed, and the same roster has six students when the report is printed.
First hypothesis. The report's loop is wrong. Cheap to check, and wrong: it is a
for (const auto& student : roster_) over the same Roster type the working tests use.
Test. Print the size from inside the report, at both ends of its life:
std::cout << "constructed with " << roster_.size() << " students\n"; // 6
…
std::cout << "rendering " << roster_.size() << " students\n"; // 0
Six, then zero. The report's data did not change — its source did.
Result. The report does not hold the roster. It holds a snapshot taken at construction, and then a second roster was swapped in, leaving the report describing the old one:
class Report {
public:
explicit Report(const Roster& roster) : snapshot_(roster.snapshot()) {} // a copy, early
private:
std::vector<Student> snapshot_;
};
Actual cause. A lifetime decision that was never written down. snapshot() is not a bug —
it is a deliberate access form, and Module 9's Example 4 teaches when it is right (a caller
that needs data to outlive the owner). Used as a member, it means "this report describes the
roster as it was when the report was built", which was not the intent. Six students at
construction, zero at print time, and no compiler complaint: a copy has no lifetime problem.
Fix. Two defensible repairs, and the right one depends on what the report is:
class Report {
public:
explicit Report(const Roster& roster) : roster_(roster) {} // borrow: must outlive us
std::string render() const { return roster_.size() ? "…" : "no students"; }
private:
const Roster& roster_; // the report is a view; the caller owns both
};
or keep the snapshot and make the name tell the truth — Report::frozenAt, printed with the
time it was taken. What is not acceptable is the original: a member whose name says "roster"
and whose type says "copy".
Lesson. When data looks stale or empty, ask whether you are holding the thing or a copy of it, and write the answer into the declaration. The compiler cannot help with that question, and neither can a debugger.
The four silent failures¶
Eliminate these before you debug anything else. Each produces correct-looking code.
- A borrowed member outliving its owner. The arrow has no lifetime line. Fix: own it,
document the ordering, or use
weak_ptr(Module 11). - A copy where a reference was meant (the diagnosis above). Fix: name the member for what it holds, and decide staleness deliberately.
- A reference handed out to a member's internals. Example 4's
students()accessor. Fix: capabilities, not containers. - A second door to a stated invariant. An inherited container, a
friend, or an accessor. Fix: enumerate the ways to change the object, and close the ones the class cannot police.
The escalation ladder¶
- Read the
In file included fromlines first. In this module they are a stack trace of the include cycle, and they name the file whose guard fired early. - Check the declaration above the error.
field 'x' has incomplete typemeans a value member was declared from a forward declaration;use of deleted functionon assignment means a reference orconstmember made the class non-assignable. - Reduce the problem until it builds, then restore it piece by piece. Two classes that hold each other, one value member, one pointer member: every failure in this module reproduces in twenty lines.
- Draw the ownership map and mark the lifetimes. Every unmarked arrow is a question.
- For anything that looks empty or stale, print the size (or the value) at both ends of the object's life. That distinguishes "wrong container" from "copy taken earlier".
- Look for the second door. If an invariant is broken, list the ways the object can change, not the ways you remember writing.
- Ask "who was supposed to delete it?" out loud. If two answers are possible, the design has two owners and the bug is not in the code — it is in the diagram.
- Then consult a solution, and rewrite your own version afterwards.
Tools introduced here¶
| Tool | What it shows | How |
|---|---|---|
| The ownership map | Who owns what, and for how long | Four arrow kinds and one lifetime line per relationship, drawn before coding |
| Constructor/destructor counters | Which object was destroyed, and when | static int alive; incremented in the constructor, decremented in the destructor — as in examples/module-09/01-… |
| Printing a size at both ends | Whether you hold the object or a copy | One line at construction, one at use |
-Wreorder |
An initialiser list that disagrees with declaration order | In -Wall; promoted to an error by the course's flags |
grep -n "&\s*$" on public members |
Reference returns, including container accessors | Then ask what a caller can do with each one |
grep -n "In file included from" |
The include stack, printed with any cycle error | Read it top-down to find the cycle |
Reading a design you did not write¶
Five minutes, in this order:
- List the members with their types. Value, pointer, reference, container — that is a first draft of the ownership map.
- For each non-owning member, find the constructor that sets it. Is it a parameter? Then the caller owns it, and the requirement is "must outlive this object".
- Look for
snapshot,copy,clonein member initialisers. Each one is a decision that the data will not be live. - List every public operation that can change the object, including anything returning a
non-
constreference. Each one has to maintain every stated invariant. - Check the destruction order against the declarations. A member that refers to a later member is destroyed before it, and any use in the destructor body that follows is a bug waiting for a bad day.
Next: Exercises — three tiers, including a design to audit for ownership, and a class to break on purpose.