C++ Part I · Module 2 · Building and Running C++ Programs
Reading Compiler and Linker Errors
You now know what the four stages of a build are and what each one does. What you do not have yet is a routine — a way to take a message you have never seen before, work out which stage produced it, and know which assumption to check first. That routine is worth more than any list of individual error messages, because the list will never be complete and the routine always applies. It also addresses the most expensive habit a beginner has: reading a message, forming an instant theory, and then spending twenty minutes examining a line that was never the problem. This chapter takes a diagnostic apart piece by piece, gives you four questions to ask of any message, and then works through one real failure from each stage of the build — including the one where the toolchain has nothing at all to say because nothing about the build was wrong.
Foundation16 min readC++Part IToolchainDebugging
Part I · Module 2 · Chapter 2.7 · Reading Compiler and Linker Errors
1. The Habit This Chapter Replaces
Here is what usually happens when a build fails.
The screen fills with text. The reader's eye catches one familiar word, forms a theory, and jumps to the source file. Twenty minutes later they are still looking at a line that was never wrong, because the theory came from one word rather than from the message.
The replacement habit is almost embarrassingly simple:
Read the first message. All of it. Before you look at any code.
Every part of this chapter serves that one instruction. A diagnostic is a structured statement, not a paragraph of complaint, and it will usually tell you the stage, the location and the claim if you give it ten seconds.
2. The Anatomy of a Diagnostic
Here is a complete error, exactly as the compiler printed it:
e_comp.cpp:6:15: error: cannot initialize a variable of type 'const int' with an lvalue of type 'const char[6]'
6 | const int transfers = "eight";
| ^ ~~~~~~~
1 error generated.Five separate pieces of information:
| Part | Here | What it tells you |
|---|---|---|
| File | e_comp.cpp | Which file. Not always the one you edited — it may be a header. |
| Line : column | 6:15 | Where. The column is often more useful than the line. |
| Severity | error | error stops the build; warning does not; fatal error stops immediately. |
| The claim | cannot initialize a variable of type 'const int' with an lvalue of type 'const char[6]' | What the compiler actually objects to, usually naming both types. |
| The excerpt | the two lines with ^ and ~~~ | Your source, with the caret under what it is talking about. |
Read the claim as a sentence rather than scanning it for keywords. Cannot initialize a variable of type const int — so there is an int being given a starting value. With an lvalue of type const char[6] — and the value offered is text, six characters including the terminator. That is the whole bug, stated by the compiler, before you have looked at anything.
The caret line matters too. The ^ sits under the variable being initialised and the ~~~~~~~ underlines the thing that cannot initialise it. When a line contains several operations, the caret is what tells you which one.
3. Four Questions
For any message you have never seen, in order:
- Which tool is speaking? A line beginning
ld:or a message with no line number is the linker. Everything else in this module is the compiler or the preprocessor. - Which stage? §4 gives you the shapes.
- Where is it pointing? File, line, column. Is it a file you edited, or a header?
- What is it actually claiming? Read the sentence. Name the two things it says are in conflict.
Only then open the source. The four questions take about fifteen seconds and they are the difference between looking in the right place and looking anywhere.
4. One Failure From Each Stage
All five below are real, from the two-file project built in Chapter 2.4.
A. Preprocessing — a header that is not there
Misspell an include:
e_pre.cpp:1:10: fatal error: 'cyles.hpp' file not found
1 | #include "cyles.hpp"
| ^~~~~~~~~~~
1 error generated.How to recognise it: the message is about a file, and it says fatal error rather than error.
That word is doing real work. A normal error lets the compiler continue and report more problems. A fatal error stops immediately — the preprocessor cannot carry on with a file it could not find, because everything that follows depends on its contents. So when you see fatal error, expect exactly one message, and expect it to be the only useful one.
What to check: the spelling, then whether the file is where you think it is, then whether you used quotes where you needed angle brackets or the reverse.
B. Compilation — a rule of the language broken
The error from §2. The file, line and column are yours; the claim names two types.
How to recognise it: file, line, column, and a statement about C++ — types, names, syntax.
What to check: the thing under the caret. If the claim mentions two types, you are almost always looking at a conversion that is not allowed, and the fix is either a different type or a deliberate conversion.
C. Linking — a promise nobody kept
Compile main.cpp and try to build a program without cycles.o:
Undefined symbols for architecture arm64:
"cycles_for(int, int)", referenced from:
_main in main.o
ld: symbol(s) not found for architecture arm64How to recognise it, and this is the one to memorise: no line number, a symbol in quotes, and ld:.
It is not being unhelpful. The linker never saw your source — Chapter 2.6 §6 — so it cannot tell you where the call was written. What it can tell you is what was wanted (cycles_for(int, int)) and who wanted it (_main in main.o), and those two facts are enough.
What to check, in order:
- Did you compile the file that defines it? This is the answer most of the time.
- Did you pass that object file to the link command? Compiling it is not enough.
- Does the definition match the declaration exactly? A different parameter type is a different symbol — Chapter 1.7 §5 showed this.
- Is it a library you have not linked?
D. Linking — the same name defined twice
Compile cycles.cpp twice under different names and link both:
duplicate symbol 'cycles_for(int, int)' in:
.../cycles_copy.o
.../cycles.o
ld: 1 duplicate symbolsHow to recognise it: duplicate symbol, and a list of the object files that each provide it.
The linker has two answers to a question with one. It will not choose.
What to check: by far the commonest cause in real code is a function defined in a header — exactly the mistake Chapter 2.4 §9 warned about. The header gets included by two source files, each one compiles a copy, and the two copies collide. The second commonest is the same source file accidentally built twice under different names.
Notice how much the message gives you: not just the symbol but the two files providing it. Those two filenames are the fix.
E. Running — the build was fine
The last category, and the one people forget is a category:
#include "cycles.hpp"
#include <iostream>
int main()
{
const int transfers = 8;
std::cout << "cycles = " << cycles_for(transfers, 0) << '\n';
return 0;
}$ c++ -std=c++17 -Wall -Wextra -Wpedantic -c e_run.cpp -o e_run.o
$ c++ e_run.o cycles.o -o e_run
$ ./e_run
cycles = 3Zero diagnostics. A clean build. And a burst of eight transfers apparently takes three cycles, because the second argument was 0.
How to recognise it: there is no message. The build succeeded.
What this tells you: every stage did its job. The toolchain checked that everything referred to exists and that every rule of the language was obeyed. It has no opinion whatever about whether your arithmetic is right. That is not a gap in the tools — it is the boundary of what any compiler can check.
Debugging this class of problem is a different skill with different tools, and Module 25 is devoted to it.
5. One Mistake, Several Messages
Delete #include <iostream> from the program in Chapter 2.2 and the compiler reports:
hello.cpp:7:5: error: use of undeclared identifier 'std'
hello.cpp:8:5: error: use of undeclared identifier 'std'
hello.cpp:9:5: error: use of undeclared identifier 'std'Three errors. One deleted line. And that line is not mentioned in any of them.
This happens because the compiler does not give up at the first problem — it reports what it found and keeps going, so you can fix several things per build. The cost is that one root cause can produce many messages, and the later ones are often consequences of the first.
The rule that follows:
Fix the first error, then rebuild. Do not read the rest.
The second and third messages here are not independent problems; they are the same missing #include observed twice more. Reading all three and trying to fix three things is wasted effort, and on a bad day it is worse than wasted — you "fix" a symptom and introduce a real bug.
6. When the Error Is in a File You Did Not Write
Sooner or later a build fails inside a header you have never opened. This is normal, and Chapter 2.5 §7 explains why: by the time the compiler ran, that header's text was physically inside your translation unit.
The useful move is to read downward through the message. Compilers print the chain — the error, then a series of notes saying "included from here", "requested here", "while compiling this". The bottom of that chain is usually a line you wrote, and that is the line to change.
C++ has a famously bad version of this problem when templates are involved, where a small mistake can produce a wall of text from deep inside the standard library. You do not have templates yet, and when you do, Module 18.7 teaches how to read those specifically. The technique is the same one: find the part of the chain that is yours.
7. The Routine
Everything above, in the form you actually use:
| What you see | Stage | First thing to check |
|---|---|---|
fatal error: 'x.hpp' file not found | Preprocessing | Spelling; location; quotes vs angle brackets |
| File, line, column, a claim about types or syntax | Compilation | The thing under the caret, in the file named |
undefined symbol / Undefined symbols, with ld: and no line | Linking | Did you compile and link the file that defines it? |
duplicate symbol, naming two object files | Linking | A function defined in a header |
| Builds cleanly, behaves wrongly | Running | Your logic. The build was fine. Module 25 |
And the four habits that go with it:
- Read the first message completely before looking at code.
- Fix one thing and rebuild. Message counts are not mistake counts.
- Check the line above when the reported line looks correct.
- Check that the build actually succeeded before you believe the program you just ran — a failed build leaves the previous program sitting there, and
./appwill run it quite happily.
8. Mistakes Worth Making Once
Reading the last error instead of the first. The last one is usually the most derivative. The first is closest to the cause.
Treating a linker error as a compiler error. Hunting for a line number that does not exist, then concluding the message is broken. Different tool, different information — and the symbol name is the information.
Assuming an undefined symbol means the function does not exist. It usually exists. It just was not compiled into any object file you handed the linker, or the declaration and definition disagree about a parameter type.
Defining a function in a header. Fine until a second source file includes it, then a duplicate symbol. The two filenames in the message are the clue.
Ignoring fatal. When the build stops after one message, there is no point reading further output for clues; there is none.
Debugging the previous build. If compilation failed, ./app is still the old program. You will see the bug you already fixed and conclude your fix did not work.
9. What You Should Be Able to Say Now
- Read the first message completely before looking at any code.
- A diagnostic has five parts: file, line and column, severity, the claim, and the source excerpt with a caret.
- The column is the most underused piece of information on the screen.
fatal errormeans the compiler stopped, so there will be one message and no further clues.- A file, line and column plus a claim about C++ means the compiler.
- A symbol in quotes, no line number and
ld:means the linker — it never saw your source. - For an undefined symbol: did you compile the defining file, did you link it, do the declaration and definition match, is it a library you forgot.
duplicate symbolnaming two object files is most often a function defined in a header.- A clean build and a wrong answer is the fifth category. The toolchain checks that names resolve and rules are obeyed, never whether the logic is right.
- One mistake can produce many messages — one deleted
#includeproduced three errors that never mentioned it. Fix the first and rebuild. - A missing semicolon is usually reported one line late, so check the line above a line that looks correct.
- Errors inside headers you did not write are normal; read down the chain of notes to the part that is yours.
10. Exercises
Exercise 1 — classify five. For each, name the stage and the first thing you would check: (a) fatal error: 'util.hpp' file not found; (b) error: expected ';' after expression; (c) ld: symbol(s) not found; (d) duplicate symbol 'reset_counters()'; (e) the program builds and reports zero errors on a design you know is broken.
Exercise 2 — read it properly. Take this message apart into the five pieces from §2, then state the bug in one sentence of plain English:
report.cpp:14:22: error: cannot initialize a variable of type 'int' with an rvalue of type 'double'Exercise 3 — count the mistakes. A build reports 26 errors. Your colleague starts at the bottom of the list. Explain why that is the wrong end, and say what you would do instead.
Exercise 4 — the existing function. You get Undefined symbols: "parse_header(char const*)". You open the file and the function is clearly there, spelled correctly. Give three distinct explanations consistent with both facts.
Exercise 5 — find the header. A project links fine with two source files and fails with duplicate symbol as soon as a third is added. The third file adds no new functions. What has almost certainly happened, and which file would you open?
Exercise 6 — no message at all. cycles_for(8, 0) returned 3 with a completely clean build. Explain precisely which checks the toolchain performed and passed, and name the one kind of question it cannot answer.
Exercise 7 — the stale binary. Write down the sequence of events by which an engineer concludes "my fix did nothing" when in fact the fix was never built. Give the one habit that prevents it.
11. Summary
- Read the first message completely before looking at code. Everything else in this chapter supports that.
- A diagnostic has five parts: file, line and column, severity, the claim, and the caret excerpt. The column is the most underused.
fatal errormeans it stopped — one message, no further clues.- Compiler: file, line, column, a claim about C++. Linker: a symbol, no line, and
ld:. That distinction is the fastest classifier you have. - Undefined symbol: compile it, link it, check the signature matches, check the library.
- Duplicate symbol: usually a function defined in a header.
- A clean build and a wrong answer is its own category. The toolchain never checks whether your logic is right.
- Message count is not mistake count. One deleted
#includeproduced three errors naming none of them. Fix the first, rebuild. - A missing semicolon is reported one line late.
- Check the build succeeded before believing the program you just ran.
Every program in this module has printed something, and std::cout has been used on trust since Chapter 2.2 with a promise that the details would come later. This is later. Chapter 2.8 closes the module by making output a tool you control deliberately — including the separate stream that error messages belong on, and a measured demonstration of the one situation where the way you end a line decides whether your output survives at all.
Standards & specifications
- Governing standard
- ISO/IEC 14882 (the C++ language standard)(opens ISO/IEC (JTC 1/SC 22/WG 21) in a new tab)
Defines the C++ language and its standard library — the type system and templates, the object model and object lifetime, initialisation and overload resolution, the memory model and the containers, algorithms and utilities the library provides. Like C's standard it is a FAMILY of editions (C++98/03, C++11, C++14, C++17, C++20, C++23) and the editions are NOT interchangeable: a feature this curriculum relies on may be unavailable, differently specified or merely proposed in another edition, so each lesson names the edition its claim depends on. It does not define any foreign-function interface to a hardware description language, nor what a compiler actually emits, nor platform behaviour on real silicon — those are the implementation's and the device's contracts. Working drafts published by WG21 are freely available and are what most engineers read day to day; a draft is not the published standard.
This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.
Where this fits
Part of the C++ Programming curriculum.
