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:
cmake -B build
cmake --build build
./build/myappcmake -B build generates the build files into a build/ directory, and cmake --build build runs the underlying build tool for you. You may also see the older two-step form mkdir build && cd build && cmake .. followed by make; it does the same thing.
Multiple 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)The flags above are GCC and Clang spellings. MSVC uses /W4 for the same general “everyday warnings” level (and does not have a direct equivalent of -pedantic). For a portable CMakeLists.txt that picks the right flags per compiler, use a generator expression:
target_compile_options(myapp PRIVATE
$<$<OR:$<CXX_COMPILER_ID:GNU>,$<CXX_COMPILER_ID:Clang>>:-Wall -Wextra -pedantic>
$<$<CXX_COMPILER_ID:MSVC>:/W4>)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++23Passing flags after -- works for one file. Real projects let CMake write a compile_commands.json so clang-tidy sees the exact flags every file is compiled with:
cmake -B build -DCMAKE_EXPORT_COMPILE_COMMANDS=ONUseful 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.
Package Managers
For most languages, “install dependency X” is one command. C++ historically had no equivalent — you found the source, built it yourself, hoped it interoperated with your toolchain, and remembered to update it later. Two modern package managers, vcpkg and Conan, make it almost as smooth as pip or cargo.
vcpkg
vcpkg (from Microsoft) builds dependencies from source and integrates with CMake. A typical workflow:
# one-time setup
git clone https://github.com/microsoft/vcpkg.git
./vcpkg/bootstrap-vcpkg.sh
# install a package
./vcpkg/vcpkg install fmt boost-asio
# point CMake at vcpkg's toolchain file
cmake -B build -DCMAKE_TOOLCHAIN_FILE=./vcpkg/scripts/buildsystems/vcpkg.cmakeIn your CMakeLists.txt, you then find_package and target_link_libraries as if the dependency had been installed system-wide:
find_package(fmt CONFIG REQUIRED)
target_link_libraries(myapp PRIVATE fmt::fmt)vcpkg shines when you want a consistent toolchain across Windows, Linux, and macOS, and when you are already using CMake.
Conan
conan is a Python-based package manager that downloads pre-built binaries when available and falls back to source builds when not. It is not tied to CMake, has a large central package index (Conan Center), and is a common choice in cross-platform projects that already use Python tooling.
Install it once with pip:
pip install conanDeclare your dependencies in a conanfile.txt:
[requires]
fmt/10.2.1
boost/1.84.0
[generators]
CMakeDeps
CMakeToolchainThen fetch them:
conan install . --output-folder=build --build=missingBoth tools generate the CMake glue you need; the choice usually comes down to which one your team already uses.
Tip: If you are starting a new project today, pick one package manager and document it in your README. “Just install Boost yourself” is the answer that wastes hours of every new contributor’s first day.
Wut: C++ standardization has not adopted a single package manager. Both vcpkg and Conan work fine; C++20 modules improve compilation and encapsulation, but they say nothing about how libraries are distributed, so a package manager is still your problem to pick.
Documentation with Doxygen
Doxygen reads specially formatted comments in your code and generates HTML / PDF / man-page documentation. You write the comments where the code is; Doxygen extracts function signatures, class hierarchies, file lists, and inheritance graphs automatically.
The basic comment style is /// (or /** ... */) directly above the entity you are documenting:
#include <string>
/// Add two integers.
///
/// \param a The first addend.
/// \param b The second addend.
/// \return The sum of \p a and \p b.
int add(int a, int b);
/// A music track in a playlist.
///
/// Tracks remember their title, artist, and release year.
class Track {
public:
/// Construct a track.
/// \param title The song's title.
/// \param artist The performer.
/// \param year The release year.
Track(std::string title, std::string artist, int year);
/// \return `true` if the track was released in the 2000s.
bool is_2000s() const noexcept;
};To generate docs, run doxygen -g once to create a default Doxyfile, edit it (set PROJECT_NAME, INPUT, OUTPUT_DIRECTORY, and RECURSIVE = YES), then run doxygen — the HTML lands in the OUTPUT_DIRECTORY you configured.
Useful Doxygen commands:
\param name description\return description\throws ExceptionType description\see other_function\note ...and\warning ...\code ... \endcodefor embedded examples\brief one-line summary(some teams prefer brief-only over full prose)
Tip: Doxygen is most valuable for library code that other people will call. Application code that only your team reads is usually better served by a README.md and well-named identifiers. The marginal value of Doxygen drops fast when no one looks at the generated HTML.
Wut: Doxygen will parse a project even if you have not written any documentation comments — it generates pages for every class and function based on declarations alone. The output is mostly empty pages, which is not very useful, but it is a fast way to verify your build is producing the right symbols.
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. - Package managers (vcpkg, Conan) make C++ dependencies installable in one command and integrate with CMake; pick one per project and document it in the README.
- Doxygen turns specially formatted
///comments into browseable HTML / PDF docs; valuable for library code, less useful for application code.
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?Think about it: 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 bug?
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.Think about it: Why does C++ rely on third-party package managers (vcpkg, Conan) instead of having a built-in one like Python’s
pipor Rust’scargo? What problem would a built-in package manager have to solve that the language committee has so far avoided?Write a small project with one header
track.hand one sourcetrack.cppdefining aTrackclass. Document the class and its public methods with///Doxygen comments. Rundoxygen -gto generate aDoxyfile, editINPUTandRECURSIVE, rundoxygen, and open the generatedhtml/index.html. What does the generated documentation contain that was not literally typed in the comments?What does this do? This program is compiled with
g++ -fsanitize=address -g sum.cppand run. What does AddressSanitizer report, and does the program print anything?#include <iostream> int main() { int* data = new int[4]{2, 4, 6, 8}; int sum = 0; for (int i = 0; i <= 4; ++i) { sum += data[i]; } std::cout << sum << '\n'; delete[] data; return 0; }