CMake: A Complete Guide to the C++ Build System
CMake is the de facto standard build system for C++ projects. It generates native build files (Makefiles, Ninja, Visual Studio solutions) from a single CMakeLists.txt configuration. This means you write your build configuration once and build on any platform.
This guide covers modern CMake (3.15+) — targets, dependencies, testing, installation, and cross-platform builds using best practices from the professional C++ ecosystem.
Getting Started
Hello World with CMake
# CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
project(MyApp VERSION 1.0.0 LANGUAGES CXX)
add_executable(myapp main.cpp)# Configure and build
cmake -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build
./build/myappThe cmake_minimum_required sets the minimum CMake version. project() declares the project name, version, and languages. add_executable creates a build target from source files.
Modern CMake: Targets and Properties
Modern CMake revolves around targets — executables, libraries, and custom utilities. Properties are attached to targets, not stored in global variables:
add_library(mylib STATIC
src/parser.cpp
src/tokenizer.cpp
src/ast.cpp
)
# Set include directories for this target
target_include_directories(mylib
PUBLIC include
PRIVATE src
)
# Link dependencies
target_link_libraries(mylib
PUBLIC fmt::fmt
PRIVATE nlohmann_json::nlohmann_json
)Target Types
| Command | Type |
|---|---|
add_executable | Executable |
add_library(... STATIC) | Static library (.a / .lib) |
add_library(... SHARED) | Shared library (.so / .dll) |
add_library(... INTERFACE) | Header-only library |
add_library(... OBJECT) | Object library (compiled but not archived) |
Visibility Keywords
- PUBLIC — Used by the target and by consumers that link to it
- PRIVATE — Used only by the target itself
- INTERFACE — Used only by consumers, not the target itself
target_include_directories(mylib
PUBLIC include/mylib # Exposed to downstream targets
PRIVATE src # Internal only
)
target_compile_features(mylib
PUBLIC cxx_std_17 # Downstream targets must use C++17 too
)Libraries and Dependencies
Finding External Libraries
# Find a system-installed package
find_package(fmt REQUIRED)
find_package(Boost REQUIRED COMPONENTS filesystem system)
# Find a specific version
find_package(OpenCV 4.5 REQUIRED)
# Link to your target
target_link_libraries(myapp
PRIVATE fmt::fmt
PRIVATE Boost::filesystem
PRIVATE ${OpenCV_LIBS}
)FetchContent — Download Dependencies at Build Time
include(FetchContent)
FetchContent_Declare(
nlohmann_json
GIT_REPOSITORY https://github.com/nlohmann/json.git
GIT_TAG v3.11.2
)
FetchContent_MakeAvailable(nlohmann_json)
target_link_libraries(myapp PRIVATE nlohmann_json::nlohmann_json)FetchContent downloads and builds dependencies as part of your project. This is the modern replacement for ExternalProject_Add when you want the dependency to be part of your build.
CMake Package Registry
Add your own package to the registry for other projects to find:
install(TARGETS mylib EXPORT MyLibTargets)
install(EXPORT MyLibTargets
FILE MyLibTargets.cmake
NAMESPACE mylib::
DESTINATION lib/cmake/MyLib
)Compiler Options and Configurations
Setting Standards
# Require C++17
target_compile_features(mylib PUBLIC cxx_std_17)
# Or set standard explicitly
set_target_properties(mylib PROPERTIES
CXX_STANDARD 17
CXX_STANDARD_REQUIRED ON
CXX_EXTENSIONS OFF
)Platform-Specific Flags
if(MSVC)
target_compile_options(mylib PRIVATE /W4 /utf-8)
target_link_options(mylib PRIVATE /LTCG)
else()
target_compile_options(mylib PRIVATE -Wall -Wextra -Wpedantic -Werror)
target_link_options(mylib PRIVATE -flto)
endif()
if(APPLE)
target_link_options(mylib PRIVATE -framework CoreFoundation)
elseif(WIN32)
target_link_options(mylib PRIVATE /SUBSYSTEM:CONSOLE)
endif()Build Types
# Set default build type if not specified
if(NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE Release CACHE STRING "Build type" FORCE)
endif()
# Custom compile options per configuration
target_compile_definitions(mylib
PRIVATE
$<$<CONFIG:Debug>:DEBUG_BUILD>
$<$<CONFIG:Release>:NDEBUG>
)
# Optimizations per config
set_target_properties(mylib PROPERTIES
INTERPROCEDURAL_OPTIMIZATION_RELEASE ON
INTERPROCEDURAL_OPTIMIZATION_RELWITHDEBINFO ON
)Testing with CTest and GoogleTest
Registering Tests
# CMakeLists.txt
enable_testing()
add_subdirectory(tests)# tests/CMakeLists.txt
find_package(GTest REQUIRED)
add_executable(test_parser
test_parser.cpp
test_tokenizer.cpp
)
target_link_libraries(test_parser
PRIVATE mylib
PRIVATE GTest::gtest GTest::gtest_main
)
target_compile_definitions(test_parser PRIVATE $<$<CONFIG:Debug>:DEBUG_BUILD>)
include(GoogleTest)
gtest_discover_tests(test_parser)# Build and run tests
cmake --build build
cd build && ctest --output-on-failure
# Run specific test
ctest -R parser --verbose
# Run with parallel workers
ctest -j8Custom Test Targets
add_test(NAME integration_test
COMMAND $<TARGET_FILE:test_runner> --config ${CMAKE_SOURCE_DIR}/tests/config.json
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/tests
)
set_tests_properties(integration_test PROPERTIES
TIMEOUT 30
ENVIRONMENT "DATABASE_URL=sqlite::memory:"
LABELS "integration;slow"
)Installation
Installing Targets
# Library installation
install(TARGETS mylib
EXPORT MyLibTargets
LIBRARY DESTINATION lib # Shared libs
ARCHIVE DESTINATION lib # Static libs
RUNTIME DESTINATION bin # DLLs on Windows
INCLUDES DESTINATION include
)
# Header installation
install(DIRECTORY include/mylib
DESTINATION include
FILES_MATCHING PATTERN "*.hpp"
)
# Executable installation
install(TARGETS myapp RUNTIME DESTINATION bin)
# Config file installation
install(FILES config.ini DESTINATION etc/myapp)CPack Packaging
# Enable packaging
set(CPACK_GENERATOR "TGZ;DEB;RPM")
set(CPACK_PACKAGE_NAME "mylib")
set(CPACK_PACKAGE_VERSION "${PROJECT_VERSION}")
set(CPACK_DEBIAN_PACKAGE_MAINTAINER "developer@example.com")
include(CPack)
# Build packages
# cpack -G DEB -C ReleaseCross-Platform Considerations
Platform Detection
if(WIN32)
add_definitions(-DWIN32_LEAN_AND_MEAN)
target_link_libraries(myapp PRIVATE ws2_32)
elseif(APPLE)
set_target_properties(myapp PROPERTIES
MACOSX_BUNDLE TRUE
MACOSX_BUNDLE_BUNDLE_VERSION ${PROJECT_VERSION}
)
elseif(UNIX)
target_link_libraries(myapp PRIVATE pthread)
endif()Generator Expressions
Generator expressions evaluate at build time and work across generators:
# Link different libraries on different platforms
target_link_libraries(myapp
PRIVATE
$<$<PLATFORM_ID:Linux>:rt>
$<$<PLATFORM_ID:Windows>:winmm>
)
# Different definitions per config
target_compile_definitions(myapp PRIVATE
$<$<CONFIG:Debug>:ENABLE_LOGGING>
$<$<CONFIG:Release>:NDEBUG>
)
# Include path only if exists
target_include_directories(myapp PRIVATE
$<$<BOOL:${USE_GPU}>:${CUDA_INCLUDE_DIRS}>
)Organizing Larger Projects
project/
├── CMakeLists.txt # Root
├── cmake/
│ ├── FindSomething.cmake # Custom find modules
│ └── Options.cmake # Option definitions
├── src/
│ ├── CMakeLists.txt
│ ├── main.cpp
│ └── app/
│ ├── CMakeLists.txt
│ └── app.cpp
├── lib/
│ ├── CMakeLists.txt
│ └── core/
│ ├── CMakeLists.txt
│ └── core.cpp
├── tests/
│ ├── CMakeLists.txt
│ ├── test_core.cpp
│ └── test_app.cpp
└── extern/
└── CMakeLists.txt # FetchContent callsRoot CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
project(MyProject VERSION 1.0.0 LANGUAGES CXX)
# Options
option(BUILD_TESTS "Build tests" ON)
option(BUILD_EXAMPLES "Build examples" OFF)
# Dependencies
add_subdirectory(extern)
# Libraries
add_subdirectory(lib)
# Executable
add_subdirectory(src)
# Tests
if(BUILD_TESTS)
enable_testing()
add_subdirectory(tests)
endif()Advanced Features
Custom Commands
# Generate a file at build time
add_custom_command(
OUTPUT ${CMAKE_SOURCE_DIR}/generated/version.h
COMMAND ${CMAKE_COMMAND}
-DOUTPUT_FILE=${CMAKE_SOURCE_DIR}/generated/version.h
-DVERSION=${PROJECT_VERSION}
-P ${CMAKE_SOURCE_DIR}/cmake/gen_version.cmake
DEPENDS ${CMAKE_SOURCE_DIR}/cmake/gen_version.cmake
COMMENT "Generating version header"
)
add_executable(myapp
main.cpp
${CMAKE_SOURCE_DIR}/generated/version.h
)Presets (CMake 3.19+)
# CMakePresets.json
{
"version": 3,
"configurePresets": [
{
"name": "default",
"generator": "Ninja",
"binaryDir": "${sourceDir}/build",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Release",
"BUILD_TESTS": "ON"
}
},
{
"name": "debug",
"inherits": "default",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug"
}
}
],
"buildPresets": [
{ "name": "default", "configurePreset": "default" },
{ "name": "debug", "configurePreset": "debug" }
]
---cmake --preset debug
cmake --build --preset debugBest Practices
- Use
target_*commands, not global ones —target_include_directoriesoverinclude_directories. Target-based commands compose correctly and respect encapsulation. - Prefer
FetchContentfor dependencies — It integrates smoothly into your build. AvoidExternalProject_Addunless you need a separate configure step. - Create INTERFACE targets for header-only libraries — They propagate usage requirements without building anything.
- Use
cmake --buildinstead of invoking the generator directly — It works regardless of whether the generator is Make, Ninja, or Visual Studio. - Export your package — Use
install(EXPORT ...)so other projects canfind_package()your library. - Keep CMake minimum version low — Use modern CMake syntax but do not require bleeding-edge CMake versions unnecessarily.
Summary
CMake is the standard build system for C++ projects across platforms. Modern CMake uses target-based commands with PUBLIC/PRIVATE/INTERFACE visibility for correct dependency propagation. FetchContent integrates external dependencies, CTest provides testing infrastructure, and CPack handles packaging. Generator expressions make build logic portable across compilers and platforms. With presets, CMake configuration is reproducible and shareable across the team.
CMake Targets and Properties
A CMake target — created by add_executable() or add_library() — is the fundamental build unit. Targets have properties that control compilation, linking, and behavior:
add_library(my_lib STATIC src/lib.cpp)
target_include_directories(my_lib PUBLIC include)
target_compile_features(my_lib PUBLIC cxx_std_17)
target_link_libraries(my_app PRIVATE my_lib)The PUBLIC, PRIVATE, and INTERFACE keywords control how properties propagate through the dependency graph. PRIVATE applies only to the target itself. PUBLIC applies to the target and its dependents. INTERFACE applies only to dependents — perfect for header-only libraries.
Generator Expressions
Generator expressions evaluate at build time rather than configure time, enabling platform- and configuration-specific logic:
target_compile_definitions(my_app PRIVATE
$<$<CONFIG:Debug>:DEBUG_MODE=1>
$<$<PLATFORM_ID:Linux>:LINUX_BUILD>
)Common generator expressions check configuration type, platform, compiler ID, and feature availability. They are essential for cross-platform projects where different flags or source files are needed per platform.
FetchContent and External Dependencies
Modern CMake uses FetchContent to download and integrate external dependencies at configure time:
include(FetchContent)
FetchContent_Declare(
nlohmann_json
GIT_REPOSITORY https://github.com/nlohmann/json.git
GIT_TAG v3.11.2
)
FetchContent_MakeAvailable(nlohmann_json)
target_link_libraries(my_app PRIVATE nlohmann_json::nlohmann_json)This replaces older approaches like ExternalProject (which builds at build time) and git submodules, providing cleaner dependency management with version pinning and transitive dependency handling.
Cross-Compilation with Toolchain Files
CMake supports cross-compilation through toolchain files that specify the target compiler, sysroot, and architecture:
# arm-cross.cmake
set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR arm)
set(CMAKE_C_COMPILER arm-linux-gnueabihf-gcc)
set(CMAKE_CXX_COMPILER arm-linux-gnueabihf-g++)
set(CMAKE_FIND_ROOT_PATH /usr/arm-linux-gnueabihf)Pass the toolchain file with -DCMAKE_TOOLCHAIN_FILE=arm-cross.cmake to configure for the target platform.
FAQ
What is the difference between malloc and new in C++? malloc allocates raw memory without calling constructors; new allocates memory and calls the constructor. In C++, prefer new for objects. free vs delete follows the same pattern — delete calls the destructor.
How do I prevent memory leaks in C/C++? Use RAII (Resource Acquisition Is Initialization) in C++ — smart pointers like std::unique_ptr and std::shared_ptr automatically free memory. In C, always pair every malloc with a free and use tools like Valgrind or AddressSanitizer to detect leaks.
What is undefined behavior in C/C++? Undefined behavior occurs when code performs operations that the language standard does not define — dereferencing a null pointer, buffer overflow, signed integer overflow. The compiler may generate any code, including unexpected results or crashes.
Should I learn C or C++ first? Learn C first if you want to understand low-level memory and system programming. Learn C++ first if you want object-oriented features and the STL. Both are valuable; C++ builds on C concepts.
What is the difference between a header file and a source file? Header files (.h) declare interfaces — function prototypes, class definitions, macros. Source files (.c or .cpp) implement the declarations. Headers are #included; source files are compiled separately and linked.
For a comprehensive overview, read our article on C File Io Guide.
For a comprehensive overview, read our article on C Memory Management.