Search Knowledge

© 2026 LIBREUNI PROJECT

Modern C++ Programming / Tools and Practices

CMake and Build Systems

Modern C++ Build Systems

CMake has become the de facto standard for C++ build configuration. It generates platform-specific build files and manages dependencies, compiler flags, and project structure.

Basic CMakeLists.txt

cmake_minimum_required(VERSION 3.20)
project(MyProject VERSION 1.0.0 LANGUAGES CXX)

# Set C++ standard
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)

# Create executable
add_executable(myapp main.cpp)

Build process:

mkdir build
cd build
cmake ..
cmake --build .

Project Structure

project/
├── CMakeLists.txt
├── src/
│   ├── main.cpp
│   ├── utils.cpp
│   └── utils.h
├── include/
│   └── mylib/
│       └── api.h
├── tests/
│   └── test_main.cpp
└── build/  (generated)

Root CMakeLists.txt:

cmake_minimum_required(VERSION 3.20)
project(MyProject)

set(CMAKE_CXX_STANDARD 20)

# Include directories
include_directories(include)

# Source files
add_executable(myapp
    src/main.cpp
    src/utils.cpp
)

# Enable testing
enable_testing()
add_subdirectory(tests)

Libraries

Static Library

add_library(mylib STATIC
    src/lib.cpp
    src/helper.cpp
)

target_include_directories(mylib PUBLIC include)

# Link library to executable
add_executable(myapp src/main.cpp)
target_link_libraries(myapp PRIVATE mylib)

Shared Library

add_library(mylib SHARED
    src/lib.cpp
)

target_include_directories(mylib PUBLIC
    $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
    $<INSTALL_INTERFACE:include>
)

Header-Only Library

add_library(mylib INTERFACE)
target_include_directories(mylib INTERFACE include)

Target Properties

Modern CMake uses target-based configuration:

add_library(mylib src/lib.cpp)

# Include directories
target_include_directories(mylib
    PUBLIC include           # Consumers need these
    PRIVATE src/internal     # Only this target needs these
)

# Compile definitions
target_compile_definitions(mylib
    PUBLIC API_VERSION=2
    PRIVATE INTERNAL_DEBUG
)

# Compile options
target_compile_options(mylib
    PRIVATE -Wall -Wextra -Wpedantic
)

# Link libraries
target_link_libraries(mylib
    PUBLIC fmt::fmt          # Consumers need this too
    PRIVATE spdlog::spdlog   # Only this target needs this
)

Visibility:

  • PUBLIC: Used by this target and propagated to consumers
  • PRIVATE: Used only by this target
  • INTERFACE: Not used by this target, only propagated to consumers

Compiler Flags

# Global flags (discouraged)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall")

# Target-specific flags (preferred)
target_compile_options(myapp PRIVATE
    $<$<CXX_COMPILER_ID:GNU>:-Wall -Wextra>
    $<$<CXX_COMPILER_ID:Clang>:-Wall -Wextra>
    $<$<CXX_COMPILER_ID:MSVC>:/W4>
)

Build Types

# Set default build type
if(NOT CMAKE_BUILD_TYPE)
    set(CMAKE_BUILD_TYPE Release)
endif()

# Build type specific flags
set(CMAKE_CXX_FLAGS_DEBUG "-g -O0")
set(CMAKE_CXX_FLAGS_RELEASE "-O3 -DNDEBUG")

Build with specific type:

cmake -DCMAKE_BUILD_TYPE=Debug ..
cmake -DCMAKE_BUILD_TYPE=Release ..

Finding Packages

find_package

find_package(Boost 1.75 REQUIRED COMPONENTS system filesystem)

add_executable(myapp main.cpp)
target_link_libraries(myapp
    Boost::system
    Boost::filesystem
)

pkg-config

find_package(PkgConfig REQUIRED)
pkg_check_modules(LIBXML libxml-2.0 REQUIRED)

target_include_directories(myapp PRIVATE ${LIBXML_INCLUDE_DIRS})
target_link_libraries(myapp PRIVATE ${LIBXML_LIBRARIES})

FetchContent: Modern Dependency Management

Download and build dependencies at configure time:

include(FetchContent)

FetchContent_Declare(
    fmt
    GIT_REPOSITORY https://github.com/fmtlib/fmt.git
    GIT_TAG 9.1.0
)

FetchContent_MakeAvailable(fmt)

add_executable(myapp main.cpp)
target_link_libraries(myapp PRIVATE fmt::fmt)

Multiple dependencies:

FetchContent_Declare(
    googletest
    GIT_REPOSITORY https://github.com/google/googletest.git
    GIT_TAG v1.14.0
)

