Setting up C++: compilers, CMake and the build workflow
Install a modern compiler, pick a language standard, wire warnings and sanitizers into the build, and structure a CMake project.
Compiler and standard
The three mainstream compilers are GCC, Clang/LLVM and MSVC. They agree on the language but differ in diagnostic quality and in how quickly new standard features land. Pick one, pin a version in CI, and keep the standard flag explicit rather than relying on whatever the compiler defaults to.
g++ --version
clang++ --version
cmake --version
# compile one translation unit with the warnings you actually want
g++ -std=c++20 -Wall -Wextra -Wpedantic -Wshadow -Wconversion -g -O2 main.cpp -o app
# feature-test a specific library facility instead of guessing
g++ -std=c++23 -dM -E -x c++ /dev/null | grep -i __cpp_lib| Standard | Flag | Headline features |
|---|---|---|
| C++17 | -std=c++17 | std::optional, string_view, structured bindings, filesystem |
| C++20 | -std=c++20 | Concepts, ranges, std::format, coroutines, modules, std::span |
| C++23 | -std=c++23 | std::expected, std::print, mdspan, ranges improvements |
| GNU extensions | -std=gnu++20 | Adds GNU extensions; fine for Linux tools, less portable |
Guard new features with the feature-test macros rather than the compiler version: __cpp_lib_expected, __cpp_concepts, __cpp_lib_format. These are defined in the standard headers and reflect what your standard library actually implements.
A minimal but correct CMake project
Modern CMake is target-based: you describe a target and attach properties to it. Global variables such as include_directories are the legacy style and leak settings between targets.
cmake_minimum_required(VERSION 3.20)
project(app LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF) # -std=c++20, not gnu++20
# a library plus a thin executable: always testable, always reusable
add_library(core STATIC src/parse.cpp src/model.cpp)
target_include_directories(core PUBLIC include)
target_compile_features(core PUBLIC cxx_std_20)
add_executable(app src/main.cpp)
target_link_libraries(app PRIVATE core)
# warnings belong to a target, via an interface library
add_library(warnings INTERFACE)
target_compile_options(warnings INTERFACE
$<$<CXX_COMPILER_ID:GNU,Clang>:-Wall -Wextra -Wpedantic -Wshadow>
$<$<CXX_COMPILER_ID:MSVC>:/W4 /permissive->)
target_link_libraries(core PRIVATE warnings)
target_link_libraries(app PRIVATE warnings)
include(CTest)
if (BUILD_TESTING)
add_executable(core_tests tests/core_tests.cpp)
target_link_libraries(core_tests PRIVATE core)
add_test(NAME core COMMAND core_tests)
endif()cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug # configure
cmake --build build -j # build
ctest --test-dir build --output-on-failure # test
cmake --build build --target install --config Release
# an out-of-tree build directory keeps the source tree clean;
# never configure in the source directory itselfadd_libraryoverfile(GLOB ...): listing sources explicitly makes CMake re-run when files are added.- Generator expressions such as
$<$<CXX_COMPILER_ID:GNU,Clang>:-Wall>apply a flag only for the right compiler. target_link_libraries(app PRIVATE core)propagates include directories automatically;PRIVATEkeeps them out of the public interface.- Use
target_precompile_headersonly after profiling a slow build — it hides include churn but can produce stale-object surprises.
Debug, sanitize and release configurations
# debug build with the sanitisers, driven from CMake options
cmake -S . -B build-asan -DCMAKE_BUILD_TYPE=Debug \
-DCMAKE_CXX_FLAGS="-fsanitize=address,undefined -fno-omit-frame-pointer -g"
cmake --build build-asan -j && ctest --test-dir build-asan
# thread sanitizer needs its own tree: it cannot be combined with ASan
cmake -S . -B build-tsan -DCMAKE_BUILD_TYPE=Debug \
-DCMAKE_CXX_FLAGS="-fsanitize=thread -g"-Wall -Wextra plus a sanitiser job in CI as the baseline, not an extra. Almost every hour spent debugging a mysterious crash is saved by having built with the sanitisers from the first commit.FAQ
What is the difference between -O2 and -DNDEBUG?
-DNDEBUG disables assert; -O2 optimises. They are independent. CMake's Release build type sets both, which is why asserts vanish in a release build and you should use a logging or expect-style check for conditions that must hold in production.Why is my build rebuilding everything after a tiny change?
ninja -d explain or cmake --build build --verbose to see which dependency triggered it.Related
Modern C++ basics CMake, dependencies and project structure
Last refreshed 2026-09-18.