20、CMake生成器表达式:$<...>语法、条件编译、平台判断、配置判断

生成器表达式,说白了就是 CMake 在构建系统生成阶段才求值的「小脚本」。

我刚开始接触这玩意儿的时候,也觉得它有点绕。明明有 if()set(),为什么还要搞一套 $<...> 的语法?

后来在项目里踩了几次坑才明白——有些信息,在 CMake 配置阶段根本拿不到。比如目标平台的架构、编译器的具体版本、或者某个依赖是静态库还是动态库。这些信息,只有生成器表达式才能帮你搞定。

20.1 生成器表达式的基本语法

它的写法很简单:$<...>。尖括号里放条件或表达式。

我习惯把它分成三类:

  • 布尔条件表达式:返回 0 或 1
  • 字符串表达式:根据条件返回不同的字符串
  • 列表表达式:对列表进行过滤或变换

先看一个最简单的例子:

target_compile_definitions(myapp PRIVATE
    $<$<CONFIG:Debug>:DEBUG_MODE>
)

这句话的意思是:如果当前构建配置是 Debug,就添加 DEBUG_MODE 这个宏定义。如果是 Release,啥也不加。

我的习惯:能用生成器表达式的地方,尽量别用 if()。因为生成器表达式是「延迟求值」的,它更灵活,也更容易处理多配置生成器(比如 Xcode 和 Visual Studio)。

20.2 条件编译:按需开关代码

条件编译是生成器表达式最常用的场景。说白了就是:不同平台、不同配置下,编译不同的代码。

举个例子,我在一个跨平台项目里需要处理 Windows 和 Linux 的路径分隔符差异:

target_compile_definitions(mylib PRIVATE
    $<$<PLATFORM_ID:Windows>:PATH_SEP='\\'>
    $<$<PLATFORM_ID:Linux>:PATH_SEP='/'>
)

这样,代码里直接用 PATH_SEP 就行,不用写一堆 #ifdef

再比如,调试日志只在 Debug 下开启:

target_compile_definitions(myapp PRIVATE
    $<$<CONFIG:Debug>:ENABLE_LOGGING>
)
注意:生成器表达式不能嵌套在 if() 的条件里。它只出现在 target_* 命令、install()file(GENERATE) 等特定上下文中。

20.3 平台判断:一套代码,多平台适配

平台判断是生成器表达式的拿手好戏。我维护过一个嵌入式项目,需要在 ARM、x86、RISC-V 三种架构上编译。

用生成器表达式,可以这样处理:

target_compile_definitions(embedded_lib PRIVATE
    $<$<PLATFORM_ID:Android>:ANDROID_BUILD>
    $<$<PLATFORM_ID:Linux>:LINUX_BUILD>
    $<$<PLATFORM_ID:Windows>:WINDOWS_BUILD>
)

还有更细粒度的判断,比如检查编译器:

target_compile_options(myapp PRIVATE
    $<$<CXX_COMPILER_ID:MSVC>:/W4>
    $<$<CXX_COMPILER_ID:GNU>:-Wall -Wextra>
)

嗯,这里要注意:PLATFORM_IDCXX_COMPILER_ID 的值是 CMake 预定义的,不能自己瞎编。我刚开始用的时候,把 Windows 写成了 win32,结果死活不生效……

20.4 配置判断:Debug vs Release

配置判断最常用的就是 $<CONFIG:...>。但很多人不知道,它还能组合使用。

比如,我想在 Debug 和 Release 下链接不同的库:

target_link_libraries(myapp PRIVATE
    $<$<CONFIG:Debug>:debug_lib>
    $<$<CONFIG:Release>:release_lib>
)

再比如,我想在 Debug 下开启 AddressSanitizer:

target_compile_options(myapp PRIVATE
    $<$<CONFIG:Debug>:-fsanitize=address>
)
target_link_options(myapp PRIVATE
    $<$<CONFIG:Debug>:-fsanitize=address>
)
我曾经踩过的坑:在多配置生成器(Visual Studio)中,CMAKE_BUILD_TYPE 是空的!这时候用 if(CMAKE_BUILD_TYPE STREQUAL "Debug") 会直接失效。必须用 $<CONFIG:Debug> 才能正确工作。

20.5 逻辑运算与组合

生成器表达式支持逻辑运算:$<AND:...>$<OR:...>$<NOT:...>

举个例子,我想在 Linux 且是 Debug 配置下,添加一个特定的宏:

target_compile_definitions(myapp PRIVATE
    $<$<AND:$<PLATFORM_ID:Linux>,$<CONFIG:Debug>>:LINUX_DEBUG>
)

看着有点绕,对吧?我刚开始也觉得这语法太丑了。但用熟了之后,发现它其实很强大——你可以在一个表达式里组合多个条件,不用写一堆嵌套的 if()

还有一个常用的技巧:用 $<0:...>$<1:...> 做强制开关。

# 强制开启某个宏
target_compile_definitions(myapp PRIVATE
    $<1:ALWAYS_ON>
)

# 强制关闭某个宏
target_compile_definitions(myapp PRIVATE
    $<0:ALWAYS_OFF>
)

这在你调试生成器表达式本身时特别有用。

20.6 知识体系总览

下面这张图,是我整理生成器表达式时画的。它把整个知识结构串起来了:

生成器表达式 布尔条件表达式 字符串表达式 列表表达式 CONFIG:Debug PLATFORM_ID CXX_COMPILER_ID AND / OR / NOT $<CONFIG:...>:value $<TARGET_FILE> $<TARGET_LINKER_FILE> $<JOIN:list,sep> $<REMOVE_DUPLICATES> $<FILTER:list,regex> 核心:延迟求值,构建阶段才展开

20.7 实战:一个完整的例子

最后,我分享一个实际项目里的片段。这个项目需要同时支持 Windows 和 Linux,并且在 Debug 下打印额外信息:

add_library(utils STATIC utils.cpp)

target_compile_definitions(utils PRIVATE
    # 平台相关
    $<$<PLATFORM_ID:Windows>:PLATFORM_WINDOWS>
    $<$<PLATFORM_ID:Linux>:PLATFORM_LINUX>
    
    # 配置相关
    $<$<CONFIG:Debug>:DEBUG_BUILD>
    $<$<CONFIG:Release>:NDEBUG>
    
    # 组合条件:Linux + Debug
    $<$<AND:$<PLATFORM_ID:Linux>,$<CONFIG:Debug>>:LINUX_DEBUG_EXTRA>
)

target_include_directories(utils PUBLIC
    $<$<PLATFORM_ID:Windows>:${CMAKE_SOURCE_DIR}/win_include>
    $<$<PLATFORM_ID:Linux>:${CMAKE_SOURCE_DIR}/linux_include>
)

你看,整个 CMakeLists.txt 干净多了。没有一堆 if(WIN32)if(CMAKE_BUILD_TYPE) 的嵌套,所有逻辑都集中在 target_* 命令里。

我个人觉得,生成器表达式是 CMake 从「脚本语言」走向「声明式构建」的关键一步。你想想看,我们不再关心「什么时候执行」,只关心「最终结果是什么」。这种思维方式,才是现代 CMake 的精髓。

调试小技巧:如果你不确定生成器表达式是否生效,可以用 message() 配合 file(GENERATE) 输出一个临时文件,看看展开后的结果是什么。

公众号:蓝海资料掘金营,微信deep3321