C++ CMake Tutorial: Build Your First Project
Learn modern CMake for C++ — targets, properties, and dependencies — and configure a small project that compiles cleanly across platforms.
What you'll learn
- ✓Write a minimal modern CMakeLists.txt
- ✓Define libraries and executables as targets
- ✓Use target_link_libraries and target_include_directories
- ✓Manage dependencies with FetchContent
- ✓Build and configure across platforms
Prerequisites
- •Install and first program — see /blog/cpp-install-and-first-program
CMake is the de facto build system for C++. It generates platform-native build files — Makefiles, Ninja, Visual Studio projects — from a single description. Modern CMake (3.15+) focuses on targets and properties rather than global variables, and it is far less painful than the CMake of a decade ago.
What and Why
A C++ build orchestrates a compiler, a linker, include paths, defines, optimization flags, and library dependencies — separately for debug and release, possibly across operating systems. Writing this by hand is tedious and error-prone. CMake describes the project once and produces the right build commands for each environment.
You will encounter CMake in nearly every open-source C++ project. Reading and writing it confidently is a baseline skill.
Mental Model
Think of CMake as a graph of targets. A target is an executable or a library. Each target has properties: sources, include directories, link dependencies, compile features. Properties are either private (used only when building the target), interface (exposed to consumers), or public (both).
The golden rule of modern CMake: never set global variables for include paths or flags. Instead, attach everything to a target.
Hands-on Example
Build a small library and a program that uses it.
cmake_minimum_required(VERSION 3.20)
project(HelloApp LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
add_library(greet STATIC src/greet.cpp)
target_include_directories(greet PUBLIC include)
add_executable(app src/main.cpp)
target_link_libraries(app PRIVATE greet)
With files include/greet.hpp, src/greet.cpp, and src/main.cpp, you can configure and build:
cmake -B build -S .
cmake --build build
./build/app
┌──────────────┐
│ app (exe) │
└──────┬───────┘
│ link
v
┌──────────────┐ PUBLIC includes ──> consumers see /include
│ greet (lib) │ PRIVATE sources ──> only build greet itself
└──────────────┘ The library exposes its include directory PUBLIC, so the executable automatically inherits it. No global include_directories() needed.
Common Pitfalls
A frequent mistake is using include_directories() or add_definitions() at the top of the file. These set state for everything below — fine for tiny projects, painful at scale. Use target_include_directories() and target_compile_definitions() instead.
Another pitfall is committing the build directory. Always build out-of-source with -B build and add build/ to .gitignore.
Forgetting target_link_libraries(app PRIVATE greet) produces baffling linker errors. The verb here is “link with” — it pulls in not just libraries but transitive include paths and compile options.
Mixing PRIVATE, INTERFACE, and PUBLIC incorrectly leads to either hidden symbols or accidentally exposed implementation headers. The rule: use PUBLIC only when consumers genuinely need the property.
Practical Tips
Use Ninja as the generator when possible. It is faster than Make and works on every platform: cmake -B build -G Ninja.
Set warnings as a target property, not a global flag: target_compile_options(greet PRIVATE -Wall -Wextra -Wpedantic).
For external dependencies, prefer FetchContent for small libs and a package manager (vcpkg or Conan) for bigger ones. Avoid git submodule for build inputs unless you really need it.
Split your CMakeLists.txt once the project grows. Each subdirectory can have its own file, included via add_subdirectory().
Use presets (CMakePresets.json) to capture common configure and build options so contributors do not need to remember flags.
Wrap-up
Modern CMake is target-centric: define libraries and executables, attach properties to them, and let consumers inherit what they need. With a cmake_minimum_required of 3.20 or higher, a clear separation of PRIVATE and PUBLIC, and an out-of-source build directory, you have a foundation that scales from a single file to a multi-module project across platforms.
Related articles
- C++ C++ Control Flow: if, for, while, switch
Master C++ control flow with if-else, for and range-for loops, while, do-while, switch, and the modern init-statement syntax for conditions.
- C++ C++ Functions, References, and Pass-By-Value
Write C++ functions that pass data correctly: by value, by reference, by const reference, with overloading, default arguments, and clear ownership.
- C++ Install C++ and Compile Your First Program
Set up a modern C++ toolchain on macOS, Linux, or Windows, then compile and run your first program with g++ or clang++ from the command line.
- C++ C++ Variables and Fundamental Types
Understand C++ fundamental types, fixed-width integers, floating point, char, bool, const, and how initialization actually works in modern C++.