Appendix A: Build Systems and Tooling
Throughout this book you have compiled programs with a single g++ command. That works for small programs, but real projects have dozens or hundreds of source files, external dependencies, and platform-specific requirements. A build system automates compilation so you do not have to type long commands or remember which files changed. This appendix covers CMake (the most widely used C++ build system), compiler flags, sanitizers, static analysis, and basic debugging.
CMake Basics
CMake is a build system generator — it reads a CMakeLists.txt file and generates the actual build files (Makefiles on Linux/macOS, Visual Studio projects on Windows).
A Minimal Project
Create a directory with two files:
my_project/
CMakeLists.txt
main.cppCMakeLists.txt:
cmake_minimum_required(VERSION 3.20)
project(MyProject LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
add_executable(myapp main.cpp)main.cpp:
#include <iostream>
int main()
{
std::cout << "Built with CMake!\n";
return 0;
}Build it:
mkdir build && cd build
cmake ..
make
./myappMultiple Source Files
add_executable(myapp
main.cpp
playlist.cpp
audio.cpp
)Libraries
# Create a library
add_library(audio audio.cpp codec.cpp)
# Link it to the executable
add_executable(myapp main.cpp)
target_link_libraries(myapp PRIVATE audio)PRIVATE means audio is only needed by myapp, not by anything that uses myapp. Use PUBLIC if the dependency is also needed by consumers of the target.
Compiler Warnings
target_compile_options(myapp PRIVATE -Wall -Wextra -pedantic)Including Headers
target_include_directories(myapp PRIVATE ${CMAKE_SOURCE_DIR}/include) External Dependencies with find_package
find_package(Threads REQUIRED)
target_link_libraries(myapp PRIVATE Threads::Threads)For popular libraries (Boost, OpenSSL, etc.), CMake provides built-in Find modules. For others, use FetchContent to download them:
include(FetchContent)
FetchContent_Declare(
fmt
GIT_REPOSITORY https://github.com/fmtlib/fmt.git
GIT_TAG 10.2.1
)
FetchContent_MakeAvailable(fmt)
target_link_libraries(myapp PRIVATE fmt::fmt)
Tip: CMake has a steep learning curve, but it is the de facto standard for C++ projects. Start with the basics above and learn more as your projects grow.
Compiler Flags and Warnings
The compiler flags you choose affect correctness, performance, and debuggability.
Warning Flags
Always compile with warnings enabled:
g++ -Wall -Wextra -pedantic -Werror main.cpp| Flag | Effect |
|---|---|
-Wall | Enable most common warnings |
-Wextra | Enable additional warnings |
-pedantic | Warn about non-standard extensions |
-Werror | Treat warnings as errors |
Tip: Use -Werror in CI/CD pipelines to prevent warnings from accumulating. In development, you may want warnings without the hard stop.
Standard Selection
g++ -std=c++23 main.cpp # C++23
g++ -std=c++20 main.cpp # C++20
g++ -std=c++17 main.cpp # C++17Optimization Levels
| Flag | Effect |
|---|---|
-O0 | No optimization (fastest compile, best debugging) |
-O1 | Basic optimization |
-O2 | Standard optimization (good for release) |
-O3 | Aggressive optimization |
-Os | Optimize for size |
-Og | Optimize for debugging |
Debug Information
g++ -g main.cpp # Include debug symbols
g++ -g -O0 main.cpp # Debug build (best for debuggers)
g++ -O2 -DNDEBUG main.cpp # Release build (disables assert)Sanitizers
Sanitizers are compiler features that instrument your code to detect bugs at run time. They add overhead but catch problems that are otherwise invisible.
AddressSanitizer (ASan)
Detects memory errors: buffer overflows, use-after-free, double-free, memory leaks:
g++ -fsanitize=address -g main.cpp -o main
./mainIf your program has a memory bug, ASan prints a detailed error report with the exact location.
UndefinedBehaviorSanitizer (UBSan)
Detects undefined behavior: signed integer overflow, null pointer dereference, misaligned access:
g++ -fsanitize=undefined -g main.cpp -o mainThreadSanitizer (TSan)
Detects data races in multithreaded programs (Chapter 10):
g++ -fsanitize=thread -g main.cpp -o main -pthread
Tip: Run your tests with sanitizers regularly. Many bugs — especially memory and threading bugs — are silent until they corrupt data or crash under production load. Sanitizers catch them early.
Combining Sanitizers
You can combine ASan and UBSan:
g++ -fsanitize=address,undefined -g main.cppBut ASan and TSan cannot be used together — they instrument memory differently.
Static Analysis
Static analysis examines your code without running it, catching bugs that the compiler’s warnings miss.
clang-tidy
clang-tidy is the most popular C++ linter. It checks for common mistakes, style issues, and modernization opportunities:
clang-tidy main.cpp -- -std=c++23Useful check categories:
| Category | What it checks |
|---|---|
bugprone-* | Common bug patterns |
modernize-* | Suggest modern C++ replacements |
performance-* | Performance issues |
readability-* | Code readability |
cppcoreguidelines-* | C++ Core Guidelines compliance |
cppcheck
cppcheck is a standalone static analyzer:
cppcheck --enable=all main.cppIt catches issues like unused variables, null pointer dereferences, and resource leaks.
Compiler Warnings as Analysis
With -Wall -Wextra -pedantic -Werror, the compiler itself is a basic static analyzer. Start there before adding external tools.
Debugging with gdb/lldb
When your program crashes or produces wrong results, a debugger lets you step through the code line by line, inspect variables, and examine the call stack.
Basic gdb Commands
g++ -g -O0 main.cpp -o main
gdb ./main| Command | Effect |
|---|---|
run | Start the program |
break main | Set a breakpoint at main |
break file.cpp:42 | Breakpoint at line 42 |
next | Execute next line (step over) |
step | Step into function call |
continue | Run until next breakpoint |
print x | Print the value of x |
backtrace | Show the call stack |
info locals | Show local variables |
quit | Exit gdb |
lldb
lldb is the LLVM debugger, used primarily on macOS. Its commands are similar:
| gdb | lldb |
|---|---|
run | run |
break main | breakpoint set --name main |
next | next |
step | step |
print x | frame variable x or p x |
backtrace | thread backtrace |
Debugging Tips
- Compile with
-g -O0for the best debugging experience. Optimizations can reorder code and eliminate variables. - Use
valgrindas an alternative to ASan for memory debugging:valgrind ./main - Core dumps: if a program crashes, the OS can save a core dump. Load it with
gdb ./main coreto examine the state at the time of the crash.
Tip: Learn to use a debugger early. std::cout debugging is tempting but slow and unreliable. A debugger shows you exactly what is happening, where, and why.
Key Points
- CMake is the standard C++ build system.
CMakeLists.txtdefines targets, sources, and dependencies. - Use
add_executablefor programs,add_libraryfor libraries, andtarget_link_librariesto connect them. - Compiler flags:
-Wall -Wextra -pedanticfor warnings,-std=c++23for the standard,-O2for release,-g -O0for debug. - Sanitizers catch runtime bugs: ASan (memory), UBSan (undefined behavior), TSan (data races). Use them in testing.
- Static analysis tools like
clang-tidyandcppcheckcatch bugs without running the code. - gdb/lldb let you step through code, set breakpoints, and inspect variables. Compile with
-g -O0for best results.
Exercises
Think about it: Why is CMake called a “build system generator” rather than a “build system”? What does it generate?
Write a CMakeLists.txt for a project with
main.cpp,audio.cpp, andaudio.h. Set the C++ standard to 23 and enable-Wall -Wextra -pedantic.Think about it: Why should you compile with
-Wall -Wextra -pedanticfrom the start of a project rather than adding them later?Calculation: You have a program with a buffer overflow that only corrupts memory silently. Which sanitizer would catch it? What compiler flag would you use?
Think about it: AddressSanitizer and ThreadSanitizer cannot be used together. Why might that be? How would you test for both memory and threading bugs?
Write a gdb session (sequence of commands) that:
- Sets a breakpoint at
main - Runs the program
- Steps through three lines
- Prints a local variable called
count - Continues to the end
- Sets a breakpoint at
Think about it: What is the difference between
-O0,-O2, and-O3? When would you use each?Where is the problem?
add_executable(myapp main.cpp) target_link_libraries(myapp fmt)What is missing compared to the example in this chapter?
Think about it: Why does the text recommend running tests with sanitizers enabled? What kinds of bugs do sanitizers catch that tests alone miss?
Set up a project with CMake that has a
main.cppand amath_utils.cpp/math_utils.hlibrary. The library should have a functionint factorial(int n). Build it with CMake, run it, and then compile with AddressSanitizer enabled and verify it runs cleanly.