Skip to content

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

  1. Confirm the stage. Did the compile succeed? If the failure text mentions ld, collect2, or undefined reference, nothing was compiled wrong — something is missing from the program.
  2. Copy the exact symbol out of the message, with its qualifiers: Book::describe() const.
  3. Write two lists.
  4. Declarations: Book.h line 12 — void describe() const;
  5. Definitions: Book.cpp — nothing.
  6. 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

You build the module's header/source example, forgetting a file:

g++ -std=c++17 -Wall -Wextra main.cpp -o book_demo
collect2.exe: error: ld returned 1 exit status

On most GCC installations the same failure prints the name of the missing symbol:

undefined reference to `Book::describe() const'
collect2: error: ld returned 1 exit status

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 collect2 summary. 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, because main is the code that uses the missing function. main is 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:

g++ -std=c++17 -Wall -Wextra main.cpp Book.cpp -o book_demo

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 a const Book, this is a const Book*. You have asked me to call a function that may modify the object. I cannot let you do that without discarding the const you promised.

The technique here is a walk inwards from the call site, and it has three stops:

  1. 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 probably const T& because that is how objects are passed without copying.
  2. Which function is being called, and is it marked const? The note: line names it, with its full signature.
  3. 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:

  1. Is there an accessor? Then use it. Note that the compiler often says so itself, in the third line.
  2. 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.cpp has hasId(int) for precisely this reason: the caller wanted to identify an item, not read an identifier.
  3. 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'

error: redefinition of 'class Book'
note: previous definition 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.

error: expected ';' before '}' token

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.

  1. Copy the file to broken.cpp.
  2. Delete half of it. Compile.
  3. The error is in the half you deleted, or in what remains — now you know which.
  4. 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:

  1. What I expected and what happened (the exact message, not a paraphrase).
  2. The minimal reproduction — the fifteen lines from rung 4, not the whole project.
  3. 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

  1. You can say, before reading a message, which stage failed — and you are right.
  2. 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.
  3. You read the first error and stop. You can say out loud what the compiler could not understand.
  4. You know the two errors that are not mistakes: is private within this context (a design signal) and discards qualifiers (a promise you made).
  5. 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