引言:施工队长的“动态翻译词典”
还记得在 3.3 节里,我们把生成器表达式(Generator Expressions)比作施工队长的“智能便签”吗?它最大的魔力在于:那些写在 CMakeLists.txt 里的表达式不会在配置阶段(Configure)立刻求值,而是被CMake原样记录下来,等到真正生成构建系统(Generate)时,再根据当时的配置、平台、目标属性等上下文动态算出结果。
如果说 3.3 节是一本“便签使用入门”,那这篇附录就是队长办公桌上的《动态翻译词典》。当你在日常施工中遇到复杂的条件编译、路径处理、目标信息查询时,随时翻开它查阅即可。
一、布尔表达式:现场决策的“逻辑开关”
布尔表达式是生成器表达式的“大脑”,负责在各种条件下做出真/假判断。它们通常嵌套在其他表达式内部,控制值的去留。
$<BOOL:字符串> —— 把文本变成“是/否”
这个表达式会把一个字符串转换成布尔值。CMake 的判断规则很“苛刻”:只有以下几种情况会被视为假(false),其余皆为真(true):
- 空字符串
- 数值
0 - 不区分大小写的
FALSE、OFF、N、IGNORE、NOTFOUND - 以
-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 开始,生成器表达式直接支持对列表进行 APPEND、INSERT、REMOVE_DUPLICATES、SORT 等操作,无需在配置阶段用 list() 命令预处理。
# 去重后再传给链接器
target_link_options(app PRIVATE
$
)
大小写转换与标识符生成
- $<UPPER_CASE:字符串>:转大写
- $<LOWER_CASE:字符串>:转小写
- $<MAKE_C_IDENTIFIER:字符串>:把字符串转换成合法的 C 语言标识符,非法字符会被替换成下划线
# 把项目名称转大写作为宏前缀
target_compile_definitions(app PRIVATE
$_VERSION=1
)
如果 PROJECT_NAME 是 MyProject,则展开为 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 简化标识,常见取值包括 Windows、Linux、Darwin(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_definitions、add_custom_command、file(GENERATE) 等),不能放在 if()、foreach() 等控制流命令中。
结语:把词典放进工具箱
生成器表达式是 Modern CMake 从“静态脚本”进化为“动态构建系统”的关键所在。这篇附录覆盖了布尔逻辑、字符串加工、目标查询、配置与平台匹配四大类核心表达式,基本上能覆盖你日常 90% 的使用场景。
当然,CMake 的官方文档里还藏着更多“冷门但好用”的表达式(比如 $<INSTALL_INTERFACE:>、$<BUILD_INTERFACE:>、$<GENEX_EVAL:> 等)。不过那些已经属于“高阶黑魔法”范畴了——等你把今天这本《动态翻译词典》翻烂之后,再去挑战也不迟。
下一篇附录,我们将整理 CMake 内置的 Find 模块速查表,让你在面对茫茫多的第三方库时,也能像查字典一样快速定位需要的模块。


没有回复内容