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).
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.
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>
)
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).
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.
${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
)
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.
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"
)
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
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.