Module 2 — Debugging¶
Module 1 taught you to tell the three stages apart. Module 2 makes that skill load-bearing: your program is now more than one file, and the errors move across the compile/link boundary, where beginners lose the most time.
This page teaches one technique properly, then gives you a ladder to climb when the technique does not finish the job.
The technique this module adds¶
The declaration/definition check¶
When the compiler reports a missing symbol and the code "obviously exists", find every declaration of that symbol and every definition, and compare the two lists.
It is a diff, not a diagnosis. It takes about thirty seconds, and it finds the answer to the entire class of multi-file errors — which is why it is the technique this module adds rather than a list of tips.
Why it works. In C++ the two words are close enough in English to be confused, and completely different to the compiler:
| Declaration | Definition | |
|---|---|---|
| Says | "this exists, here is its type" | "here is the code" |
| Appears | In a header, in the class body, as a prototype | In exactly one .cpp |
| Checked by | the compiler, in every file that includes it | the linker, once, at the end |
| Missing it means | an unknown name — a compile error | an unkept promise — a link error |
A declaration is a promise. A definition keeps it. The linker is the only component that ever checks whether the promise was kept, and it checks after everything else has succeeded — which is why the error appears so late.
The routine¶
- Confirm the stage. Did the compile succeed? If the failure text mentions
ld,collect2, orundefined reference, nothing was compiled wrong — something is missing from the program. - Copy the exact symbol out of the message, with its qualifiers:
Book::describe() const. - Write two lists.
- Declarations:
Book.hline 12 —void describe() const; - Definitions:
Book.cpp— nothing. - Diff them. Every declared function needs exactly one definition. The gap is the answer, and it is usually one of four things:
| Gap | Fix |
|---|---|
| No definition exists at all | Write it |
| It exists, but in a file not named in the compile command | Add the file |
It exists, but the signature differs (const missing, different parameter) |
Make them match exactly |
It exists, but was written without Book:: — so it declared a free function |
Add the scope qualifier |
That last row is the one that costs people an hour, so it has its own diagnosis below.
The signature must match exactly¶
These are two different functions, and the compiler is entitled to treat them as such:
// Book.h
void describe() const;
// Book.cpp
void describe() { // ✗ no `const` — this declares a DIFFERENT function
std::cout << title_;
}
The linker reports undefined reference to Book::describe() const, and the definition
is right there on the screen looking innocent. The const is part of the type, so it is
part of the name. This asymmetry is exactly why the course writes the header first and
copies each signature when defining it.
Diagnosing it four times, in full¶
Diagnosis A — the link error, whole¶
You build the module's header/source example, forgetting a file:
On most GCC installations the same failure prints the name of the missing symbol:
Two things to note about this, both honest and both useful:
- The wording varies by toolchain. Some MinGW builds (including the one this
course was prepared on) print only the
collect2summary. The shape never varies: the compile succeeded and the link failed. Do not hunt for a particular sentence. - The location in the message is a lie, and a necessary one. The traceback names
main, becausemainis the code that uses the missing function.mainis correct. The mistake is an absent definition.
Run the routine: declarations in Book.h, definitions in Book.cpp, diff → the
definitions file was never given to the compiler. Add it:
And check that the file really contained what you thought. A .cpp you created but
never saved is not a myth; it is the most common cause of this error in a student's own
project.
Diagnosis B — discards qualifiers¶
error: passing 'const Book' as 'this' argument discards qualifiers [-fpermissive]
note: in call to 'void Book::correctPagesTo(int)'
This message is unusual in that it tells you the whole story if you read it as reasoning:
You passed me a
const Book. On aconst Book,thisis aconst Book*. You have asked me to call a function that may modify the object. I cannot let you do that without discarding theconstyou promised.
The technique here is a walk inwards from the call site, and it has three stops:
- Which object is
const? Look at the parameter or variable named in the message. If it is a parameter, its type is in the function's own signature — and it is probablyconst T&because that is how objects are passed without copying. - Which function is being called, and is it marked
const? Thenote:line names it, with its full signature. - Which of the two is wrong? This is a design question with two legitimate answers, and one wrong one:
| Situation | Correct fix |
|---|---|
| The function does not modify the object | Mark it const — the compiler is right |
| The function does modify the object | The parameter should not have been const — change the caller's signature |
| "I know it does not modify, but..." | Almost always the first case, and mutable (Module 4) is the narrow legitimate exception |
Never const_cast. If casting away const makes an error disappear, the error was
about a promise you made, and the cast means you are now lying to the compiler instead
of to the caller — the failure moves from compile time to runtime, which is strictly
worse.
Diagnosis C — is private within this context¶
error: 'int Book::id_' is private within this context
note: declared private here
note: field 'int Book::id_' can be accessed via 'int Book::id() const'
This is not a debugging problem. It is a design signal, and the only mistake available is to "fix" it by making the member public.
Ask in order:
- Is there an accessor? Then use it. Note that the compiler often says so itself, in the third line.
- Is there no accessor? That is a decision someone made, possibly you. Before
adding one, ask what the caller is actually doing, and whether the class should
offer that operation instead.
examples/module-02/01-book-class/main.cpphashasId(int)for precisely this reason: the caller wanted to identify an item, not read an identifier. - Is the code you are writing part of the same concept as the class? It might
belong inside the class, as a member function. It is occasionally the right answer to
make a function a
friend(Module 4), and it is a real coupling cost, so do it deliberately.
Diagnosis D — redefinition of 'class Book'¶
The class was pasted into one translation unit twice. Causes, in order of likelihood:
| Cause | Check |
|---|---|
The header has no #pragma once (or include guard) |
First line of the file |
The class was defined in a .cpp and a header |
Search the project for class Book — it must appear once |
| A definition was written inside another header that several files include | Same search |
The same header is included via two paths (e.g. ./Book.h and Book.h) |
Consistent include style in the project |
Because #include is a paste operation, this is exactly what you would expect if you
pasted the file twice by hand — which is a good mental model for what the preprocessor
does, and why the guard exists.
The escalation ladder¶
When the technique above does not finish the job, climb. Each rung answers a specific question, and the rule is: do not climb until the rung below has been ruled out. Most time lost in debugging is spent on rung 6 while the answer sat on rung 2.
Rung 1 — Read only the first error¶
Compilers cascade: one real error produces ten follow-on messages. Fix the first, then recompile. If you are reading the fifth error before the first is fixed, you are reading fiction — the later errors are about code the compiler could not fully understand.
Rung 2 — Go to the exact line the message names¶
Not the line you think it means. Compilers report the line where they gave up, which is usually one line after the mistake. So look at the named line, then at the previous one.
Nine times out of ten the missing semicolon is on the line above.
Rung 3 — Run the declaration/definition check¶
This page's technique. Applies to anything involving a missing name, especially link errors.
Rung 4 — Delete code until it compiles¶
The minimal reproduction, and the fastest tool in the whole ladder for anything non-obvious.
- Copy the file to
broken.cpp. - Delete half of it. Compile.
- The error is in the half you deleted, or in what remains — now you know which.
- Repeat until you have fifteen lines that reproduce the error and nothing else.
The error is often self-evident at fifteen lines. And when it is not, you now have something to ask a human about — which is rung 7.
Rung 5 — Ask the compiler a different question¶
Same code, different information:
g++ -std=c++17 -Wall -Wextra -c main.cpp # compile only: is this a link error?
g++ -std=c++17 -Wall -Wextra -E main.cpp | less # preprocessed output: what did #include paste?
g++ -std=c++17 -Wall -Wextra -fsyntax-only main.cpp
clang++ -std=c++17 -Wall -Wextra main.cpp -o a.out # a second opinion; different wording
-E is genuinely revealing with headers: it shows the pasted result, which is how you
see a header included twice, or a macro that changed the code you wrote.
Rung 6 — Explain it out loud¶
Say, in words, what you believe each line does, to an empty room or a rubber duck. The moment you hear yourself say "and then it... hmm" you have found it.
This is not a joke technique. It works because it converts pattern-matching (which is fast and silent) into articulation (which is slow and explicit), and the bug is usually in the part you had been skipping silently.
Rung 7 — Ask somebody, with a prepared question¶
A good question contains, in this order:
- What I expected and what happened (the exact message, not a paraphrase).
- The minimal reproduction — the fifteen lines from rung 4, not the whole project.
- What I have already ruled out, and how.
You will solve perhaps half of these yourself while preparing the question. The other half receive an answer in minutes, because the reader does not have to guess what you did.
Rung 8 — When it is not a compiler error¶
Some problems produce no message at all. Module 1's distinction applies:
| Symptom | The likely layer |
|---|---|
| Wrong output, everything compiled | your logic — add temporary output to see actual values |
| Crash, "segmentation fault", nothing else | memory or an invalid pointer — Modules 10 and 11 |
| A program that "works" but the output changes on another machine | undefined behaviour — the same modules |
| Nothing happens at all | the program ran and exited: check that your main did something, and that a loop's condition was not false from the start |
The reason the course compiles with -Wall -Wextra and treats warnings as errors is
that most of that last category is announced at compile time as a warning, and
warnings are the only messages most people learn to ignore.
A debugging log, and why to keep one¶
Two or three lines per problem, written as you go. Not for the exercise — for the sixteenth week, when the same error appears in a bigger program and you want to know how you solved it last time.
### Link error: undefined reference to `Catalogue::find(int)`
- Stage: link (compile was clean)
- Symbol: Catalogue::find(int) ← note: NOT const
- Declared: library.h, `Book* find(int);` — non-const ✓
- Defined: library.cpp — defined as `Book* Catalogue::find(int id) const` ← there it is
- Fix: removed the stray `const` from the definition
- Time: 4 minutes. Checked the definition *last* — should have been first.
The last two lines are the ones that change behaviour. A log that records how long something took, and what you looked at in the wrong order, turns into a better search order next time.
What good looks like, at this point in the course¶
- You can say, before reading a message, which stage failed — and you are right.
- You type the compile command, not a remembered one. Half of all link errors are a missing filename, which you can only notice by seeing the command.
- You read the first error and stop. You can say out loud what the compiler could not understand.
- You know the two errors that are not mistakes:
is private within this context(a design signal) anddiscards qualifiers(a promise you made). - You have broken one of your own classes and reproduced each error above deliberately. Exercise C2 asks for exactly that, and it is the cheapest way to make these messages familiar before they cost you an evening.
Next: Exercises — three tiers, each stating its level and what success looks like. · All modules