Table of Contents

  1. What Are Generator Expressions?
  2. Boolean Expressions
  3. Conditional Expressions
  4. Target Queries
  5. Platform and Compiler Queries
  6. Build Configuration
  7. Install vs Build Interface
  8. Output Transformations
  9. Practical Patterns
Back to CMake Mastery Series

Part 7: Generator Expressions

June 4, 2026 Wasil Zafar 40 min read

Master CMake's powerful $<...> syntax — conditional logic that is evaluated at build-system generation time, enabling per-configuration flags, platform-specific paths, and clean build/install interfaces.

What Are Generator Expressions?

Generator expressions (often called "genexes") are $<...> constructs that CMake evaluates at generation time — after the configure step completes but before the build system files are written. They enable logic that depends on information not available during configuration, such as the active build configuration in multi-config generators (Visual Studio, Xcode, Ninja Multi-Config).

Key Insight: In single-config generators (Unix Makefiles, Ninja), the build type is known at configure time via CMAKE_BUILD_TYPE. But in multi-config generators (Visual Studio, Xcode), all configurations exist simultaneously — you choose at build time. Generator expressions work correctly in both scenarios, making them the portable way to write conditional logic.

Configure Time vs Generate Time

Understanding when different constructs are evaluated is crucial. See the official generator expressions documentation for the full reference.

