Skip to content

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:

  1. 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.
  2. 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.

  1. A borrowed member outliving its owner. The arrow has no lifetime line. Fix: own it, document the ordering, or use weak_ptr (Module 11).
  2. A copy where a reference was meant (the diagnosis above). Fix: name the member for what it holds, and decide staleness deliberately.
  3. A reference handed out to a member's internals. Example 4's students() accessor. Fix: capabilities, not containers.
  4. 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

  1. Read the In file included from lines first. In this module they are a stack trace of the include cycle, and they name the file whose guard fired early.
  2. Check the declaration above the error. field 'x' has incomplete type means a value member was declared from a forward declaration; use of deleted function on assignment means a reference or const member made the class non-assignable.
  3. 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.
  4. Draw the ownership map and mark the lifetimes. Every unmarked arrow is a question.
  5. 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".
  6. Look for the second door. If an invariant is broken, list the ways the object can change, not the ways you remember writing.
  7. 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.
  8. 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:

  1. List the members with their types. Value, pointer, reference, container — that is a first draft of the ownership map.
  2. 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".
  3. Look for snapshot, copy, clone in member initialisers. Each one is a decision that the data will not be live.
  4. List every public operation that can change the object, including anything returning a non-const reference. Each one has to maintain every stated invariant.
  5. 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.