50. 附录B:生成器表达式完整参考

引言:施工队长的“动态翻译词典”

还记得在 3.3 节里,我们把生成器表达式(Generator Expressions)比作施工队长的“智能便签”吗?它最大的魔力在于:那些写在 CMakeLists.txt 里的表达式不会在配置阶段(Configure)立刻求值,而是被CMake原样记录下来,等到真正生成构建系统(Generate)时,再根据当时的配置、平台、目标属性等上下文动态算出结果。

如果说 3.3 节是一本“便签使用入门”,那这篇附录就是队长办公桌上的《动态翻译词典》。当你在日常施工中遇到复杂的条件编译、路径处理、目标信息查询时,随时翻开它查阅即可。

一、布尔表达式:现场决策的“逻辑开关”

布尔表达式是生成器表达式的“大脑”,负责在各种条件下做出真/假判断。它们通常嵌套在其他表达式内部,控制值的去留。

$<BOOL:字符串> —— 把文本变成“是/否”

这个表达式会把一个字符串转换成布尔值。CMake 的判断规则很“苛刻”:只有以下几种情况会被视为假(false),其余皆为真(true)

  • 空字符串
  • 数值 0
  • 不区分大小写的 FALSEOFFNIGNORENOTFOUND
  • -NOTFOUND 结尾的字符串

典型用法是检查某个变量是否有“真值”:

target_compile_definitions(app PRIVATE
    $<$:USE_FEATURE>
)

如果 ENABLE_FEATURE 被设为 ON,则 USE_FEATURE 宏会被添加;如果为 OFF 或未定义,则整个表达式展开为空,相当于什么都没发生。

$<IF:条件,真值,假值> —— 三元运算符

这是布尔表达式里使用频率最高的“大杀器”,相当于 C++ 里的 ?: 三元运算符。注意它的语法是逗号分隔的三个参数。

target_link_libraries(app PRIVATE
    $<IF:$,libd.so,lib.so>
)

上例表示:如果是 Debug 配置,就链接 libd.so;否则链接 lib.so

$<AND:条件1,条件2,…> / $<OR:…> / $<NOT:条件>

这三个是标准的逻辑组合运算符,支持多个条件嵌套。

# 只有当平台是 Windows 并且配置是 Release 时才定义 NDEBUG
target_compile_definitions(app PRIVATE
    $<$<AND:$,$>:NDEBUG>
)

$<NOT:> 则用于取反:

# 只要不是 Debug 配置,就开启优化宏
target_compile_definitions(app PRIVATE
    $<$<NOT:$>:OPTIMIZE>
)

二、字符串表达式:文本的“现场加工车间”

当布尔表达式负责“判断”,字符串表达式就负责“加工”。它们常用来处理列表、转换大小写、生成合法标识符等。

$<JOIN:列表,分隔符> —— 把列表粘成字符串

CMake 的列表本质是分号分隔的字符串。$<JOIN:> 可以把这些列表元素用你指定的分隔符重新拼接。

set(MY_LIST "a;b;c")
# 生成器表达式版本
target_compile_options(app PRIVATE
    $
)

上例会把 a;b;c 转换成 a b c(注意第二个参数里包含了一个空格)。这在需要把 CMake 列表转换为编译器命令行参数时非常有用。

$<LIST:操作,列表,参数…> —— 列表的原子操作(CMake 3.18+)

从 CMake 3.18 开始,生成器表达式直接支持对列表进行 APPENDINSERTREMOVE_DUPLICATESSORT 等操作,无需在配置阶段用 list() 命令预处理。

# 去重后再传给链接器
target_link_options(app PRIVATE
    $
)

大小写转换与标识符生成

  • $<UPPER_CASE:字符串>:转大写
  • $<LOWER_CASE:字符串>:转小写
  • $<MAKE_C_IDENTIFIER:字符串>:把字符串转换成合法的 C 语言标识符,非法字符会被替换成下划线
# 把项目名称转大写作为宏前缀
target_compile_definitions(app PRIVATE
    $_VERSION=1
)

如果 PROJECT_NAMEMyProject,则展开为 MYPROJECT_VERSION=1

三、目标表达式:Target 信息的“实时调取”

这是 Modern CMake 中最常用的一类生成器表达式,它们让你能够在构建时查询某个 Target 的“身份信息”——它最终生成的文件在哪?叫什么名字?有哪些属性?

文件路径类

