CMake is the de-facto build system for C++; nearly every library and IDE speaks it.
Compiling by hand stops scaling after a few files. CMake is a build-system generator: you describe your project in CMakeLists.txt, and CMake generates Ninja files, Makefiles or Visual Studio projects. IDEs (CLion, VS Code, Visual Studio) open CMake projects directly.
Modern CMake is all about targets:
add_executable(app main.cpp)andadd_library(core STATIC a.cpp b.cpp)define targets.target_link_libraries(app PRIVATE core)links them, and also pulls in core's public include paths and flags.target_include_directories,target_compile_optionsandtarget_compile_definitionsattach settings to one target.
The keywords matter. PRIVATE applies only to this target, PUBLIC applies to it and everything that links to it, and INTERFACE applies only to dependents. Avoid the old global commands like include_directories() and add_compile_options() in new code.
Set the standard with set(CMAKE_CXX_STANDARD 20), CMAKE_CXX_STANDARD_REQUIRED ON and CMAKE_CXX_EXTENSIONS OFF (so you get -std=c++20, not -std=gnu++20). CMAKE_EXPORT_COMPILE_COMMANDS writes compile_commands.json, which clangd and clang-tidy use.
Dependencies come in through find_package(Foo CONFIG REQUIRED) for installed packages (often via vcpkg or Conan), or FetchContent to download and build a dependency as part of your project.
Always build out of source: cmake -S . -B build configures into a separate build directory, cmake --build build compiles only what changed, and ctest runs tests registered with add_test. Choose -DCMAKE_BUILD_TYPE=Debug or Release with single-config generators. CMake presets (CMakePresets.json) let a team share these settings.
Example
cmake_minimum_required(VERSION 3.20)
project(inventory LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON) # compile_commands.json for tools
add_library(core STATIC src/inventory.cpp src/storage.cpp)
target_include_directories(core PUBLIC include) # users of core see include/
if(MSVC)
target_compile_options(core PRIVATE /W4)
else()
target_compile_options(core PRIVATE -Wall -Wextra -Wpedantic)
endif()
find_package(fmt CONFIG REQUIRED) # an installed dependency
add_executable(app src/main.cpp)
target_link_libraries(app PRIVATE core fmt::fmt)
enable_testing()
add_executable(core_tests tests/core_tests.cpp)
target_link_libraries(core_tests PRIVATE core)
add_test(NAME core_tests COMMAND core_tests)
# cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
# cmake --build build -j
# ctest --test-dir buildRemember
- Think in targets:
add_library/add_executable+target_*commands PRIVATE/PUBLIC/INTERFACEcontrol what propagates to dependents- Configure out of source:
cmake -S . -B build, thencmake --build build
Watch out for
- Using global
include_directories()and rawCMAKE_CXX_FLAGSedits instead of per-target settings.