Debugging — 8 seeded hunts

← Module home · For each program: predict, run, explain the gap, then name the practice that prevents the whole class. Diagnoses in a separated section.


Diagnoses

D1. auto total = 0; deduces int. Three million marks averaging ~500 → a sum near 1.5 billion — inside int’s range until the unlucky semester: overflow is undefined behaviour, and on typical machines the total wraps negative, producing nonsense averages for large-but-legal inputs. The violated rule: name the type when the type carries meaning — this variable’s whole job is “sum that must not overflow”: long long total = 0; (or the seed-type lesson: accumulate(marks.begin(), marks.end(), 0LL)). auto on a numeric literal is exactly where the rule bites.

D2. After move(body), body is empty-but-alive — the audit line logs an empty string: the “audit trail” is a lie, which is this module’s silent-failure species in modern clothing. The rule broken: after `std::move(x), treat x as gone — no reads. Fix: log before the move, or log the destination:

string body = collectData();
log.append(body);          // audit BEFORE stealing
string out = move(body);   // now transfer
return out;                // (the return itself would move anyway)

D3. A use-counted cycle: every node is kept alive by its two neighbours’ shared_ptrs; the ring’s counts never reach zero, so clear() (which drops the map’s references) leaves the circle counting itself — immortal. The trap: shared ownership between mutually-referring nodes is a keep-alive circle. The redesign in one sentence: the ring structure itself owns the nodes (vector<Node> or unique_ptr chain), and neighbour links become raw non-owning views — one owner, views for the rest (exercise M8’s verdict, in the wild).

D4. Leak one: the !in path returns nullptr while out’s heap vector is abandoned — leaked. Leak two, structural: even the happy path forces every caller to delete the returned pointer and to remember that nullptr means “no file” — an ask-door bolted onto an ownership obligation. The modern rewrite deletes the whole bug class:

vector<Record> load(const string& path) {          // by value: the move is automatic
    ifstream in = openOrThrow(path);               // Robustness module: loud open
    vector<Record> out;
    string line;
    while (getline(in, line)) out.push_back(parse(line));
    return out;
}

No owner to forget, no null to test, and the missing-file policy moved to the layer that can act.

D5. Original: takes vector<int> by value (so the caller’s data was safe by copying) but mutates its own copy mid-computation — a surprise to every reader who trusted “stdDev computes; it does not transform,” and a hidden dependency if spread is later given the caller’s view. The reviewer’s const vector<int>& turned the hidden surprise into a compile error — the const correctness payoff: intent, checked. Correct design: the function is not a pure stdDev — it is “centre then spread”: either split it (centred() returns a new vector; stdDev stays pure) or rename honestly (centredStdDev) and document the transform. The practice: const-first on parameters makes the compiler the reviewer.

D6. 0 and NULL are integer-ish literals; overload resolution for onTimeout(0) happily picks onTimeout(int) — the “no callback” intent silently becomes “timeout code zero” (this module’s most modern bug: type confusion the language used to permit). The two-word rule: nullptr only — it has no integer conversion and always names the pointer overload. The archaeology lesson: the codebase’s mixed 0/NULL/nullptr spellings are a bug reservoir; the modern idiom is a retirement, not a style choice.

D7. Useless — and the honest answer is that the compiler already does it. return decorated; from a local is automatically moved (or elided) — the explicit move adds nothing and, worse, suppresses the NRVO-shaped elision opportunities in some compilers, plus it signals “I needed to know about moves” to every reader. The general rule: never std::move a return of a local — return it plainly; reserve move for transferring named variables you are done with into containers, members, or parameters. (getName’s const& return stays the right design for the member itself.)

D8. readConfigInt reads a file at run time — a constexpr variable must be computable at compile time, so the definition is a compile error (the compiler refusing the lie). The distinction proved: const = unchanged after init (any run-time value allowed); constexpr = usable at compile time (compile-time value required). A config-file value belongs to run time: const int rows = readConfigInt("rows"); with a run-time-shaped container (vector<int> grid(rows) — or the validated range check). The deeper habit: compile-time constants are for shapes and invariants the code owns; user configuration is data, and data is checked at run time — with the Robustness module’s guards.


Fix-list recap

Hunt Bug class The practice that prevents the class
D1 auto overflow name the type when meaning rides on it
D2 read after move moved-from is gone — audit first
D3 shared_ptr cycle one owner; neighbours are views
D4 raw-owner leak on early exit containers/unique_ptr own; value returns
D5 hidden mutation const-first parameters
D6 0/NULL overload confusion nullptr only
D7 pointless return-move return locals plainly
D8 constexpr on run-time data const vs constexpr; config is run-time