FetchContent_Declare(
    spdlog
    GIT_REPOSITORY https://github.com/gabime/spdlog.git
    GIT_TAG v1.12.0
)

FetchContent_MakeAvailable(googletest spdlog)

Generator Expressions

Conditional compilation based on configuration:

target_compile_definitions(myapp PRIVATE
    $<$<CONFIG:Debug>:DEBUG_MODE>
    $<$<CONFIG:Release>:RELEASE_MODE>
)

target_link_libraries(myapp PRIVATE
    $<$<PLATFORM_ID:Windows>:ws2_32>
    $<$<PLATFORM_ID:Linux>:pthread>
)

Testing with CTest

enable_testing()

add_executable(test_main tests/test_main.cpp)
target_link_libraries(test_main PRIVATE mylib GTest::gtest_main)

add_test(NAME MainTests COMMAND test_main)

Run tests:

cmake --build build
ctest --test-dir build

Installation

install(TARGETS myapp mylib
    RUNTIME DESTINATION bin
    LIBRARY DESTINATION lib
    ARCHIVE DESTINATION lib
)

install(DIRECTORY include/
    DESTINATION include
)

Install to custom location:

cmake -DCMAKE_INSTALL_PREFIX=/usr/local ..
cmake --install build

Presets (CMake 3.19+)

CMakePresets.json:

{
    "version": 3,
    "configurePresets": [
        {
            "name": "debug",
            "binaryDir": "build/debug",
            "cacheVariables": {
                "CMAKE_BUILD_TYPE": "Debug"
            }
        },
        {
            "name": "release",
            "binaryDir": "build/release",
            "cacheVariables": {
                "CMAKE_BUILD_TYPE": "Release"
            }
        }
    ],
    "buildPresets": [
        {
            "name": "debug",
            "configurePreset": "debug"
        },
        {
            "name": "release",
            "configurePreset": "release"
        }
    ]
}

Use presets:

cmake --preset debug
cmake --build --preset debug

Package Management: vcpkg and Conan

vcpkg

vcpkg install fmt spdlog
cmake -DCMAKE_TOOLCHAIN_FILE=/path/to/vcpkg/scripts/buildsystems/vcpkg.cmake ..

Conan

conanfile.txt:

[requires]
fmt/9.1.0
spdlog/1.12.0

[generators]
CMakeDeps
CMakeToolchain
conan install . --build=missing
cmake -DCMAKE_TOOLCHAIN_FILE=build/conan_toolchain.cmake ..

Modern CMake Best Practices

  1. Use targets, not variables: Prefer target_* commands
  2. Avoid global commands: Use target-specific settings
  3. Proper visibility: PUBLIC/PRIVATE/INTERFACE
  4. Minimum version: Set cmake_minimum_required appropriately
  5. Out-of-source builds: Always build in separate directory
  6. Generator expressions: For conditional configuration
  7. Export targets: For library packages
  8. Use FetchContent: For dependencies when appropriate

Complete Example

cmake_minimum_required(VERSION 3.20)
project(ModernApp VERSION 1.0.0 LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)

# Options
option(BUILD_TESTS "Build tests" ON)
option(ENABLE_WARNINGS "Enable compiler warnings" ON)

# Dependencies
include(FetchContent)
FetchContent_Declare(
    fmt
    GIT_REPOSITORY https://github.com/fmtlib/fmt.git
    GIT_TAG 9.1.0
)
FetchContent_MakeAvailable(fmt)

# Library
add_library(applib
    src/lib.cpp
    src/utils.cpp
)

target_include_directories(applib PUBLIC include)
target_link_libraries(applib PUBLIC fmt::fmt)

if(ENABLE_WARNINGS)
    target_compile_options(applib PRIVATE
        $<$<CXX_COMPILER_ID:GNU,Clang>:-Wall -Wextra -Wpedantic>
        $<$<CXX_COMPILER_ID:MSVC>:/W4>
    )
endif()

# Executable
add_executable(myapp src/main.cpp)
target_link_libraries(myapp PRIVATE applib)

# Tests
if(BUILD_TESTS)
    enable_testing()
    add_subdirectory(tests)
endif()

# Installation
install(TARGETS myapp applib
    RUNTIME DESTINATION bin
    LIBRARY DESTINATION lib
    ARCHIVE DESTINATION lib
)
Conceptual Check

What is the recommended way to set compiler flags in modern CMake?

Interactive Lab

Complete the Code

# Link fmt library to myapp target
add_executable(myapp main.cpp)
(myapp PRIVATE fmt::fmt)