Your toolchain
Compiler flags, warnings, debuggers, and sanitizers — the tools that turn silent bugs into loud ones.
By the end of this chapter you can
- Compile with a warning set that catches real bugs
- Run a program under AddressSanitizer and read its report
- Step through a program in a debugger and inspect a variable
Every sample in this book has been compiled with a particular set of flags, and that choice has been doing real work — catching mistakes in the writing, and producing the diagnostics you have been reading. This chapter is that command line, explained.
The command line
g++ -std=c++20 -Wall -Wextra \
-fsanitize=address,undefined -fno-omit-frame-pointer -g \
-o program main.cpp
./programPiece by piece:
| Flag | What it does |
|---|---|
-std=c++20 |
Selects the language version. Without it you get the compiler’s default, which may be older than you expect. |
-Wall -Wextra |
Turns on the warnings worth having. Not “all” warnings despite the name. |
-fsanitize=address,undefined |
Instruments the program to detect memory errors and undefined behaviour at run time. |
-fno-omit-frame-pointer |
Keeps stack traces readable in sanitizer reports. |
-g |
Emits debug information: function names and line numbers in traces, and the ability to use a debugger. |
-o program |
Names the output. Without it you get a.out. |
clang++ accepts all of these identically. On Windows, MSVC uses /std:c++20 /W4 and has /fsanitize=address; the concepts transfer, the spellings do not.
Warnings are the cheapest tool you have
-Wall -Wextra costs nothing at run time and catches a class of bug before the
program exists.
#include <iostream>
int main() {
int uninitialised;
std::cout << "reading an uninitialised value: " << uninitialised << '\n';
int signed_value = -1;
unsigned int unsigned_value = 1;
if (signed_value < unsigned_value) {
std::cout << "-1 < 1 as expected\n";
} else {
std::cout << "-1 is NOT less than 1 — the comparison converted it\n";
}
}Read the warnings on that one. -Wmaybe-uninitialized catches the first;
-Wsign-compare catches the second, which is the unsigned-conversion trap from
Chapter 1.2 appearing as a comparison that reports the opposite of the truth.
Three more worth adding once you are comfortable:
g++ -std=c++20 -Wall -Wextra -Wpedantic -Wshadow -Wconversion ...-Wpedanticrejects compiler extensions, so your code stays portable.-Wshadowcatches an inner variable hiding an outer one with the same name.-Wconversionwarns on narrowing —doubletoint,longtoint. It is noisy on existing code and excellent on new code.
Sanitizers
A sanitizer instruments your program so that undefined behaviour announces
itself. The two you want by default are AddressSanitizer and
UndefinedBehaviorSanitizer, and they compose: -fsanitize=address,undefined.
#include <iostream>
#include <vector>
int main() {
std::vector<int> v(4, 7);
std::cout << "reading one past the end\n";
std::cout << v[4] << '\n';
}Reading the report
The output is long, and three parts of it matter:
- The first line names the error and the address:
ERROR: AddressSanitizer: heap-buffer-overflow on address 0x... - The first stack frame is where the bad access happened — your code, with
a file and line if you compiled with
-g. - The allocation section at the bottom says where the memory came from and
how large it was:
allocated by thread T0 here, followed by the size and how far past it you went.
If the trace shows addresses instead of function names, you compiled without
-g. Add it — the report’s whole value is in the names.
#include <climits>
#include <iostream>
int main() {
int big = INT_MAX;
std::cout << "adding one to INT_MAX\n";
std::cout << big + 1 << '\n';
}UBSan’s reports are shorter: a file, a line, and a sentence saying what the program did that the standard does not define.
What they do not catch
Chapter 6.3 has the full table. The one to remember now: ASan does not catch
uninitialised reads. That is MemorySanitizer’s job, and MSan is Clang-only and
requires every library it touches to be rebuilt with it. Your defence against
uninitialised reads is -Wall and initialising at the point of declaration.
The debugger
A debugger lets you stop a program and look at it. The two are gdb (GNU) and
lldb (LLVM); the commands below are gdb, and lldb’s are similar.
g++ -std=c++20 -g -O0 -o program main.cpp
gdb ./programInside, the commands that cover most of what you need:
| Command | Short | Does |
|---|---|---|
break main.cpp:12 |
b |
Stop at line 12 |
run |
r |
Start the program |
next |
n |
Run the next line, stepping over calls |
step |
s |
Run the next line, stepping into calls |
continue |
c |
Run until the next breakpoint |
print total |
p |
Show a variable’s value |
backtrace |
bt |
Show the call stack |
finish |
Run until the current function returns | |
quit |
q |
Leave |
Compile with -O0 for debugging. At -O2 the compiler reorders and merges
code, so stepping jumps around and variables report <optimized out> — the
program is correct, but it no longer corresponds line-by-line to your source.
Your editor almost certainly wraps this in a UI — VS Code, CLion, and Visual
Studio all drive gdb or lldb underneath. Learning the commands is still
worth an hour, because they are what you have on a remote machine or in CI.
Which compiler
Both major open-source compilers are excellent and you should have both if you can:
- GCC (
g++) is the default on most Linux distributions. - Clang (
clang++) generally produces clearer error messages, and its sanitizer support arrived first.
Compiling with both is a genuinely useful habit: they warn about different
things, and code that satisfies both is more portable than code that satisfies
one. This book’s verification scripts accept CPPTB_COMPILER=clang++ for
exactly that reason.
npm run verify:snippets
CPPTB_COMPILER=clang++ npm run verify:snippetsGetting a compiler
- Debian or Ubuntu:
sudo apt install g++ gdb - Fedora:
sudo dnf install gcc-c++ gdb - macOS:
xcode-select --installgives youclang++andlldb - Windows: install “Desktop development with C++” from the Visual Studio Installer, or use WSL and follow the Ubuntu instructions
Check it worked:
g++ --version
echo 'int main(){}' > t.cpp && g++ -std=c++20 t.cpp -o t && ./t && echo ok