第二十二讲:生成器表达式——$<...>语法的使用,条件编译与平台判断
生成器表达式,说白了就是 CMake 在构建时才会求值的「魔法字符串」。
我第一次接触这东西时,觉得它就是个花哨的语法糖。直到有一次,我在一个跨平台项目里被折腾得够呛——Windows 上要链接 ws2_32.lib,Linux 上要链接 pthread,macOS 上又得用 CoreFoundation。那时候我还在用 if-else 堆变量,代码写得又臭又长。
后来我发现了生成器表达式,嗯,真香。
什么是生成器表达式?
生成器表达式以 $<...> 的形式出现。它不像普通变量那样在 CMakeLists.txt 解析时就展开,而是等到构建系统生成时才求值。
为什么要等到构建时?因为有些信息在配置阶段根本拿不到。比如:
- 当前构建的是 Debug 还是 Release?
- 目标平台是 Android 还是 iOS?
- 编译器是 GCC 还是 MSVC?
这些信息,生成器表达式都能帮你搞定。
核心区别:
- 普通变量
${VAR}:配置阶段求值 - 生成器表达式
$<...>:生成阶段求值
基本语法与常用表达式
生成器表达式的语法其实很简单,就两种形式:
# 条件式
$<condition:value>
# 纯值式
$<value>
条件式里,如果 condition 为真,就返回 value;否则返回空字符串。纯值式则直接返回 value。
我整理了一份常用表达式清单,你写代码时可以直接查:
| 表达式 | 含义 | 典型场景 |
|---|---|---|
$<CONFIG> |
当前构建配置 | Debug/Release 区分 |
$<PLATFORM_ID> |
目标平台标识 | Windows/Linux/Android |
$<CXX_COMPILER_ID> |
编译器标识 | GNU/MSVC/Clang |
$<TARGET_FILE:tgt> |
目标文件路径 | 获取输出路径 |
$<BOOL:expr> |
布尔表达式求值 | 自定义条件 |
条件编译:让代码在不同平台下「变脸」
条件编译是生成器表达式最常用的场景。我举个例子,假设你要给不同平台添加不同的编译宏:
target_compile_definitions(myapp PRIVATE
$<$<PLATFORM_ID:Windows>:PLATFORM_WINDOWS>
$<$<PLATFORM_ID:Linux>:PLATFORM_LINUX>
$<$<PLATFORM_ID:Darwin>:PLATFORM_MACOS>
)
这段代码的意思是:如果目标平台是 Windows,就定义 PLATFORM_WINDOWS;如果是 Linux,就定义 PLATFORM_LINUX;如果是 macOS,就定义 PLATFORM_MACOS。
你想想看,如果没有生成器表达式,你得写三个 if() 块,每个块里再写 target_compile_definitions。代码量翻倍不说,可读性还差。
我的习惯: 我会把平台相关的宏定义集中写在一个地方,用生成器表达式一次性搞定。这样后续维护时,打开文件扫一眼就知道哪些平台有特殊处理。
平台判断:不止是操作系统
平台判断不只是判断操作系统。编译器、架构、构建类型,这些都属于「平台」的范畴。
来看一个更复杂的例子:
target_link_libraries(myapp PRIVATE
# Windows 下链接 ws2_32
$<$<PLATFORM_ID:Windows>:ws2_32>
# Linux 下链接 pthread
$<$<PLATFORM_ID:Linux>:pthread>
# 如果是 MSVC 编译器,添加特定链接选项
$<$<CXX_COMPILER_ID:MSVC>:msvcrt.lib>
)
这里我做了三件事:
- Windows 平台链接
ws2_32库 - Linux 平台链接
pthread库 - MSVC 编译器额外链接
msvcrt.lib
你可能会问:为什么不用 if(WIN32)?
因为 if(WIN32) 是在配置阶段判断的,而生成器表达式是在生成阶段判断的。如果你的项目是交叉编译,配置阶段的 WIN32 变量可能不准确。生成器表达式用的是 PLATFORM_ID,它反映的是真正的目标平台。
我曾经踩过的坑: 有一次我在做 Android NDK 交叉编译,用 if(WIN32) 判断主机平台,结果死活不对。后来换成 $<PLATFORM_ID:Windows> 才搞定。记住:生成器表达式判断的是目标平台,不是主机平台。
嵌套与组合:生成器表达式的进阶玩法
生成器表达式可以嵌套使用。比如,你想在 Debug 模式下开启某个功能,但只在 Linux 平台上:
target_compile_definitions(myapp PRIVATE
$<$<AND:$<CONFIG:Debug>,$<PLATFORM_ID:Linux>>:FEATURE_DEBUG_LINUX>
)
这里用了 $<AND:...> 组合条件,只有 Debug 配置且 Linux 平台时,才定义 FEATURE_DEBUG_LINUX。
常用的逻辑运算符有:
$<AND:cond1,cond2,...>:全部为真才为真$<OR:cond1,cond2,...>:任一为真即为真$<NOT:cond>:取反
我个人建议,嵌套不要超过三层。超过三层的话,可读性会急剧下降。这时候不如拆成多个 target_compile_definitions 调用,或者用变量先算好中间结果。
实战:一个完整的跨平台示例
我们来写一个完整的 CMakeLists.txt,把今天讲的东西串起来:
cmake_minimum_required(VERSION 3.20)
project(CrossPlatformDemo CXX)
add_executable(demo main.cpp)
# 平台相关的编译宏
target_compile_definitions(demo PRIVATE
$<$<PLATFORM_ID:Windows>:OS_WINDOWS>
$<$<PLATFORM_ID:Linux>:OS_LINUX>
$<$<PLATFORM_ID:Darwin>:OS_MACOS>
)
# 编译器相关的编译选项
target_compile_options(demo PRIVATE
$<$<CXX_COMPILER_ID:GNU>:-Wall -Wextra>
$<$<CXX_COMPILER_ID:MSVC>:/W4>
$<$<CXX_COMPILER_ID:Clang>:-Wall -Weverything>
)
# 构建类型相关的链接库
target_link_libraries(demo PRIVATE
$<$<CONFIG:Debug>:debug_helper>
$<$<CONFIG:Release>:release_helper>
)
# 平台相关的链接库
target_link_libraries(demo PRIVATE
$<$<PLATFORM_ID:Windows>:ws2_32>
$<$<PLATFORM_ID:Linux>:pthread>
)
这段代码覆盖了平台、编译器、构建类型三个维度的条件判断。你拿到任何平台,只要跑一遍 CMake,它就能自动适配。
生成器表达式的知识体系
为了让你更直观地理解生成器表达式的整体结构,我画了一张图:
避坑指南
生成器表达式虽然强大,但用不好也会踩坑。我分享几个亲身经历:
- 不要在 message() 里用生成器表达式:
message()在配置阶段执行,生成器表达式还没求值,你只会看到一串$<...>字符串。 - 注意引号:生成器表达式里如果包含空格,记得加引号。比如
$<$<CONFIG:Debug>:"-DDEBUG=1">。 - 不要过度嵌套:超过三层的嵌套,连我自己都看不懂。这时候用变量拆分会更清晰。
- 版本兼容性:有些生成器表达式是 CMake 3.x 才支持的。如果你的项目需要兼容旧版本,记得检查
cmake_minimum_required的版本号。
调试小技巧: 如果你不确定生成器表达式是否生效,可以用 add_custom_command 配合 COMMAND echo 来输出。虽然 message() 不行,但构建时的命令可以拿到求值后的结果。
好了,生成器表达式的内容就讲到这里。这东西说白了就是 CMake 给你的一个「延迟求值」的能力,让你在构建时再做决策。掌握了它,你的 CMakeLists.txt 会变得更简洁、更灵活、更跨平台。
公众号:蓝海资料掘金营,微信 deep3321