CMake Processing Phases
        flowchart LR
            A[CMakeLists.txt] -->|Configure| B["Variables resolved
if() evaluated
set() executed"] B -->|Generate| C["Generator expressions
$<...> evaluated
Build files written"] C -->|Build| D["Compiler invoked
Linker invoked
Binaries produced"] style B fill:#3B9797,color:#fff style C fill:#BF092F,color:#fff
cmake_minimum_required(VERSION 3.21)
project(GenexDemo LANGUAGES CXX)

# This variable is set at CONFIGURE time
set(MY_VAR "hello")
message(STATUS "MY_VAR = ${MY_VAR}")  # Printed during cmake configuration

add_executable(app main.cpp)

# This genex is evaluated at GENERATE time — NOT during configure
target_compile_definitions(app PRIVATE
    $<$<CONFIG:Debug>:DEBUG_BUILD=1>
)

# You CANNOT print a genex with message() — it hasn't been evaluated yet
message(STATUS "Genex: $<CONFIG>")  # Prints literally "$<CONFIG>"

Boolean Expressions

Boolean genexes produce 1 (true) or 0 (false). They're the building blocks for conditional logic.

$<BOOL:string>

Converts a string to a boolean: empty string, 0, FALSE, OFF, NO, IGNORE, NOTFOUND, or strings ending in -NOTFOUND evaluate to 0. Everything else is 1.

cmake_minimum_required(VERSION 3.21)
project(BoolGenex LANGUAGES CXX)

option(ENABLE_VERBOSE "Enable verbose output" OFF)

add_executable(app main.cpp)

# $<BOOL:...> converts the option value to 0 or 1
# Then $<...> uses it as a condition
target_compile_definitions(app PRIVATE
    $<$<BOOL:${ENABLE_VERBOSE}>:VERBOSE_MODE=1>
)

Logical Operators: AND, OR, NOT

cmake_minimum_required(VERSION 3.21)
project(LogicalGenex LANGUAGES CXX)

option(ENABLE_LOGGING "Enable logging" ON)
option(ENABLE_TRACING "Enable detailed tracing" OFF)

add_executable(app main.cpp)

# AND: both conditions must be true
target_compile_definitions(app PRIVATE
    $<$<AND:$<CONFIG:Debug>,$<BOOL:${ENABLE_LOGGING}>>:DEBUG_LOGGING=1>
)

# OR: at least one condition true
target_compile_definitions(app PRIVATE
    $<$<OR:$<CONFIG:Debug>,$<CONFIG:RelWithDebInfo>>:HAS_DEBUG_INFO=1>
)

# NOT: invert a condition
target_compile_definitions(app PRIVATE
    $<$<NOT:$<CONFIG:Debug>>:OPTIMIZED_BUILD=1>
)

Conditional Expressions

The $<IF:condition,true_string,false_string> genex is the ternary operator of CMake — choose between two values based on a condition.

cmake_minimum_required(VERSION 3.21)
project(ConditionalGenex LANGUAGES CXX)

add_executable(app main.cpp)

# Output different values based on build type
target_compile_definitions(app PRIVATE
    LOG_LEVEL=$<IF:$<CONFIG:Debug>,0,2>
)

# Set different output directories per config
set_target_properties(app PROPERTIES
    RUNTIME_OUTPUT_DIRECTORY
        $<IF:$<CONFIG:Debug>,${CMAKE_BINARY_DIR}/debug,${CMAKE_BINARY_DIR}/release>
)

String Comparisons

cmake_minimum_required(VERSION 3.21)
project(StringCompare LANGUAGES CXX)

set(BACKEND "opengl" CACHE STRING "Rendering backend")

add_executable(app main.cpp)

# $<STREQUAL:a,b> — case-sensitive string equality
target_compile_definitions(app PRIVATE
    $<$<STREQUAL:${BACKEND},opengl>:USE_OPENGL=1>
    $<$<STREQUAL:${BACKEND},vulkan>:USE_VULKAN=1>
    $<$<STREQUAL:${BACKEND},metal>:USE_METAL=1>
)

# Version comparisons
target_compile_definitions(app PRIVATE
    $<$<VERSION_GREATER_EQUAL:${CMAKE_VERSION},3.25>:HAS_CMAKE_3_25_FEATURES=1>
)

Target Queries

Target query genexes extract information about CMake targets — file paths, properties, and existence checks. See the target-dependent queries reference.

cmake_minimum_required(VERSION 3.21)
project(TargetQueries LANGUAGES CXX)

add_library(mylib SHARED src/mylib.cpp)
add_executable(app src/main.cpp)
target_link_libraries(app PRIVATE mylib)

# $<TARGET_FILE:tgt> — full path to the target's output file
add_custom_command(TARGET app POST_BUILD
    COMMAND ${CMAKE_COMMAND} -E echo "Built: $<TARGET_FILE:app>"
    COMMAND ${CMAKE_COMMAND} -E echo "Library: $<TARGET_FILE:mylib>"
)

# $<TARGET_FILE_DIR:tgt> — directory containing the output
# Copy the shared library next to the executable
add_custom_command(TARGET app POST_BUILD
    COMMAND ${CMAKE_COMMAND} -E copy_if_different
        $<TARGET_FILE:mylib>
        $<TARGET_FILE_DIR:app>
)

$<TARGET_PROPERTY:tgt,prop>

cmake_minimum_required(VERSION 3.21)
project(TargetProperty LANGUAGES CXX)

add_library(mylib src/mylib.cpp)
set_target_properties(mylib PROPERTIES
    VERSION 2.1.0
    SOVERSION 2
)

add_executable(app src/main.cpp)
target_link_libraries(app PRIVATE mylib)

# Query a target's property at generation time
target_compile_definitions(app PRIVATE
    LIB_VERSION="$<TARGET_PROPERTY:mylib,VERSION>"
)

# $<TARGET_EXISTS:tgt> — check if target exists
target_compile_definitions(app PRIVATE
    $<$<TARGET_EXISTS:optional_dep>:HAS_OPTIONAL_DEP=1>
)
Hands-On Post-Build Copy with Target Queries

Create a project with a shared library and executable. Use $<TARGET_FILE:...> and $<TARGET_FILE_DIR:...> to automatically copy the DLL/so next to the executable after building. Test on both Windows (DLL) and Linux (shared object).

TARGET_FILE POST_BUILD Cross-Platform

Platform and Compiler Queries

cmake_minimum_required(VERSION 3.21)
project(PlatformGenex LANGUAGES CXX)

add_executable(app main.cpp)

# $<PLATFORM_ID:id> — true if building for this platform
target_compile_definitions(app PRIVATE
    $<$<PLATFORM_ID:Windows>:ON_WINDOWS=1>
    $<$<PLATFORM_ID:Linux>:ON_LINUX=1>
    $<$<PLATFORM_ID:Darwin>:ON_MACOS=1>
)

# $<CXX_COMPILER_ID:ids> — true if compiler matches
target_compile_options(app PRIVATE
    $<$<CXX_COMPILER_ID:GNU,Clang>:-Wall -Wextra>
    $<$<CXX_COMPILER_ID:MSVC>:/W4>
)

# Combine platform + compiler
target_link_libraries(app PRIVATE
    $<$<PLATFORM_ID:Linux>:pthread>
    $<$<PLATFORM_ID:Windows>:ws2_32>
)

Build Configuration

The $<CONFIG:cfg> genex is one of the most commonly used — it resolves to 1 when the active build configuration matches.

Per-Configuration Flags and Sources

cmake_minimum_required(VERSION 3.21)
project(ConfigGenex LANGUAGES CXX)

add_executable(app main.cpp)

# Different optimization flags per config
target_compile_options(app PRIVATE
    $<$<CONFIG:Debug>:-O0 -g3>
    $<$<CONFIG:Release>:-O3 -DNDEBUG>
    $<$<CONFIG:RelWithDebInfo>:-O2 -g>
)

# Include debug-only source files
target_sources(app PRIVATE
    $<$<CONFIG:Debug>:src/debug_helpers.cpp>
    $<$<CONFIG:Debug>:src/memory_tracker.cpp>
)

# Link debug-only libraries
target_link_libraries(app PRIVATE
    $<$<CONFIG:Debug>:profiler_lib>
)

Install vs Build Interface

The $<BUILD_INTERFACE:...> and $<INSTALL_INTERFACE:...> genexes are essential for libraries that will be both built locally and installed for consumers. They let you specify different include paths (or other properties) depending on whether the library is being used from the build tree or after installation.

Key Insight: During development, your headers are in ${PROJECT_SOURCE_DIR}/include. After installation, they're in ${PREFIX}/include. The BUILD_INTERFACE/INSTALL_INTERFACE pair handles this transparently.
cmake_minimum_required(VERSION 3.21)
project(InterfaceDemo VERSION 1.0.0 LANGUAGES CXX)

add_library(mylib src/mylib.cpp)

# Include directories differ between build and install
target_include_directories(mylib PUBLIC
    $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
    $<INSTALL_INTERFACE:include>
)

# Compile definitions can also differ
target_compile_definitions(mylib PUBLIC
    $<BUILD_INTERFACE:MYLIB_BUILDING=1>
)

# Install the library and generate config files
install(TARGETS mylib EXPORT mylibTargets
    LIBRARY DESTINATION lib
    ARCHIVE DESTINATION lib
    INCLUDES DESTINATION include
)
install(DIRECTORY include/ DESTINATION include)
install(EXPORT mylibTargets
    FILE mylibTargets.cmake
    NAMESPACE mylib::
    DESTINATION lib/cmake/mylib
)

Complete Build/Install Pattern

cmake_minimum_required(VERSION 3.21)
project(CompleteLibrary VERSION 2.0.0 LANGUAGES CXX)

add_library(networking src/networking.cpp src/socket.cpp)
target_compile_features(networking PUBLIC cxx_std_17)

target_include_directories(networking PUBLIC
    $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
    $<BUILD_INTERFACE:${CMAKE_CURRENT_BINARY_DIR}/generated>
    $<INSTALL_INTERFACE:include>
)

# Generated header available in build tree, installed to include/
configure_file(config.h.in generated/networking/config.h)

install(TARGETS networking EXPORT networkingTargets
    LIBRARY DESTINATION lib
    ARCHIVE DESTINATION lib
)
install(DIRECTORY include/networking DESTINATION include)
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/generated/networking/config.h
    DESTINATION include/networking
)
Hands-On Installable Library with Interfaces

