Debugging Checklist¶
A procedure, in order. The point of a fixed order is that it stops you from changing code at random and calling it debugging — which is what most beginners do, and why a bug that should take twenty minutes takes three evenings.
Pairs with Module 16 — Advanced OOP Capstone · Companions: Common Compiler Errors · Common OOP Bugs
The loop¶
1 reproduce → 2 shrink → 3 locate → 4 understand → 5 fix → 6 verify → 7 prevent
↑ │
└────────────────────── one variable changed at a time ────────────────────────┘
Never skip step 1 or step 6. A bug you cannot reproduce is a bug you cannot verify you fixed, and a fix you have not verified is a hope.
1. Reproduce¶
- [ ] Write down the exact command, input and build flags that show the bug.
- [ ] Is it always reproduced, or sometimes? If sometimes, make it always before you do anything else — see intermittent bugs below.
- [ ] Turn on every diagnostic:
-std=c++17 -Wall -Wextra -Werror. - [ ] Write a failing test first, if you have a test suite. It is the only way to know later that the fix worked and stayed working.
2. Shrink it¶
The fastest route to a C++ bug is deleting code until the bug is obvious.
- [ ] Comment out half the body and re-run. Still broken? The cause is in the remaining half.
- [ ] Delete unrelated features, then unrelated classes, then unrelated files. Keep deleting while the bug survives.
- [ ] Reduce data: one element instead of a hundred, one line instead of a file,
2instead of a computed value. - [ ] If the program is multi-file, bisect the link: does the bug survive without that translation unit?
- [ ] Stop when the remaining code is smaller than your tolerance for reading it. That is usually under twenty lines.
A minimal reproduction is also the thing to attach when you ask for help. Nobody can help with "my project doesn't work".
3. Locate¶
Print with a reason, and flush¶
- [ ] Put markers at the boundaries of the suspect region, then halve the interval repeatedly.
- [ ] Flush. Output is buffered, so a crash loses whatever was printed but not flushed. The absence of a marker does not prove the program never got there — see Common OOP Bugs §4.
- [ ] Print values and invariants, not "here":
"balance=" << a.balance() << " expected>=0\n".
Count objects when the question is about lifetime¶
- [ ] Add a counter to the constructor and destructor of the suspect type, and print it at the end. Not zero? Something was never destroyed, or was destroyed twice.
- [ ] Add a print in each destructor, temporarily, to see the order.
Read the diagnostic properly¶
- [ ] Compile errors: fix the first one only, then rebuild. Later errors are often its consequences.
- [ ] Read every
note:line — the fix is usually in one. - [ ] Template errors: read
[with T = ...]and therequired from hereline; the error is in the body, the cause is the type. - [ ] Link errors: no file and no line, by design. Use
nm -C— see Common Compiler Errors.
Assert what must be true¶
An assertion is a claim the compiler checks for you at run time. It is the cheapest way to find the moment an invariant broke rather than the place it was noticed.
4. Understand before you change¶
- [ ] State the hypothesis in one sentence: "the destructor is not virtual, so the derived member is leaked."
- [ ] Say what you would see if it were true, and what you would see if it were false.
- [ ] Test the falsifying case, not the confirming one. Confirmation is cheap and misleading.
- [ ] If you cannot write the hypothesis, you are still in step 3. Keep shrinking.
5. Fix¶
- [ ] Change one thing. Two changes and a working program teach you nothing.
- [ ] Fix the cause, not the symptom. Guarding a call site against a value that should never exist hides the bug that produces it.
- [ ] Resist the defensive patch.
if (p != nullptr)in five places is a sign the ownership design is wrong, not that five checks were missing. - [ ] If the fix is "add a virtual destructor", "invert the ownership", or "split the class", it is a design fix — make it properly, and update the diagram.
Two changes at once
If you change your fix and add an experiment in the same edit, and the program starts working, you cannot say which one did it — and you have silently kept the other. Revert the experiment, rebuild, and confirm the fix still holds on its own.
6. Verify¶
- [ ] The minimal reproduction now behaves correctly.
- [ ] The whole test suite passes — not only the test you were staring at.
- [ ] The failure path behaves as designed, not only the success path.
- [ ] Still no warnings with
-Wall -Wextra -Werror. - [ ] A rebuild from scratch works (
g++on every source, not just the one you edited). - [ ] Keep the new test, forever. It is now a regression test.
7. Prevent¶
- [ ] Write one line in your mistakes file: symptom → cause → the rule that would have prevented it.
- [ ] Run the five suspects below in order next time.
- [ ] If the bug took more than half an hour, ask what in the design made it findable — or not.
The five suspects, in order¶
- Lifetime — is anything used after it was destroyed, or destroyed twice?
virtual— is a polymorphic base missing a virtual destructor, or is a function missingvirtual/override?- Copying — does an owning class rely on a compiler-generated copy?
const— is a non-constmember called on aconstobject, or is the wrong overload chosen?- Ownership — does anything own what it should not, or refer to something that outlived it?
Then, if all five are clean: is the same fact stored in two places? That is the sixth suspect, and it is usually an invariant waiting to be broken.
Intermittent bugs¶
A bug that appears sometimes is usually one of these. Check in this order:
| Cause | Test |
|---|---|
| Uninitialised value | Zero-initialise everything; does it stop varying? |
| Undefined behaviour | Remove the "clever" part; does the symptom move or vanish? UB can look like luck |
| Iteration order of an unordered container | Sort before processing; is it stable now? |
| A dangling reference/pointer that usually lands on valid memory | Add a destruction counter; does the object outlive its user? |
| Unflushed output lost on a crash | Flush after every marker; does an extra line appear? |
| Time or randomness | Inject the clock; seed the generator — the projects require both for exactly this reason |
| Stale build | Delete the object files and rebuild everything |
Asking for help, properly¶
Anyone — a teacher, a classmate, a forum — can only help with a specific question. Write the handover first; you will often solve it while writing.
What I am trying to do: (one sentence)
The smallest program that shows it: (code, or the file and function)
The exact command: g++ -std=c++17 -Wall -Wextra -Werror a.cpp -o a
What I expected: (the exact output or behaviour)
What happened: (paste the output, the message, or the exit status)
What I have already ruled out: (and how I ruled it out)
My current hypothesis: (one sentence)
Rubber-duck first. Explaining the problem out loud, to a person who knows nothing about it, forces the reduction you should have done anyway. A great many bugs do not survive the explanation.
Tools, and what this toolchain gives you¶
| Tool | Use | Note |
|---|---|---|
g++ -Wall -Wextra -Werror |
Catches most defects before you run anything | Always on |
assert |
Finds the moment an invariant broke | Disabled if NDEBUG is defined |
| Live-object counters | Finds leaks and double frees without extra tools | Portable; the reliable option here |
Marker prints with std::flush |
Finds where | Cheap, and still the most-used technique |
nm -C file.o |
Finds a missing or duplicated symbol | The only way to read this toolchain's link failures |
| AddressSanitizer / Valgrind | Finds memory errors automatically | If your toolchain supports it; the sanitizer build does not link on some MinGW installations, so keep the counter technique |
The five-minute triage for a project that "just doesn't work"¶
- Does it build with no warnings? If not, stop: fix that first. Half of "doesn't work" is a warning being ignored.
- Which stage fails — compile or link? Compile names a line; link names nothing, so use
nm -C. - Does it run and do nothing? Add a flushed marker at the top of
main. - Does it crash? Get the exit status, then add markers to find the last thing that ran.
- Does it run and misbehave? Print the invariant at the end and work backwards to the first moment it was false.
Related: Common OOP Bugs · Common Compiler Errors · Reading Compiler Errors · Study Plan · Module 16 — Advanced OOP Capstone