The C++ Textbook

Part 1 · Foundations

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

What this book compiles with
g++ -std=c++20 -Wall -Wextra \
    -fsanitize=address,undefined -fno-omit-frame-pointer -g \
    -o program main.cpp
./program

Piece 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.

Four warnings worth reading
#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:

A stricter set
g++ -std=c++20 -Wall -Wextra -Wpedantic -Wshadow -Wconversion ...
  • -Wpedantic rejects compiler extensions, so your code stays portable.
  • -Wshadow catches an inner variable hiding an outer one with the same name.
  • -Wconversion warns on narrowing — double to int, long to int. 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.

AddressSanitizer, on a buffer overflow
#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:

  1. The first line names the error and the address: ERROR: AddressSanitizer: heap-buffer-overflow on address 0x...
  2. The first stack frame is where the bad access happened — your code, with a file and line if you compiled with -g.
  3. 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.

UndefinedBehaviorSanitizer, on signed overflow
#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.

A first session
g++ -std=c++20 -g -O0 -o program main.cpp
gdb ./program

Inside, 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.

Checking a chapter against both
npm run verify:snippets
CPPTB_COMPILER=clang++ npm run verify:snippets

Getting a compiler

  • Debian or Ubuntu: sudo apt install g++ gdb
  • Fedora: sudo dnf install gcc-c++ gdb
  • macOS: xcode-select --install gives you clang++ and lldb
  • 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

Check yourself

Practice