第六章:生成器表达式与编译选项条件控制

各位同学,今天我们来聊聊 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 默认的配置名是 DebugRelease,首字母大写。

6.6 知识体系图

下面这张图帮你理清生成器表达式的核心逻辑:

生成器表达式核心逻辑 $<condition:value> 条件类型:CONFIG / PLATFORM_ID / COMPILE_LANGUAGE Debug 配置 Release 配置 其他配置 输出:编译选项 / 链接脚本 / 宏定义

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