Create a library with public headers in include/. Set up BUILD_INTERFACE and INSTALL_INTERFACE for include directories. Install it to a local prefix, then create a separate consumer project that uses find_package() to locate and link your library.

BUILD_INTERFACE INSTALL_INTERFACE find_package

Output Transformations

Generator expressions can transform strings at generation time:

cmake_minimum_required(VERSION 3.21)
project(Transforms LANGUAGES CXX)

add_executable(app main.cpp)

# $<LOWER_CASE:string> and $<UPPER_CASE:string>
set(PROJECT_NAME_UPPER "$<UPPER_CASE:${PROJECT_NAME}>")

# $<JOIN:list,separator> — join a list with a separator
set(MY_LIST "one;two;three")
# At generation time: "one,two,three"

# $<REMOVE_DUPLICATES:list>
# $<FILTER:list,INCLUDE,regex> / $<FILTER:list,EXCLUDE,regex>

# Practical: generate a version string for the binary
target_compile_definitions(app PRIVATE
    APP_VERSION="$<LOWER_CASE:${PROJECT_NAME}>-${PROJECT_VERSION}"
)

Practical Patterns

Here are battle-tested generator expression patterns used in real-world projects:

Pattern 1: Multi-Compiler Warning Flags

cmake_minimum_required(VERSION 3.21)
project(Patterns LANGUAGES CXX)