假设你有一个目标叫 mylib,下面这些表达式可以在生成阶段精确获取它的输出信息:

  • $<TARGET_FILE:mylib>:目标文件的完整绝对路径(如 /path/to/libmylib.so
  • $<TARGET_FILE_NAME:mylib>:仅文件名(如 libmylib.so
  • $<TARGET_FILE_DIR:mylib>:仅所在目录(如 /path/to
  • $<TARGET_SONAME_FILE:mylib>:共享库的 SONAME 文件路径(仅适用于支持 SONAME 的平台)

典型应用场景是在 add_custom_command 中,把某个库文件复制到指定目录:

add_custom_command(TARGET app POST_BUILD
    COMMAND ${CMAKE_COMMAND} -E copy
        $
        ${CMAKE_BINARY_DIR}/deploy/
)

属性查询类

$<TARGET_PROPERTY:目标名,属性名> 可以读取任意目标属性。这在接口库(Interface Library)中尤其好用,你可以根据依赖目标的属性动态调整自己的配置。

# 读取 mylib 的 INCLUDE_DIRECTORIES 属性
target_include_directories(app PRIVATE
    $
)

不过通常更推荐用 target_link_libraries 配合 PRIVATE/PUBLIC/INTERFACE 自动传播,只有在需要读取非传递性属性时才直接用这个表达式。

对象文件类

$<TARGET_OBJECTS:目标名> 专门用于提取 OBJECT 库(对象库)中的 .o / .obj 文件列表。这在 2.1 节中我们提到过,它是 Unity Build 或手动控制链接顺序的利器。

add_library(objlib OBJECT src1.cpp src2.cpp)

add_executable(app main.cpp $)

四、配置表达式:构建环境的“身份标牌”

配置表达式用来感知当前的构建环境——是 Debug 还是 Release?是 Windows 还是 Linux?是 GCC 还是 MSVC?它们就像工地入口的身份标牌,告诉队长该按哪套标准施工。

$<CONFIG:配置1,配置2,…> —— 配置匹配

这是最常见的配置表达式。如果当前构建类型匹配括号内的任意一个配置,展开为 1(真),否则展开为 0(假)。

# 仅在 Debug 或 RelWithDebInfo 下保留调试符号相关的定义
target_compile_definitions(app PRIVATE
    $<$:KEEP_SYMBOLS>
)

注意:它和 $<IF:$<CONFIG:Debug>,A,B> 经常搭配使用,实现“按配置分发不同值”。

$<PLATFORM_ID:平台ID> —— 平台识别

PLATFORM_ID 对应 CMake 的 CMAKE_SYSTEM_NAME 简化标识,常见取值包括 WindowsLinuxDarwin(macOS)等。

# Windows 下链接 ws2_32,其他平台忽略
target_link_libraries(app PRIVATE
    $<$:ws2_32>
)

扩展:编译器与语言标识(选学)

作为“完整参考”,这里再补充几个同家族的常用表达式:

  • $<CXX_COMPILER_ID:GNU,Clang,MSVC>:匹配 C++ 编译器厂商
  • $<C_COMPILER_VERSION:版本范围>:匹配 C 编译器版本
  • $<LINK_LANGUAGE:CXX>:判断当前目标的链接语言

它们和 $<CONFIG:>$<PLATFORM_ID:> 的语法逻辑完全一致,都是“匹配则为真,否则为假”。

五、调试技巧:当“动态便签”失灵时

生成器表达式虽然强大,但因为它延迟求值,调试起来比普通变量更麻烦。这里给队长们三个实用技巧:

技巧 1:用 file(GENERATE) 导出结果

如果你想知道某个复杂的生成器表达式最终展开成什么,可以把它写到一个文件里:

file(GENERATE OUTPUT debug_genex.txt CONTENT
    "Result: $<IF:$,yes,no>n"
)

配置并生成后,查看 debug_genex.txt 即可验证。

技巧 2:在自定义命令中 echo

add_custom_target(show_genex
    COMMAND ${CMAKE_COMMAND} -E echo "$"
)

运行 cmake --build . --target show_genex 就能在终端看到展开后的路径。

技巧 3:避免在 if() 命令中直接使用

很多新手会犯这个错:

# 错误!if() 在配置阶段求值,此时生成器表达式还未展开
if($)
    ...
endif()

记住:生成器表达式只能用在命令的参数位置(如 target_compile_definitionsadd_custom_commandfile(GENERATE) 等),不能放在 if()foreach() 等控制流命令中。

结语:把词典放进工具箱

生成器表达式是 Modern CMake 从“静态脚本”进化为“动态构建系统”的关键所在。这篇附录覆盖了布尔逻辑、字符串加工、目标查询、配置与平台匹配四大类核心表达式,基本上能覆盖你日常 90% 的使用场景。

当然,CMake 的官方文档里还藏着更多“冷门但好用”的表达式(比如 $<INSTALL_INTERFACE:>$<BUILD_INTERFACE:>$<GENEX_EVAL:> 等)。不过那些已经属于“高阶黑魔法”范畴了——等你把今天这本《动态翻译词典》翻烂之后,再去挑战也不迟。

下一篇附录,我们将整理 CMake 内置的 Find 模块速查表,让你在面对茫茫多的第三方库时,也能像查字典一样快速定位需要的模块。

请登录后发表评论

    没有回复内容

正在唤醒异次元光景……