Table of Contents

  1. C++ Standard Selection
  2. Compiler Feature Detection
  3. Compile Definitions
  4. Compile Options
  5. Platform-Specific Flags
  6. Optimization Levels
  7. Position-Independent Code
  8. User-Facing Options
  9. Sanitizer Integration
  10. Putting It All Together
Back to CMake Mastery Series

Part 6: Compiler Configuration and Flags

June 4, 2026 Wasil Zafar 35 min read

Master C++ standard selection, compiler warnings, optimization levels, sanitizers, and cross-platform flag management to produce robust, performant builds across all compilers.

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)
Key Insight: Always set 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.

C++ Standard Propagation Through Dependencies
        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()
Hands-On Feature Detection Pattern

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.

CheckCXXCompilerFlag Conditional Compilation

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>
)
Warning: Be careful with -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()
Compiler Identification Decision Tree
        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.

FlagGCC/ClangMSVCEffect
No optimization-O0/OdFastest compile, slowest execution, best debugging
Basic-O1/O1Minimal optimizations, reduced code size
Standard-O2/O2Most optimizations without space tradeoffs
Aggressive-O3/OxAll -O2 plus vectorization, inlining
Size-Os/O1Optimize for binary size
Min size-OzAggressive 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)
Hands-On LTO Performance Comparison

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.

LTO CheckIPOSupported Performance

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()
Important: Sanitizer flags must be passed to both the compiler and the linker. Use 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()
Hands-On Complete Compiler Configuration Project

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.

Production CMake Sanitizers LTO