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_ID 和 CXX_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>
)
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 知识体系总览
下面这张图,是我整理生成器表达式时画的。它把整个知识结构串起来了:
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