第六章:生成器表达式与编译选项条件控制
各位同学,今天我们来聊聊 CMake 里一个非常实用、但初学者往往觉得有点绕的特性——生成器表达式。说白了,就是那个 $<...> 语法。
我记得刚接触 CMake 那会儿,看到这种尖括号加美元符号的写法,第一反应是「这什么鬼?」。后来在嵌入式项目里被坑了几次,才真正体会到它的价值。今天我就把这几年的经验掰开揉碎讲给你听。
6.1 生成器表达式是什么?
生成器表达式,本质上是一种「延迟求值」的机制。什么意思呢?
普通 CMake 变量,在 CMakeLists.txt 解析阶段就被替换掉了。但生成器表达式不同——它等到构建系统生成阶段(也就是 cmake --build 的时候)才被求值。
为什么会这样?因为有些信息在 CMake 配置时根本不知道。比如:
- 当前编译的是 Debug 还是 Release?
- 目标平台是 ARM Cortex-M4 还是 M7?
- 编译器是 GCC 还是 IAR?
这些信息,生成器表达式可以帮你「到时候再说」。
核心语法:
$<condition:value> # 条件为真时返回 value
$<condition:value1:value2> # 条件为真返回 value1,否则返回 value2
6.2 编译选项条件控制
嵌入式项目里,最常见的场景就是根据构建类型设置不同的编译选项。我在项目中遇到过好几次这样的需求:
- Debug 版本要加
-g -O0,方便调试 - Release 版本要加
-Os -DNDEBUG,优化体积 - 有时候还要区分模拟器版本和硬件版本
用生成器表达式,可以写得很优雅:
target_compile_options(my_firmware PRIVATE
$<$<CONFIG:Debug>:-g -O0>
$<$<CONFIG:Release>:-Os -DNDEBUG>
$<$<CONFIG:RelWithDebInfo>:-g -Os>
)
嗯,这里要注意:$<CONFIG:Debug> 本身就是一个生成器表达式,它返回 1 或 0。外面再套一层 $<...>,就构成了条件判断。
说白了,这就是 CMake 里的三目运算符。
6.3 调试与发布配置实战
让我给你看一个我实际用过的例子。之前做一个 STM32 项目,需要根据构建类型选择不同的链接脚本:
# Debug 版本用默认链接脚本
# Release 版本用优化后的链接脚本
target_link_options(my_app PRIVATE
$<$<CONFIG:Debug>:-T ld/debug.ld>
$<$<CONFIG:Release>:-T ld/release.ld>
)
# 调试版本开启断言
target_compile_definitions(my_app PRIVATE
$<$<CONFIG:Debug>:DEBUG_ASSERT>
)
你想想看,如果没有生成器表达式,你可能得写两个 target_compile_options 命令,再用 if 判断。代码会变得很啰嗦。
我的个人习惯:对于简单的条件判断,直接用生成器表达式。但如果逻辑特别复杂(比如嵌套三层以上),我会改用 if 块 + 变量缓存的方式。可读性更重要。
6.4 常用生成器表达式一览
| 表达式 | 含义 | 典型用途 |
|---|---|---|
$<CONFIG:cfg> |
当前构建类型是否为 cfg | 区分 Debug/Release |
$<PLATFORM_ID:id> |
目标平台是否为 id | 区分 Linux/Windows/ARM |
$<COMPILE_LANGUAGE:lang> |
当前编译语言是否为 lang | C 和 C++ 用不同选项 |
$<TARGET_PROPERTY:tgt,prop> |
获取目标 tgt 的属性 prop | 读取目标的编译选项 |
$<JOIN:list,sep> |
用 sep 连接列表 | 拼接路径或标志 |
6.5 避坑指南
我曾经踩过的坑:
- 生成器表达式不能用在
set()命令里。它只在target_*命令或add_custom_command中生效。 - 嵌套生成器表达式时,注意括号匹配。我建议写复杂表达式时先拆开测试。
$<CONFIG:Debug>是大小写敏感的。CMake 默认的配置名是Debug、Release,首字母大写。
6.6 知识体系图
下面这张图帮你理清生成器表达式的核心逻辑:
6.7 进阶技巧:组合使用
生成器表达式可以组合使用。比如,我想在 Debug 模式下且使用 ARM 编译器时,加一个特殊选项:
target_compile_options(my_app PRIVATE
$<$<AND:$<CONFIG:Debug>,$<PLATFORM_ID:ARM>>:-DDEBUG_ON_ARM>
)
这里用了 $<AND:...> 逻辑运算符。类似的还有 $<OR:...> 和 $<NOT:...>。
小技巧:如果你觉得嵌套太多看着眼花,可以先用 set() 定义中间变量,再用生成器表达式引用。比如:
set(DEBUG_ARM_FLAGS "$<$<AND:$<CONFIG:Debug>,$<PLATFORM_ID:ARM>>:-DDEBUG_ON_ARM>")
target_compile_options(my_app PRIVATE ${DEBUG_ARM_FLAGS})
这样可读性会好很多。
6.8 总结
生成器表达式是 CMake 里一个非常强大的工具。它让编译选项的条件控制变得灵活而简洁。我个人建议:
- 简单条件直接用
$<CONFIG:...> - 复杂逻辑拆成多个
target_*命令 - 实在搞不定的时候,用
if块 +set_property兜底
记住,工具是为人服务的。别为了炫技把代码写成一团乱麻。嗯,今天就到这里,下次我们聊聊 CMake 的模块化设计。
公众号:蓝海资料掘金营,微信deep3321