Skip to content

Module 8 — Debugging

Module 7's technique was the declared-type test on call sites. This module's is about the interface itself, because that is where an abstract class fails: not at the call, and not at the object, but in the contract that was written either too wide or too late.

The technique this module adds

The contract audit. Two columns, one line per function of the interface, written before you debug anything:

function                can EVERY implementation honour it, without lying?
----------------------  ------------------------------------------------------
send(to, message)       yes — three implementations, including a recorder
                        (a recorder that records "nothing" is still truthful)
name()                  yes — it is a string every implementation has
scan()                  no — the receipt printer has no scanner

The rule the audit applies: the first "no" is the bug. Not a bug in an implementation — a bug in the interface. Everything downstream of it is a symptom: a function that throws, an implementation that returns false for "unsupported", a caller reaching for dynamic_cast, or a switch on the concrete type that reappears three files away.

The audit takes ten minutes and is usually faster than reading the failing implementation, because it tells you which implementation is being forced to lie.

The second half: the abstractness audit. When something will not compile because a type is abstract, read the note: lines before anything else — they are a checklist the compiler wrote for you:

note:   because the following virtual functions are pure within 'WrongConst':
note:     'virtual double Shape::area() const'

Note that the note names the base's declaration. That is the fastest way to find a missing or misspelled override in a hierarchy you have not seen: it tells you the exact name, parameter list and const-ness you failed to match.

A worked diagnosis

Symptom. A new ReceiptPrinter implementing the Printer interface compiles, and every call to scan() returns false. The reviewer says "that is a bug in ReceiptPrinter".

First hypothesis. ReceiptPrinter::scan() has a logic error. Cheap to check, and wrong: the function is three lines long and returns false unconditionally, which is what the symptom says it does.

Test. Run the contract audit on Printer instead of reading ReceiptPrinter:

function        honest implementation possible?
--------------  -----------------------------------------------------------
print(line)     yes
flush()         yes
scan()          no — a printer with no scanner
staple()        no
email(dest)     no

Result. Three of the five functions cannot be honoured. ReceiptPrinter is not defective; it is being asked to keep promises it cannot keep, and return false is the interface silently accepting a lie.

Actual cause. A wide interface: five capabilities in one contract, where the only relationship between them is that one printer model happened to have all five. The implementations that could lie did; the ones that could not threw.

Fix. Split by capability, and let callers depend on the smallest one they need:

class Printable { public: virtual ~Printable() = default; virtual void print(const std::string&) = 0; };
class Scannable { public: virtual ~Scannable() = default; virtual Image scan() = 0; };
// ReceiptPrinter : public Printable        ← implements what it can honour
// OfficePrinter  : public Printable, public Scannable

The consequence to state out loud: a caller that needs both capabilities now names both types, and at a boundary where the concrete type is unknown it must ask — dynamic_cast — or be given a combined interface (class AllInOne : public Printable, public Scannable). That is a real cost, and choosing between it and the lying implementation is the decision the audit exists to inform.

Lesson. When an implementation "cannot" do something an interface requires, ask the audit question before debugging the implementation. Three unsupported functions is not three bugs; it is one contract that was written too wide.

The four failure modes, and what each looks like

Failure What you see What it means
A forgotten override cannot declare variable … to be of abstract type, with a note: listing the pure function The class never became concrete
A mismatched signature A hidden [-Woverloaded-virtual] warning at the declaration and an abstract-type error at the use site Two messages, two locations, one const
A wide interface Implementations that throw, return false, or do nothing The contract asks for capabilities that do not belong together
A missing definition A link error, which may name no symbol at all A pure virtual destructor was declared and never defined

Three of those four are detectable without running the program: the first two by the compiler, the third by the audit, and the fourth by grepping for = 0 on destructors.

The escalation ladder

  1. Read the note: lines first. In this module they are the diagnosis, not an afterthought: they name the pure functions that are still unimplemented, and the base function your signature failed to match.
  2. Ask which line the error is on. An abstract-type error at a declaration means a forgotten or mismatched override; at a use site (new, a variable definition, a by-value parameter) it means the type is abstract where a complete object was needed.
  3. Reduce the problem until it builds, then restore it piece by piece. Two classes, one pure virtual function, one derived override: every failure mode in this module reproduces in twenty lines.
  4. Run the contract audit on any interface whose implementations are lying, throwing or returning a bare false.
  5. Add override everywhere and rebuild — it converts the two-location signature problem into one compile error at your line.
  6. Check the destructors. grep '= 0' over the abstract bases: a pure virtual destructor without a definition is a link error waiting.
  7. Restate the problem in words without code — "a receipt printer is being asked to scan" is the whole diagnosis, and it took a sentence.
  8. Then consult a solution, and rewrite your own version afterwards.

Tools introduced here

Tool What it shows How
The note: lines Which pure virtual functions are unimplemented, and which base function a signature failed to match Read them first; they name the base declaration
override That your function really overrides, at the declaration Write it always; it collapses two distant messages into one
The contract audit An interface that lets implementations lie Two columns, one line per function, before you debug any implementation
grep -n '= 0' on abstract bases Pure virtual functions, including destructors that need a definition grep -rn "= 0;" include/
-Woverloaded-virtual The hidden pure function behind an abstract-type error In the course's warning set; the hidden warning is the clue
dynamic_cast — as a diagnostic That a caller needs to know the concrete type behind an interface If you need it to make the program work, the audit has found something

Reading an interface you did not write

Five minutes, in this order:

  1. List its functions. Count them. More than three or four is a suspicion, not a verdict.
  2. For each one, name the implementations that could not honour it. That is the audit.
  3. Find the ownership. Does the interface have a virtual destructor? Who deletes the objects? If the answer is "whoever allocated them", Module 11's question has been left open — say so.
  4. Look for a type query. A dynamic_cast, a kind() function, or a typeid comparison in a caller is the interface telling you it is missing an operation.
  5. Check the depth. A stateful base at the top, another below it, and an interface bolted on the side is normal. Four stateful levels are not; apply §8 of Concepts.

Next: Exercises — three tiers, including an interface that must be split, and an abstract base with a missing definition to find.