add_library(mylib src/mylib.cpp)

# Clean, single-command warning configuration
target_compile_options(mylib PRIVATE
    $<$<CXX_COMPILER_ID:MSVC>:
        /W4 /WX /permissive- /utf-8
        /wd4100  # unreferenced parameter
    >
    $<$<CXX_COMPILER_ID:GNU>:
        -Wall -Wextra -Werror -pedantic
        -Wno-unused-parameter
    >
    $<$<CXX_COMPILER_ID:Clang,AppleClang>:
        -Wall -Wextra -Werror -pedantic
        -Wno-unused-parameter
    >
)

Pattern 2: Debug-Only Dependencies

cmake_minimum_required(VERSION 3.21)
project(DebugDeps LANGUAGES CXX)

find_package(Sanitizers QUIET)

add_executable(app main.cpp)

# Only link debug tools in debug builds
target_link_libraries(app PRIVATE
    $<$<CONFIG:Debug>:debug_allocator>
    $<$<CONFIG:Debug>:leak_checker>
)

# Conditional source files for debug instrumentation
target_sources(app PRIVATE
    $<$<CONFIG:Debug>:${CMAKE_CURRENT_SOURCE_DIR}/src/debug_ui.cpp>
)

Pattern 3: Relocatable Install Prefix

cmake_minimum_required(VERSION 3.21)
project(Relocatable LANGUAGES CXX)

add_executable(app main.cpp)

# Use genex to create a relocatable RPATH
set_target_properties(app PROPERTIES
    INSTALL_RPATH "$ORIGIN/../lib"
    BUILD_RPATH_USE_ORIGIN TRUE
)

# Configure install location using genex for the output name
set_target_properties(app PROPERTIES
    OUTPUT_NAME "$<LOWER_CASE:${PROJECT_NAME}>"
    DEBUG_POSTFIX "d"
)
Generator Expression Evaluation Flow
        flowchart TD
            A["$<$<CONFIG:Debug>:-DDEBUG=1>"] --> B{Is CONFIG Debug?}
            B -->|Yes| C["-DDEBUG=1"]
            B -->|No| D["(empty string)"]
            E["$<IF:$<BOOL:ON>,A,B>"] --> F{"$<BOOL:ON> = 1?"}
            F -->|Yes| G["A"]
            F -->|No| H["B"]
            style C fill:#3B9797,color:#fff
            style D fill:#132440,color:#fff
            style G fill:#3B9797,color:#fff
    
Hands-On Generator Expression Debug Output

Use file(GENERATE ...) to write generator expression values to a file at generation time. Create a CMakeLists.txt that writes all $<CONFIG>, $<CXX_COMPILER_ID>, $<PLATFORM_ID>, and $<TARGET_FILE:app> to a text file so you can inspect what they resolve to.

file(GENERATE) Debugging Genex Inspection