Module 6 — Debugging¶
Module 5's debugging page was about testing a relation — does your comparator
behave like <? This module's technique is about drawing before reading.
The technique this module adds¶
The subobject map. Before you read a single line of a hierarchy's behaviour, draw three things on paper:
- The boxes. Which classes exist, and what data each one holds.
- The arrows. Which class derives from which, and in what order the bases are declared.
- The inside of one object. For the object you are about to watch, write the subobjects in the order they will be constructed, top to bottom, and mark where the members of each class sit.
Almost every hierarchy bug is a claim about that drawing being wrong. Three questions, and between them they account for nearly everything:
| Question about the map | If the answer surprises you, the bug is |
|---|---|
How many copies of Base's state are inside this object? |
A diamond — the object contains more than you think |
| Which class's function runs, given the expression's static type? | Hiding, or a non-virtual function, or a call on an object rather than a reference |
| Which line runs first — and does it use anything not yet built? | Construction order: a base or member used before it exists |
The map is cheap (two minutes with a pencil) and it moves the question from the
program's text to the object's structure, which is where the answer usually is.
Debugging by printing std::cout into a hierarchy without a map produces
contradictory evidence, because the print statements themselves change the ordering
story you are trying to read.
A worked diagnosis¶
Symptom. A colleague's file stops compiling the day a new feature was added. Nothing in their file changed.
The call worked last week. The class it calls into gained one function.
First hypothesis. The int overload was deleted from Printer. Cheap to
check, and wrong.
Test. Read Printer.h — all three overloads are still there, and one added
virtual before them (Module 7's change). Then make the call explicit:
Result. It compiles. The overload was never deleted; it is unreachable from a
ColourPrinter — and the reason is now visible in the map: ColourPrinter declares
print(std::string, bool), and one declaration of a name in a derived class hides
every base declaration of that name.
Actual cause. Name hiding in ColourPrinter (§6 of
Concepts). The new feature did not break the call; the derived
class's declaration did. The colleague's file was the messenger.
Fix. One line in the derived class, which fixes every call site rather than this one:
class ColourPrinter : public Printer {
public:
using Printer::print; // restore the whole overload set
void print(const std::string& label, bool colour) const;
};
Lesson. When a working call site stops compiling and its file did not change, the change is in a header, and the question is what that header now hides. Read the error as a question about lookup, not about the function that "disappeared".
Rule out the three silent failures first¶
Three defects in this module produce no message at all. They are worth eliminating before you debug anything else, because evidence collected in their presence is unreliable.
- The slice. Anything copied into a base-class object keeps only the base
part.
Employee e = salaried;compiles, andeis not aSalariedEmployee. Look for base-class objects (not references, not pointers) as parameters, members and containers: - The hidden overload.
-Woverloaded-virtualin the course's compile command reports a derived function that hides a virtual base function. If a call reaches the wrong function while compiling cleanly, this is usually why. - The duplicated subobject. The diamond has no diagnostic. Ask the map
question: how many copies of the shared base does this object contain? If the
answer is two and you expected one, stop and decide deliberately —
virtualinheritance for interfaces, an association for state.
The escalation ladder¶
- Read the first error, including any
note:line. In this module thenote:is often the answer: it names the base constructor you should have called, or the member you may not reach. - Check the line above the reported one. A missing base initialiser is reported at the derived constructor's opening brace; the omission is in the line before.
- Reduce the problem until it builds, then restore it piece by piece. A three-level hierarchy reduced to two levels usually makes the fault obvious, because one of the three rules of §4 stops applying.
- Draw the subobject map. Which classes, which members, how many copies, what order. Do it before adding another print statement.
- Check this module's common mistakes table, then the glossary for the words you are using loosely.
- Ask the machine. Compile with the course's full warning set; run under
-fsanitize=addresswhen the symptom is corruption rather than a wrong branch; addTracermembers to print the construction and destruction order. - Restate the problem in words, without code — "a
Manageris being destroyed, and the line that fails runs beforeEmployee's destructor" is already half the diagnosis. - Then consult a solution, and rewrite your own version afterwards.
Tools introduced here¶
| Tool | What it shows | How |
|---|---|---|
The Tracer member |
The construction and destruction order of one object, printed as it happens | A member whose constructor and destructor print, as in examples/module-06/03-construction-and-destruction-order/main.cpp |
-Wreorder |
An initialiser list that disagrees with declaration order | Already in -Wall; the course adds -Werror |
-Woverloaded-virtual |
A derived function that hides a virtual base function | Add to the compile command when a call reaches the wrong function |
| The qualified call | Whether a name is hidden or genuinely absent | obj.Base::f(); compiles ⇒ hidden; fails ⇒ absent |
using Base::name; |
The fix for hiding, applied to the whole overload set | One line in the derived class's public section |
-fsanitize=address |
Use-after-free and buffer overruns, including across a bad hierarchy cast | g++ -std=c++17 -g -fsanitize=address main.cpp -o main |
Reading a hierarchy you did not write¶
Five minutes, in this order, and you will know more about it than most of its callers:
- Draw the boxes and arrows (§1 above).
- Mark each member's access level:
-,#,+. - List the
virtualfunctions. If an object can be deleted through a base pointer, check for a virtual destructor — a missing one is Module 7's headline bug. - List what each derived class hides rather than overrides: same name, different signature.
- Ask of each inheritance arrow: is every public operation of the base true of the derived type? The first "no" is where the design will hurt.
Next: Exercises — three tiers, including a hierarchy whose initialisation order you must predict before running.