Understanding CMake Through Code
This file is a companion to CMakeLists.txt. It's a runnable program that demonstrates how CMake settings translate to compiler behavior. Read this alongside the CMakeLists.txt for a complete picture.
HOW TO BUILD THIS FILE:
Method 1: With CMake (recommended) mkdir build cd build cmake .. -G Ninja (or -G "Unix Makefiles" or omit -G) cmake --build . ./cmake_explained (Linux/Mac) cmake_explained.exe (Windows)
Method 2: Direct compilation (what CMake generates internally) g++ -std=c++20 -Wall -Wextra -o cmake_explained cmake_explained.cpp cl /std:c++20 /W4 /EHsc cmake_explained.cpp (MSVC)
Prerequisites: None (this is a standalone explanation file).
WHAT CMAKE DOES BEHIND THE SCENES
When you write this in CMakeLists.txt:
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)
add_executable(my_app main.cpp utils.cpp)
target_compile_options(my_app PRIVATE -Wall -Wextra)
target_link_libraries(my_app PRIVATE pthread)
CMake generates roughly this compiler command:
g++ -std=c++20 -Wall -Wextra -o my_app main.cpp utils.cpp -lpthread
But CMake does much MORE than just generate the command:
1. Dependency tracking: if utils.cpp changes, only utils.o is
recompiled, then the final link step reruns. main.o is reused.
2. Header dependency scanning: if main.cpp #includes "utils.h",
and utils.h changes, main.o is rebuilt too. CMake tracks this
automatically using compiler-generated dependency files (.d files).
3. Parallel builds: CMake tells the build tool (Ninja/Make) which
files are independent, so main.o and utils.o compile in parallel.
4. Incremental builds: only changed files are rebuilt. On a large
project (thousands of files), this saves minutes of build time.
5. Cross-platform: the same CMakeLists.txt generates Visual Studio
projects on Windows and Makefiles on Linux. PROJECT STRUCTURE BEST PRACTICES
A well-organized C++ project typically looks like:
MyProject/
├── CMakeLists.txt <- Root build file
├── CMakePresets.json <- Build presets (optional, CMake 3.21+)
├── README.md
├── .gitignore <- Include "build/" here!
│
├── src/ <- Implementation files (.cpp)
│ ├── CMakeLists.txt <- Subdirectory build rules
│ ├── main.cpp
│ └── utils.cpp
│
├── include/ <- Public headers (.h / .hpp)
│ └── myproject/
│ └── utils.hpp
│
├── tests/ <- Test files
│ ├── CMakeLists.txt
│ └── test_utils.cpp
│
├── external/ <- Third-party dependencies
│ └── ...
│
└── build/ <- Build output (NOT in git)
└── ...
Key conventions:
- Headers in include/<project_name>/ so #include is unambiguous:
#include <myproject/utils.hpp>
- Source in src/, tests in tests/, each with their own CMakeLists.txt
- build/ is always in .gitignore
- Use add_subdirectory() to include sub-CMakeLists.txt files COMMON CMAKE MISTAKES AND HOW TO AVOID THEM
MISTAKE 1: Using include_directories() instead of target_include_directories()
BAD: include_directories(include/) <- affects ALL targets
GOOD: target_include_directories(my_lib PUBLIC include/) <- affects only my_lib
MISTAKE 2: Not setting CXX_STANDARD_REQUIRED
BAD: set(CMAKE_CXX_STANDARD 20) <- silently falls back if unsupported
GOOD: set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON) <- error if C++20 not available
MISTAKE 3: Building in the source directory
BAD: cd MyProject && cmake . <- pollutes source with build files
GOOD: cd MyProject && mkdir build && cd build && cmake ..
MISTAKE 4: Hardcoding compiler paths or flags
BAD: set(CMAKE_CXX_COMPILER /usr/bin/g++-12) <- breaks on other machines
GOOD: Let the user choose: cmake .. -DCMAKE_CXX_COMPILER=g++-12
MISTAKE 5: Not using generator expressions for cross-compiler flags
BAD: target_compile_options(app PRIVATE -Wall) <- fails on MSVC
GOOD: target_compile_options(app PRIVATE
$<$<NOT:$<CXX_COMPILER_ID:MSVC>>:-Wall>
$<$<CXX_COMPILER_ID:MSVC>:/W4>
)
MISTAKE 6: Forgetting to link threading on Linux
The program compiles fine but crashes at runtime with threading.
GOOD: find_package(Threads REQUIRED)
target_link_libraries(app PRIVATE Threads::Threads) int main() {
std::cout << "=== CMake Build System Explained ===\n\n";
// --- Show what the compiler set for us ---
std::cout << "--- Compiler information ---\n";
#if defined(_MSC_VER)
std::cout << std::format(" Compiler: MSVC {}\n", _MSC_VER);
#elif defined(__clang__)
std::cout << std::format(" Compiler: Clang {}.{}.{}\n",
__clang_major__, __clang_minor__, __clang_patchlevel__);
#elif defined(__GNUC__)
std::cout << std::format(" Compiler: GCC {}.{}.{}\n",
__GNUC__, __GNUC_MINOR__, __GNUC_PATCHLEVEL__);
#else
std::cout << " Compiler: Unknown\n";
#endif
// --- C++ standard version ---
// __cplusplus is set by the compiler to indicate the standard version.
// CMake's CMAKE_CXX_STANDARD = 20 translates to -std=c++20 or /std:c++20,
// which makes the compiler set __cplusplus accordingly.
std::cout << std::format(" __cplusplus = {}\n", __cplusplus);
// Expected values:
// 199711L = C++98/03
// 201103L = C++11
// 201402L = C++14
// 201703L = C++17
// 202002L = C++20
// 202302L = C++23
// Note: MSVC historically reported 199711L regardless of the actual
// standard unless /Zc:__cplusplus was set. Modern MSVC with /std:c++20
// should report correctly.
// --- Build type ---
// CMake has build types that control optimization:
// Debug: -g -O0 (debuggable, slow)
// Release: -O3 -DNDEBUG (fast, no asserts)
// RelWithDebInfo: -O2 -g (fast with debug info)
// MinSizeRel: -Os -DNDEBUG (small binary)
//
// Set with: cmake .. -DCMAKE_BUILD_TYPE=Release
std::cout << "\n--- Build configuration ---\n";
#ifdef NDEBUG
std::cout << " Build type: Release (NDEBUG defined, asserts disabled)\n";
#else
std::cout << " Build type: Debug (asserts enabled)\n";
#endif
// --- Platform detection ---
std::cout << "\n--- Platform ---\n";
#if defined(_WIN32)
std::cout << " Platform: Windows\n";
#elif defined(__linux__)
std::cout << " Platform: Linux\n";
#elif defined(__APPLE__)
std::cout << " Platform: macOS\n";
#else
std::cout << " Platform: Unknown\n";
#endif
// --- Quick build command reference ---
std::cout << "\n--- Quick reference: building with CMake ---\n";
std::cout << " 1. mkdir build && cd build\n";
std::cout << " 2. cmake .. (generate build files)\n";
std::cout << " 3. cmake --build . (compile everything)\n";
std::cout << " 4. cmake --build . --target X (compile just target X)\n";
std::cout << " 5. ctest (run tests)\n";
std::cout << " 6. cmake --install . (install to system)\n";
std::cout << "\n--- Quick reference: useful CMake flags ---\n";
std::cout << " cmake .. -G Ninja (use Ninja generator)\n";
std::cout << " cmake .. -DCMAKE_BUILD_TYPE=Release (optimize for speed)\n";
std::cout << " cmake .. -DCMAKE_CXX_COMPILER=clang++ (choose compiler)\n";
std::cout << " cmake .. -DCMAKE_EXPORT_COMPILE_COMMANDS=ON (for IDE/linters)\n";
return 0;
}