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

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 .cpp only): PRIVATE.
  • Appears in a public header: PUBLIC.
  • Header-only library, no .cpp: INTERFACE everywhere.

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