C++ and CMake
- Description: Modern target-based CMake for C++ — projects, libraries, dependencies (
find_package,FetchContent), install/export, idioms and anti-patterns - My Notion Note ID: K2A-B2-1
- Created: 2020-01-13
- Updated: 2026-04-30
- License: Reuse is very welcome. Please credit Yu Zhang and link back to the original on yuzhang.io
Table of Contents
- 1. Overview
- 2. Minimal Project
- 3. Targets
- 4. Sources, Includes, Definitions, Options
- 5. Build Configuration
- 6. Finding and Pulling Dependencies
- 7. Build, Test, Install Commands
- 8. Install and Export
- 9. References
1. Overview
CMakeLists.txt files describe the build at a high level; the cmake command reads them and emits files for the underlying build tool — Makefiles, Ninja, Visual Studio projects, Xcode projects — selected with -G <generator>. The same project can target any of them.
Modern CMake (3.x, especially 3.15+) is target-centric: each target declares its own sources, headers, compile flags, and dependencies; consumers inherit those declarations. The older directory-scoped style (include_directories, add_definitions, link_directories) is still supported but leaks settings across unrelated targets.
2. Minimal Project
cmake_minimum_required(VERSION 3.20)
project(myapp LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF) # use -std=c++20, not -std=gnu++20
add_executable(myapp main.cpp)
Out-of-source build (the only sane way):
cmake -S . -B build # configure (generate build files into build/)
cmake --build build -j # build using the underlying tool, parallel
./build/myapp # run
cmake_minimum_required should reflect the lowest version your CMake code actually requires. Set it to whatever you can realistically expect on contributor machines — bumping it later is a breaking change for downstream packagers.
3. Targets
3.1 Executables, Libraries, Tests
add_executable(app main.cpp)
add_library(net STATIC net.cpp) # libnet.a
add_library(net SHARED net.cpp) # libnet.so / .dll
add_library(net OBJECT net.cpp) # object files only, no archive
add_library(net INTERFACE) # header-only; no compiled artifact
add_library(net) # type follows BUILD_SHARED_LIBS
# default: STATIC
target_link_libraries(app PRIVATE net)
Tests use CTest:
include(CTest)
enable_testing()
add_executable(net_test net_test.cpp)
target_link_libraries(net_test PRIVATE net GTest::gtest_main)
add_test(NAME net_test COMMAND net_test)
Run with ctest --test-dir build --output-on-failure.
3.2 PUBLIC, PRIVATE, INTERFACE
target_link_libraries, target_include_directories, target_compile_definitions, target_compile_options, and target_compile_features all take a visibility keyword:
| Keyword | Used by this target's sources? | Propagated to consumers that link this target? |
|---|---|---|
PRIVATE |
Yes | No |
INTERFACE |
No | Yes |
PUBLIC |
Yes | Yes |
Picking the right one is the single most important skill in modern CMake. Rule of thumb:
- Implementation detail (used in
.cpponly):PRIVATE. - Appears in a public header:
PUBLIC. - Header-only library, no
.cpp:INTERFACEeverywhere.
Getting this wrong silently leaks dependencies up the graph (everyone who links you also links your private deps) or hides them (your consumer can't see a header you exposed).
4. Sources, Includes, Definitions, Options
Always attach things to a target, not to a directory.
add_library(net
src/socket.cpp
src/dns.cpp
)
target_include_directories(net
PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include # consumers see <net/socket.h>
PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src # internal headers
)
target_compile_definitions(net
PUBLIC NET_VERSION=2
PRIVATE NET_INTERNAL_DEBUG=1
)
target_compile_options(net PRIVATE
-Wall -Wextra -Wpedantic
)
target_compile_features(net PUBLIC cxx_std_20) # cleaner than CMAKE_CXX_STANDARD
target_compile_features requesting a language standard at the target level beats setting CMAKE_CXX_STANDARD globally — it travels with the target if the project is consumed via add_subdirectory or find_package.
Don't file(GLOB) to collect sources. CMake won't notice when files are added or removed unless you reconfigure. Either list .cpp files explicitly (gives you a clear inventory in git diff) or use file(GLOB CONFIGURE_DEPENDS ...) and accept a configure-time scan on every build.
Don't write to CMAKE_CXX_FLAGS from inside the project, and don't bake in -O2 / -g yourself. Those settings belong to the user's toolchain or build type — overwriting them surprises packagers and breaks cross-compilation. Use target_compile_options for target-specific flags and let CMAKE_BUILD_TYPE drive optimization level.
For include directories that need to behave differently in the build tree vs after install (very common for libraries), use generator expressions:
target_include_directories(net PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include>
)
5. Build Configuration
5.1 Standard, Generators, Build Types
cmake -S . -B build -G Ninja # use Ninja (recommended)
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release # single-config generators
cmake --build build --config Release # multi-config (VS, Xcode)
Standard build types: Debug, Release, RelWithDebInfo, MinSizeRel. Single-config generators (Make, Ninja) bake the type at configure time. Multi-config generators (Visual Studio, Xcode, "Ninja Multi-Config") pick the type at build time.
# Default to Release if the user didn't pick one (single-config only)
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
set(CMAKE_BUILD_TYPE Release CACHE STRING "" FORCE)
endif()
5.2 User Options
option(MYAPP_BUILD_TESTS "Build unit tests" ON)
option(MYAPP_USE_TBB "Enable TBB-based parallelism" OFF)
if(MYAPP_BUILD_TESTS)
enable_testing()
add_subdirectory(tests)
endif()
Pass at configure time:
cmake -S . -B build -DMYAPP_BUILD_TESTS=OFF -DMYAPP_USE_TBB=ON
Prefix project options (MYAPP_*) so they don't collide when consumed via add_subdirectory.
6. Finding and Pulling Dependencies
6.1 find_package
For libraries already installed on the system:
find_package(Threads REQUIRED)
find_package(fmt 10 CONFIG REQUIRED)
find_package(Boost 1.81 REQUIRED COMPONENTS system filesystem)
target_link_libraries(app PRIVATE
Threads::Threads
fmt::fmt
Boost::system Boost::filesystem
)
CMake locates the package via either a config file shipped by the library (<pkg>Config.cmake) or a Find-module shipped with CMake (Find<pkg>.cmake). Config mode is preferred when available — Find-modules are a legacy fallback.
Always link the imported target (fmt::fmt), not a raw variable like ${FMT_LIBRARIES}. Imported targets carry include dirs, compile flags, and transitive deps with them.
6.2 FetchContent
To pull a dependency at configure time and build it as part of your project:
include(FetchContent)
FetchContent_Declare(
fmt
GIT_REPOSITORY https://github.com/fmtlib/fmt.git
GIT_TAG 10.2.1
GIT_SHALLOW TRUE
)
FetchContent_MakeAvailable(fmt)
target_link_libraries(app PRIVATE fmt::fmt)
FetchContent_MakeAvailable (CMake 3.14+) clones, then add_subdirectorys the dep, exposing its targets to your project.
6.3 add_subdirectory
For sibling subprojects in your own monorepo:
add_subdirectory(libs/net)
add_subdirectory(apps/server)
target_link_libraries(server PRIVATE net)
7. Build, Test, Install Commands
# Configure
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release
# Build (parallel; -j without a number uses all cores)
cmake --build build -j
# Build a specific target
cmake --build build --target net
# Test
ctest --test-dir build --output-on-failure -j
# Install
cmake --install build --prefix /usr/local
# Clean
cmake --build build --target clean
cmake --build is preferred over calling make/ninja directly — it works regardless of which generator was selected at configure time.
8. Install and Export
To make a library findable via find_package from another project, install the target and generate a config file.
install(TARGETS net
EXPORT netTargets
LIBRARY DESTINATION lib
ARCHIVE DESTINATION lib
RUNTIME DESTINATION bin
INCLUDES DESTINATION include
)
install(DIRECTORY include/ DESTINATION include)
install(EXPORT netTargets
FILE netTargets.cmake
NAMESPACE net::
DESTINATION lib/cmake/net
)
include(CMakePackageConfigHelpers)
configure_package_config_file(
${CMAKE_CURRENT_SOURCE_DIR}/cmake/netConfig.cmake.in
${CMAKE_CURRENT_BINARY_DIR}/netConfig.cmake
INSTALL_DESTINATION lib/cmake/net
)
write_basic_package_version_file(
${CMAKE_CURRENT_BINARY_DIR}/netConfigVersion.cmake
VERSION ${PROJECT_VERSION}
COMPATIBILITY SameMajorVersion
)
install(FILES
${CMAKE_CURRENT_BINARY_DIR}/netConfig.cmake
${CMAKE_CURRENT_BINARY_DIR}/netConfigVersion.cmake
DESTINATION lib/cmake/net
)
Downstream consumers then write:
find_package(net 2 CONFIG REQUIRED)
target_link_libraries(myapp PRIVATE net::net)
The net:: namespace is convention — it makes it obvious in linker errors that net::net is an imported target, not a system library.
9. References
- CMake official docs — language reference, command and variable list.
- Professional CMake by Craig Scott — the practical reference for modern CMake.
- Effective Modern CMake (gist by Manuel Binna) — concise list of dos and don'ts.
- It's Time to Do CMake Right (Pablo Arias) — the article that popularized the target-centric approach.