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
- Use targets, not variables: Prefer
target_*commands - Avoid global commands: Use target-specific settings
- Proper visibility: PUBLIC/PRIVATE/INTERFACE
- Minimum version: Set
cmake_minimum_requiredappropriately - Out-of-source builds: Always build in separate directory
- Generator expressions: For conditional configuration
- Export targets: For library packages
- 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)