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 describes build at high level; cmake reads + emits files for underlying tool — Makefiles, Ninja, VS, Xcode. Selected with -G <generator>. Same project targets any.
  • Modern CMake (3.x, esp. 3.15+) — target-centric: each target declares its own sources, headers, flags, deps; consumers inherit.
  • Older directory-scoped style (include_directories, add_definitions, link_directories) still supported but leaks 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 (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 — lowest version your code actually requires. Set to what you can realistically expect on contributor machines. Bumping later = 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 via 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: ctest --test-dir build --output-on-failure.

3.2 PUBLIC, PRIVATE, INTERFACE

  • target_link_libraries, target_include_directories, target_compile_definitions, target_compile_options, target_compile_features — all take 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 right one = single most important skill in modern CMake. Rules of thumb:

  • Implementation detail (used in .cpp only) → PRIVATE.

  • Appears in public headerPUBLIC.

  • Header-only library, no .cppINTERFACE everywhere.

  • Wrong choice silently leaks deps up the graph (everyone linking you also links your private deps) or hides them (consumer can't see exposed header).

4. Sources, Includes, Definitions, Options

  • Always attach things to a target, not 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 for language standard at target level beats CMAKE_CXX_STANDARD globally — travels with target when consumed via add_subdirectory or find_package.

  • Don't file(GLOB) to collect sources. CMake won't notice when files added/removed without reconfigure. Either list .cpp explicitly (clear inventory in git diff) or file(GLOB CONFIGURE_DEPENDS ...) (configure-time scan on every build).

  • Don't write CMAKE_CXX_FLAGS from inside project. Don't bake -O2 / -g yourself. Those belong to user's toolchain / build type — overwriting surprises packagers, breaks cross-compile.

  • Use target_compile_options for target-specific flags; let CMAKE_BUILD_TYPE drive optimization level.

  • Include dirs needing different behavior in build tree vs install (common for libs) — 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 types: Debug, Release, RelWithDebInfo, MinSizeRel.
  • Single-config (Make, Ninja) — type baked at configure time.
  • Multi-config (VS, Xcode, "Ninja Multi-Config") — type picked 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_*) → no collision when consumed via add_subdirectory.

6. Finding and Pulling Dependencies

6.1 find_package

  • For already-installed system libs:
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
)
  • Locates package via lib's config file (<pkg>Config.cmake) or CMake's Find-module (Find<pkg>.cmake).
  • Config mode preferred. Find-modules = legacy fallback.
  • Always link imported target (fmt::fmt), not raw ${FMT_LIBRARIES}. Imported targets carry include dirs, flags, transitive deps.

6.2 FetchContent

  • Pull dep at configure time, build 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, exposes its targets.

6.3 add_subdirectory

  • For sibling subprojects in your 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 preferred over calling make/ninja directly — works regardless of generator.

8. Install and Export

  • To make a lib findable via find_package from another project — install target and generate 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:
find_package(net 2 CONFIG REQUIRED)
target_link_libraries(myapp PRIVATE net::net)
  • net:: namespace = convention. Makes linker errors obvious — net::net is imported target, not system library.

9. References