C++ Standard Selection
Every modern C++ project must declare which language standard it requires. CMake provides two approaches: global variables for project-wide defaults and per-target compile features for fine-grained control. The official documentation covers the full specification.
CMAKE_CXX_STANDARD Variable
The simplest approach sets a project-wide C++ standard using cache variables:
cmake_minimum_required(VERSION 3.21)
project(MyProject LANGUAGES CXX)
# Set the C++ standard for all targets in this project
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
add_executable(app main.cpp)
CMAKE_CXX_STANDARD_REQUIRED ON to make CMake error out if the compiler doesn't support the requested standard. Without it, CMake silently falls back to an older standard. Set CMAKE_CXX_EXTENSIONS OFF to disable GNU/MSVC extensions and ensure portable code.
target_compile_features — Per-Target Standards
For library authors or projects with mixed requirements, target_compile_features() lets you specify the standard per target:
cmake_minimum_required(VERSION 3.21)
project(MixedStandards LANGUAGES CXX)
# Library requires C++17
add_library(core_lib src/core.cpp)
target_compile_features(core_lib PUBLIC cxx_std_17)
# Application uses C++20 features
add_executable(app src/main.cpp)
target_compile_features(app PRIVATE cxx_std_20)
# Another library needing C++23
add_library(modern_lib src/modern.cpp)
target_compile_features(modern_lib PUBLIC cxx_std_23)
The available meta-features are cxx_std_11, cxx_std_14, cxx_std_17, cxx_std_20, cxx_std_23, and cxx_std_26. Using PUBLIC propagates the requirement to consumers — if a target links to core_lib, it automatically gets C++17 or higher.
flowchart TD
A[app
cxx_std_20] -->|links| B[core_lib
PUBLIC cxx_std_17]
A -->|links| C[modern_lib
PUBLIC cxx_std_23]
D[test_runner] -->|links| B
D -->|links| E[test_utils
PRIVATE cxx_std_17]
style A fill:#3B9797,color:#fff
style C fill:#BF092F,color:#fff
style B fill:#16476A,color:#fff
CMake resolves the highest standard among all requirements. In the diagram above, app requests C++20 but links to modern_lib which requires C++23 — CMake will compile app with C++23.
Compiler Feature Detection
Sometimes you need to check whether a specific compiler flag is supported before using it. The CheckCXXCompilerFlag module is your primary tool.
check_cxx_compiler_flag
cmake_minimum_required(VERSION 3.21)
project(FlagDetection LANGUAGES CXX)
include(CheckCXXCompilerFlag)
# Check if the compiler supports -Wconversion
check_cxx_compiler_flag(-Wconversion HAS_WCONVERSION)
if(HAS_WCONVERSION)
message(STATUS "Compiler supports -Wconversion")
endif()
# Check for C++20 coroutines flag
check_cxx_compiler_flag(-fcoroutines HAS_COROUTINES_FLAG)
add_executable(app main.cpp)
if(HAS_WCONVERSION)
target_compile_options(app PRIVATE -Wconversion)
endif()
if(HAS_COROUTINES_FLAG)
target_compile_options(app PRIVATE -fcoroutines)
endif()
Create a project that detects and conditionally enables these flags: -Wshadow, -Wnon-virtual-dtor, -Wold-style-cast, -Wcast-align, and -Woverloaded-virtual. Log which flags are available on your system.
Compile Definitions
Compile definitions are preprocessor macros (-DFOO=bar) passed to the compiler. Use target_compile_definitions() rather than the legacy add_definitions().
cmake_minimum_required(VERSION 3.21)
project(Definitions LANGUAGES CXX)
add_library(network src/network.cpp)
# PRIVATE: only for compiling this target
target_compile_definitions(network PRIVATE
NETWORK_INTERNAL_BUILD
MAX_CONNECTIONS=1024
)
# PUBLIC: visible to this target AND consumers
target_compile_definitions(network PUBLIC
NETWORK_VERSION_MAJOR=2
NETWORK_VERSION_MINOR=1
)
# INTERFACE: only visible to consumers, not this target
target_compile_definitions(network INTERFACE
USING_NETWORK_LIB
)
Per-Configuration Definitions with Generator Expressions
cmake_minimum_required(VERSION 3.21)
project(ConfigDefs LANGUAGES CXX)
add_executable(app main.cpp)
# Define DEBUG_MODE only in Debug builds, NDEBUG in Release
target_compile_definitions(app PRIVATE
$<$<CONFIG:Debug>:DEBUG_MODE=1>
$<$<CONFIG:Debug>:ENABLE_LOGGING=1>
$<$<CONFIG:Release>:NDEBUG>
$<$<CONFIG:RelWithDebInfo>:NDEBUG>
$<$<CONFIG:RelWithDebInfo>:ENABLE_LOGGING=1>
)
Compile Options
The target_compile_options() command adds flags to the compiler invocation. This is where you set warnings, optimization tweaks, and architecture-specific options.
Warning Flags
cmake_minimum_required(VERSION 3.21)
project(Warnings LANGUAGES CXX)
add_executable(app main.cpp)
# Strict warnings for GCC/Clang
target_compile_options(app PRIVATE
-Wall
-Wextra
-Wpedantic
-Wshadow
-Wnon-virtual-dtor
-Wold-style-cast
-Wcast-align
-Woverloaded-virtual
-Wconversion
-Wsign-conversion
-Wnull-dereference
-Wdouble-promotion
-Wformat=2
)
Per-Compiler Conditionals
Different compilers use different flag syntax. Use CMAKE_CXX_COMPILER_ID or generator expressions to handle this:
cmake_minimum_required(VERSION 3.21)
project(CrossPlatformWarnings LANGUAGES CXX)
add_executable(app main.cpp)
# Method 1: if() blocks
if(MSVC)
target_compile_options(app PRIVATE /W4 /WX /permissive-)
elseif(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
target_compile_options(app PRIVATE -Wall -Wextra -Werror -pedantic)
endif()
# Method 2: Generator expressions (preferred for libraries)
target_compile_options(app PRIVATE
$<$<CXX_COMPILER_ID:MSVC>:/W4 /WX /permissive->
$<$<CXX_COMPILER_ID:GNU,Clang>:-Wall -Wextra -Werror -pedantic>
)
-Werror (treat warnings as errors) in libraries that others will build. Their compiler version may produce new warnings your code doesn't account for. Use -Werror in CI but consider omitting it from installed config files.
Platform-Specific Flags
CMake provides several variables for compiler and platform identification. The CMAKE_<LANG>_COMPILER_ID variable is the most reliable way to detect your compiler.
MSVC vs GCC vs Clang
cmake_minimum_required(VERSION 3.21)
project(PlatformFlags LANGUAGES CXX)
add_library(mylib src/mylib.cpp)
if(MSVC)
# MSVC-specific flags
target_compile_options(mylib PRIVATE
/utf-8 # Source and execution charset UTF-8
/EHsc # Standard C++ exception handling
/Zc:__cplusplus # Report correct __cplusplus value
/Zc:preprocessor # Use conforming preprocessor
)
target_compile_definitions(mylib PRIVATE
_CRT_SECURE_NO_WARNINGS # Disable CRT deprecation warnings
NOMINMAX # Don't define min/max macros
WIN32_LEAN_AND_MEAN # Reduce Windows.h bloat
)
elseif(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
# GCC-specific flags
target_compile_options(mylib PRIVATE
-fdiagnostics-color=always
-Wduplicated-cond
-Wduplicated-branches
-Wlogical-op
-Wuseless-cast
)
elseif(CMAKE_CXX_COMPILER_ID STREQUAL "Clang" OR
CMAKE_CXX_COMPILER_ID STREQUAL "AppleClang")
# Clang-specific flags
target_compile_options(mylib PRIVATE
-fcolor-diagnostics
-Wno-unused-command-line-argument
)
endif()
flowchart TD
A[CMAKE_CXX_COMPILER_ID] --> B{Value?}
B -->|MSVC| C["/W4 /WX /permissive-
/utf-8 /EHsc"]
B -->|GNU| D["-Wall -Wextra
-Wduplicated-cond
-Wlogical-op"]
B -->|Clang| E["-Wall -Wextra
-fcolor-diagnostics"]
B -->|AppleClang| F["Same as Clang
+ Apple-specific"]
B -->|Intel| G["Intel-specific flags"]
style C fill:#16476A,color:#fff
style D fill:#3B9797,color:#fff
style E fill:#BF092F,color:#fff
Optimization Levels
CMake handles optimization through build types (Debug, Release, RelWithDebInfo, MinSizeRel), which set appropriate flags automatically. Understanding what each level does helps when you need custom tuning.
| Flag | GCC/Clang | MSVC | Effect |
|---|---|---|---|
| No optimization | -O0 | /Od | Fastest compile, slowest execution, best debugging |
| Basic | -O1 | /O1 | Minimal optimizations, reduced code size |
| Standard | -O2 | /O2 | Most optimizations without space tradeoffs |
| Aggressive | -O3 | /Ox | All -O2 plus vectorization, inlining |
| Size | -Os | /O1 | Optimize for binary size |
| Min size | -Oz | — | Aggressive size reduction (Clang only) |
Link-Time Optimization (LTO)
LTO enables cross-translation-unit optimizations at link time. CMake provides first-class support via CMAKE_INTERPROCEDURAL_OPTIMIZATION:
cmake_minimum_required(VERSION 3.21)
project(LTOExample LANGUAGES CXX)
# Check if LTO is supported
include(CheckIPOSupported)
check_ipo_supported(RESULT lto_supported OUTPUT lto_error)
if(lto_supported)
message(STATUS "LTO is supported")
set(CMAKE_INTERPROCEDURAL_OPTIMIZATION TRUE)
else()
message(WARNING "LTO not supported: ${lto_error}")
endif()
add_executable(app main.cpp helper.cpp utils.cpp)
Build the same project twice — once with LTO disabled and once enabled. Compare binary sizes and run a benchmark to measure the performance difference. Try with a project that has multiple translation units calling functions across files.
Position-Independent Code
Position-independent code (PIC) is required for shared libraries on most Unix-like platforms. CMake sets this automatically for SHARED libraries, but you may need to enable it for static libraries that will be linked into shared libraries:
cmake_minimum_required(VERSION 3.21)
project(PICExample LANGUAGES CXX)
# Global setting: all targets get -fPIC
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
# Or per-target:
add_library(static_lib STATIC src/static.cpp)
set_target_properties(static_lib PROPERTIES POSITION_INDEPENDENT_CODE ON)
# This shared lib links the static one — PIC is required
add_library(shared_lib SHARED src/shared.cpp)
target_link_libraries(shared_lib PRIVATE static_lib)
User-Facing Options
The option() command creates boolean cache variables that users can toggle. This is the standard way to make builds configurable.
option() Command
cmake_minimum_required(VERSION 3.21)
project(ConfigurableProject LANGUAGES CXX)
# User-facing options (visible in cmake-gui and ccmake)
option(BUILD_TESTING "Build the test suite" ON)
option(BUILD_SHARED_LIBS "Build shared libraries" OFF)
option(ENABLE_ASAN "Enable AddressSanitizer" OFF)
option(ENABLE_COVERAGE "Enable code coverage" OFF)
option(USE_SYSTEM_JSON "Use system nlohmann_json instead of bundled" OFF)
message(STATUS "Build testing: ${BUILD_TESTING}")
message(STATUS "Shared libs: ${BUILD_SHARED_LIBS}")
message(STATUS "ASAN: ${ENABLE_ASAN}")
add_library(mylib src/mylib.cpp)
if(BUILD_TESTING)
enable_testing()
add_subdirectory(tests)
endif()
Cache Variables for Non-Boolean Options
cmake_minimum_required(VERSION 3.21)
project(CacheVars LANGUAGES CXX)
# String cache variable with allowed values
set(LOG_LEVEL "INFO" CACHE STRING "Logging level")
set_property(CACHE LOG_LEVEL PROPERTY STRINGS "TRACE" "DEBUG" "INFO" "WARN" "ERROR")
# Path cache variable
set(CUSTOM_INCLUDE_DIR "" CACHE PATH "Additional include directory")
# Use the cache variables
add_executable(app main.cpp)
target_compile_definitions(app PRIVATE LOG_LEVEL_${LOG_LEVEL})
if(CUSTOM_INCLUDE_DIR)
target_include_directories(app PRIVATE ${CUSTOM_INCLUDE_DIR})
endif()
Sanitizer Integration
Address Sanitizer (ASan) and Undefined Behavior Sanitizer (UBSan) are invaluable for finding bugs. Here's the recommended pattern for integrating them:
cmake_minimum_required(VERSION 3.21)
project(SanitizedProject LANGUAGES CXX)
option(ENABLE_ASAN "Enable AddressSanitizer" OFF)
option(ENABLE_UBSAN "Enable UndefinedBehaviorSanitizer" OFF)
option(ENABLE_TSAN "Enable ThreadSanitizer" OFF)
add_executable(app main.cpp)
if(ENABLE_ASAN)
target_compile_options(app PRIVATE -fsanitize=address -fno-omit-frame-pointer)
target_link_options(app PRIVATE -fsanitize=address)
endif()
if(ENABLE_UBSAN)
target_compile_options(app PRIVATE -fsanitize=undefined)
target_link_options(app PRIVATE -fsanitize=undefined)
endif()
if(ENABLE_TSAN)
target_compile_options(app PRIVATE -fsanitize=thread)
target_link_options(app PRIVATE -fsanitize=thread)
endif()
target_compile_options() and target_link_options() together. Also note that ASan and TSan cannot be used simultaneously — they are mutually exclusive.
Enable sanitizers from the command line without modifying CMakeLists.txt:
# Configure with ASan enabled
cmake -B build -DCMAKE_BUILD_TYPE=Debug -DENABLE_ASAN=ON
# Build and run — ASan will report memory errors at runtime
cmake --build build
./build/app
Putting It All Together
Here's a complete, production-quality compiler configuration that handles multiple compilers, build types, and optional features:
cmake_minimum_required(VERSION 3.21)
project(ProductionProject
VERSION 1.0.0
LANGUAGES CXX
DESCRIPTION "A well-configured C++ project"
)
# ─── Global Settings ───────────────────────────────────────
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
# ─── Options ───────────────────────────────────────────────
option(BUILD_TESTING "Build tests" ON)
option(ENABLE_ASAN "Enable AddressSanitizer" OFF)
option(ENABLE_UBSAN "Enable UndefinedBehaviorSanitizer" OFF)
option(ENABLE_LTO "Enable Link-Time Optimization" OFF)
option(WARNINGS_AS_ERRORS "Treat warnings as errors" ON)
# ─── LTO ───────────────────────────────────────────────────
if(ENABLE_LTO)
include(CheckIPOSupported)
check_ipo_supported(RESULT lto_supported)
if(lto_supported)
set(CMAKE_INTERPROCEDURAL_OPTIMIZATION TRUE)
endif()
endif()
# ─── Targets ──────────────────────────────────────────────
add_library(mylib src/mylib.cpp src/utils.cpp)
target_include_directories(mylib PUBLIC include)
add_executable(app src/main.cpp)
target_link_libraries(app PRIVATE mylib)
# ─── Compiler Warnings ─────────────────────────────────────
function(set_project_warnings target)
set(MSVC_WARNINGS /W4 /permissive- /utf-8 /Zc:__cplusplus)
set(GCC_CLANG_WARNINGS
-Wall -Wextra -Wpedantic -Wshadow -Wnon-virtual-dtor
-Wold-style-cast -Wcast-align -Woverloaded-virtual
-Wconversion -Wsign-conversion -Wnull-dereference
-Wdouble-promotion -Wformat=2
)
set(GCC_ONLY -Wduplicated-cond -Wduplicated-branches -Wlogical-op)
if(WARNINGS_AS_ERRORS)
list(APPEND MSVC_WARNINGS /WX)
list(APPEND GCC_CLANG_WARNINGS -Werror)
endif()
if(MSVC)
target_compile_options(${target} PRIVATE ${MSVC_WARNINGS})
elseif(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
target_compile_options(${target} PRIVATE ${GCC_CLANG_WARNINGS} ${GCC_ONLY})
elseif(CMAKE_CXX_COMPILER_ID MATCHES "Clang")
target_compile_options(${target} PRIVATE ${GCC_CLANG_WARNINGS})
endif()
endfunction()
set_project_warnings(mylib)
set_project_warnings(app)
# ─── Sanitizers ────────────────────────────────────────────
if(ENABLE_ASAN)
target_compile_options(app PRIVATE -fsanitize=address -fno-omit-frame-pointer)
target_link_options(app PRIVATE -fsanitize=address)
endif()
if(ENABLE_UBSAN)
target_compile_options(app PRIVATE -fsanitize=undefined)
target_link_options(app PRIVATE -fsanitize=undefined)
endif()
# ─── Tests ─────────────────────────────────────────────────
if(BUILD_TESTING)
enable_testing()
add_subdirectory(tests)
endif()
Create the project above with a simple main.cpp and library. Build it with different combinations: -DENABLE_ASAN=ON, -DENABLE_LTO=ON, -DWARNINGS_AS_ERRORS=OFF. Intentionally introduce a memory error and verify ASan catches it.