How to Read C++ Compiler Errors: Warnings and Sanitizers
A C++ diagnostic gives file, line, column, severity and message. Which warning flags to enable, how to read template and linker errors, plus ASan and UBSan.
- Course: C++ study plan
- Module: C++ and the toolchain
- Kind: Lesson
- Reading time: 14 min
- Runtime: Clang 18 · C++20
How do you read a C++ compiler error?
A C++ compiler error has the form file:line:col: error: message, followed by the source line, a caret under the offending token and any note: lines that give context. Fix the first error first, because the errors after it are often its consequences, and read the notes. For a template error, the first line says what failed and the required from here line names your code.
Lesson
A C++ compiler is the most thorough reviewer your code will ever have, and it works for free — if you read what it says. This lesson teaches the shape of a diagnostic so that you can read it like a stack trace: which file, which line and column, what kind of message, and what the notes underneath are pointing at. It also covers the warning flags worth turning on, what a template error and a linker error look like, and the two run-time tools — sanitizers and a debugger — that catch what the compiler cannot.
The three compilers
| Compiler | Driver | Where | Notes |
|---|---|---|---|
| GCC | g++ | Linux default, MinGW on Windows | Ships libstdc++, the standard library most Linux C++ uses |
| Clang | clang++ | macOS default (Apple Clang), Linux, Windows | LLVM-based; famously readable diagnostics; uses libstdc++ or its own libc++ |
| MSVC | cl | Windows, Visual Studio | Different flag syntax (/W4, /std:c++20) and a different message format |
GCC and Clang accept the same flags and produce diagnostics in the same file:line:col: kind: message shape, so what you learn on one transfers. The study judge is Clang 18 with libstdc++ 14, -std=c++20 -O2; the local gate this track was written against is GCC 14 with -Wall -Wextra. Code that is clean on either compiles on the other unless it leans on a compiler extension — variable-length arrays, __int128, #include <bits/stdc++.h> — which is why the track avoids them.
Anatomy of a compile error
#include <iostream>
int main() {
int total = 0;
for (int i = 0; i < 3; ++i) {
totl += i;
}
std::cout << total << '\n';
}
main.cpp:6:9: error: use of undeclared identifier 'totl'; did you mean 'total'?
6 | totl += i;
| ^~~~
| total
main.cpp:4:9: note: 'total' declared here
4 | int total = 0;
| ^
1 error generated.
Read it left to right. main.cpp:6:9 — file, line 6, column 9; every editor can jump there. error — the severity: an error stops the build, a warning does not, a note is not a problem at all but context for the message above it. Then the message, often with a suggestion. The source line and the caret show exactly which token, and the note: says where the related declaration is. 1 error generated. is Clang's summary; GCC ends with nothing.
Two habits: fix the first error first, because one missing semicolon or unknown type cascades into dozens of nonsense errors below it; and read the notes, because for an overload or template failure the note chain (candidate function not viable: ...) is the actual explanation.
Warnings, and which flags to turn on
A warning is the compiler saying "this is legal and probably wrong". -Wall enables the ones almost always worth fixing; -Wextra adds a second tier; -Wpedantic complains about extensions the standard does not allow; -Werror turns every warning into an error, so a build with warnings fails — which is how teams keep the count at zero.
| Warning (flag) | What it catches | Why it matters |
|---|---|---|
unused variable (-Wunused-variable) | a name declared and never read | usually a typo elsewhere, or dead code |
may be used uninitialised (-Wuninitialized, -Wmaybe-uninitialized) | a read before any write | undefined behaviour; initialise with {} |
control reaches end of non-void function (-Wreturn-type) | a missing return on some path | undefined behaviour, and -O2 makes it bite |
comparison of integers of different signs (-Wsign-compare) | i < v.size() with int i | the int converts to unsigned; -1 < v.size() is false |
unused value (-Wunused-value) | x + 1; | a statement with no effect |
-Wshadow (not in -Wall) | an inner variable hiding an outer one | the bug from lesson 3 |
-Wconversion (not in -Wall) | int x = 3.7;, long into int | silent narrowing |
Build with -Wall -Wextra from day one. A warning you have decided is harmless is noise that hides the next real one; fix it, or silence that one case deliberately, never by lowering the flags.
A template error
Template errors are long because the compiler reports the failure where it happened — deep in the library — and then the chain of instantiations that led there. Sorting a vector of a type that has no <:
/usr/include/c++/14/bits/predefined_ops.h:45:23: error: no match for 'operator<' (operand types are 'const Point' and 'const Point')
45 | { return *__it1 < *__it2; }
| ~~~~~~~^~~~~~~~
main.cpp:12:14: required from here
12 | std::sort(points.begin(), points.end());
| ~~~~~~~~~^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Read the first line for the actual problem (no match for 'operator<' on two Points) and the required from here line (Clang says in instantiation of ... requested here) for your line that triggered it. Everything in between is the library's plumbing. C++20 concepts (Module 12 lesson 5) exist largely to turn this wall of text into one line: constraints not satisfied.
A linker error
/usr/bin/ld: main.o: in function `main':
main.cpp:(.text+0x1a): undefined reference to `compute(int)'
collect2: error: ld returned 1 exit status
No error: with a column, ld or collect2 in the text, and undefined reference (or undefined symbol from Clang's lld): the compile stage passed, and the linker cannot find a definition. Lesson 2 listed the causes — a file not on the command line, a library not linked, a declaration whose definition was never written or does not match.
Sanitizers: the checks the compiler cannot do
Undefined behaviour is invisible at compile time by design. The sanitizers add the checks at run time:
$ g++ -std=c++20 -g -O1 -fsanitize=address,undefined main.cpp -o main
$ ./main
main.cpp:7:14: runtime error: signed integer overflow: 2147483647 + 1 cannot be represented in type 'int'
=================================================================
==4242==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x602000000034
READ of size 4 at 0x602000000034 thread T0
#0 0x55d4 in main main.cpp:9
AddressSanitizer (ASan) reports out-of-bounds reads and writes, use-after-free, double free and leaks, with a stack trace naming your line. UndefinedBehaviorSanitizer (UBSan) reports signed overflow, bad shifts, null dereference and misaligned access. -g gives the reports line numbers. The program runs two to three times slower, so these are development flags, never release ones — and the judge, which compiles with plain -O2, cannot tell you why a run failed, only that it did. Module 7 lesson 6 and Module 19 use them on the memory-error catalogue.
A debugger, in outline
Build with -g -O0 (debug information, no optimisation, so variables and lines are where you expect them), then:
$ gdb ./main
(gdb) break main.cpp:9 # stop at line 9
(gdb) run < input.txt # start, feeding the file as stdin
(gdb) next # execute one line, stepping over calls
(gdb) step # execute one line, stepping into calls
(gdb) print total # show a variable
(gdb) backtrace # the call stack: where am I, and how did I get here
(gdb) continue # run to the next breakpoint
lldb (Clang's debugger) has the same commands with slightly different spellings, and every IDE wraps one of the two. The one command to know from memory is backtrace after a crash: it names the line that failed and the calls that led to it.
Pitfalls
- Reading only the first word of the first error and guessing. Read the location, the message and the note.
- Fixing the last error first. The first is the cause; the rest are often its consequences.
- A warning-free build with
-w. That flag suppresses all warnings; it is how bugs become invisible. - Debugging optimised code. At
-O2variables vanish and lines reorder; build with-O0 -gto debug.
Key takeaways
- A diagnostic is
file:line:col: severity: message, followed by the source line, a caret and notes; errors stop the build, warnings do not, notes explain. - Fix the first error first and read the notes; for a template error, the first line and
required from here. -Wall -Wextraalways;-Werrorto keep a codebase at zero warnings; never-w.undefined referencewithldin the text is the linker, not the compiler.-fsanitize=address,undefinedwith-gfinds memory errors and UB at run time; the judge (Clang 18,-std=c++20 -O2) has no sanitizers, so test locally with them.
Common questions
What do -Wall and -Wextra do?
They are GCC and Clang flags that enable warnings: -Wall turns on the ones almost always worth fixing, such as unused variables, missing returns and signed/unsigned comparisons, and -Wextra adds a second tier. Despite its name, -Wall is not everything — -Wshadow and -Wconversion must be asked for separately.
What is the difference between GCC and Clang?
Both are C++ compilers that accept the same flags and print diagnostics in the same file:line:col shape. GCC (g++) is the Linux default and ships libstdc++; Clang (clang++) is LLVM-based, the macOS default, known for readable messages, and uses either libstdc++ or its own libc++.
What are AddressSanitizer and UndefinedBehaviorSanitizer?
They are run-time checkers enabled with -fsanitize=address,undefined. AddressSanitizer reports out-of-bounds access, use-after-free, double free and leaks; UndefinedBehaviorSanitizer reports signed overflow, bad shifts, null dereferences and misaligned access. Add -g for line numbers; programs run two to three times slower, so they are development flags, not release ones.
What does "comparison of integers of different signs" mean?
It is the -Wsign-compare warning, typically for i < v.size() with int i. The int is converted to the unsigned type that size() returns, so a negative value becomes huge and -1 < v.size() is false. Use a std::size_t index, or convert the size deliberately.
How do you debug a C++ program with gdb?
Build with -g -O0 so variables and lines match the source, then run gdb ./main. Set a breakpoint with break main.cpp:9, start with run < input.txt, step with next or step, inspect with print total, and after a crash type backtrace to see the failing line and the calls that led to it.
Exercises
Diagnostic summary
Summarise a compiler's output. Read lines until the input ends. A line is a diagnostic when it contains : error: , : warning: or : note: ; everything before that marker is its location (file:line:col). Other lines (source excerpts, carets, 1 error generated.) are ignored.
Input: zero or more lines. Output: two lines: errors=<e> warnings=<w> notes=<n>, then first error: <location> for the first error line, or first error: none.
Read with while (std::getline(std::cin, line)); line.find(": error: ") returns the position of the marker or std::string::npos when it is absent, and line.substr(0, pos) is the location.
Example: input
main.cpp:12:5: error: use of undeclared identifier 'x'
main.cpp:3:10: note: 'y' declared here
main.cpp:20:9: warning: unused variable 'z' [-Wunused-variable]
util.cpp:4:1: error: expected ';' after expression
→
errors=2 warnings=1 notes=1
first error: main.cpp:12:5Warnings as errors
Apply -Werror to a compiler's output. The first line of input is the set of flags the build used, for example -Wall -Werror or -O2 -Wall. Every following line is compiler output. When the flags contain -Werror, every : warning: marker is rewritten to : error: before the line is printed; otherwise lines are printed unchanged. Every line is echoed. After the last line print Clang's summary: <w> warning(s) and <e> error(s) generated. with correct singular/plural forms and only the non-zero parts — 1 warning generated., 2 errors generated., 1 warning and 2 errors generated. — or build ok when there were neither.
Input: the flags line, then zero or more lines of compiler output. Output: every output line (promoted when -Werror is on), then the summary line.
line.find(": warning: ") locates the marker and line.replace(pos, len, ": error: ") rewrites it in place.
Example: input
-Wall -Werror
main.cpp:4:9: warning: unused variable 'x' [-Wunused-variable]
main.cpp:9:1: error: expected ';' after expression
→
main.cpp:4:9: error: unused variable 'x' [-Wunused-variable]
main.cpp:9:1: error: expected ';' after expression
2 errors generated.In this module: C++ and the toolchain
← Console input and output · Checkpoint — C++ and the toolchain →