第九章:生成器表达式——CMake 的“运行时魔法”
说实话,我第一次接触生成器表达式时,心里想的是:“这玩意儿是啥?怎么长得像乱码?”
但用了一段时间后,我彻底爱上了它。为什么呢?因为它解决了 CMake 里一个老大难问题:配置时不知道,生成时才知道的信息。
举个例子。你在写 CMakeLists.txt 时,根本不知道用户用的是 Debug 还是 Release 配置。也不知道编译器是 GCC 还是 MSVC。更不知道目标平台是 32 位还是 64 位。这些信息,只有在 CMake 生成构建系统(比如 Makefile 或 Visual Studio 工程)时才会确定。
生成器表达式,就是用来处理这种“延迟求值”场景的。
9.1 什么是生成器表达式?
生成器表达式(Generator Expression)是 CMake 提供的一种特殊语法。它不会在 cmake .. 配置阶段被求值,而是在 cmake --build 或生成构建系统时被展开。
说白了,它就像 C++ 里的模板——写的时候是个占位符,用的时候才被替换成真实值。
它的通用语法长这样:
$<...>
尖括号里写的是具体的表达式。CMake 支持几十种内置的表达式,我挑最常用的给你讲。
9.2 条件表达式:让 CMake 学会“如果”
最基础的一类,是条件判断。语法是:
$<condition:value>
如果 condition 为真,就展开为 value;否则展开为空字符串。
举个实际例子。我在项目中经常需要根据编译器类型添加不同的编译选项:
target_compile_options(myapp PRIVATE
$<$<CXX_COMPILER_ID:MSVC>:/W4>
$<$<CXX_COMPILER_ID:GNU>:-Wall -Wextra>
)
你看,这里用了嵌套的生成器表达式。内层 $<CXX_COMPILER_ID:MSVC> 判断编译器是不是 MSVC,如果是,外层就展开为 /W4;如果是 GNU,就展开为 -Wall -Wextra。
9.3 编译选项中的实战用法
编译选项是生成器表达式最常用的场景之一。为什么?因为不同编译器、不同配置、不同平台,需要的编译选项天差地别。
我曾经维护过一个跨平台项目,Windows 用 MSVC,Linux 用 GCC,macOS 用 Clang。每个编译器对警告选项的命名都不一样。用生成器表达式,一份 CMakeLists.txt 搞定:
target_compile_options(myapp PRIVATE
# MSVC 特有的选项
$<$<CXX_COMPILER_ID:MSVC>:
/W4 /utf-8 /Zc:__cplusplus
>
# GCC 和 Clang 共用的选项
$<$<OR:$<CXX_COMPILER_ID:GNU>,$<CXX_COMPILER_ID:Clang>>:
-Wall -Wextra -Wpedantic
>
# 只有 Debug 才加的选项
$<$<CONFIG:Debug>:
-g -O0 -DDEBUG
>
# 只有 Release 才加的选项
$<$<CONFIG:Release>:
-O2 -DNDEBUG
>
)
嗯,这里要注意:$<OR:...> 是逻辑或操作,可以组合多个条件。类似的还有 $<AND:...> 和 $<NOT:...>。
9.4 链接中的生成器表达式
链接阶段同样离不开生成器表达式。最常见的场景是:Debug 配置链接 Debug 版本的库,Release 配置链接 Release 版本的库。
比如我有个第三方库,它的 Debug 版本叫 mylib_d.lib,Release 版本叫 mylib.lib。用生成器表达式可以优雅处理:
target_link_libraries(myapp PRIVATE
$<$<CONFIG:Debug>:mylib_d>
$<$<CONFIG:Release>:mylib>
)
另一个常见需求是:只在某个平台上链接特定库。比如 Windows 上需要链接 ws2_32,Linux 上不需要:
target_link_libraries(myapp PRIVATE
$<$<PLATFORM_ID:Windows>:ws2_32>
)
set() 或 message() 等配置阶段执行的命令中。我曾经犯过这个错,在 message() 里打印 $<CONFIG>,结果输出的是原样字符串,不是实际值。因为 message() 在配置阶段就执行了,而生成器表达式要等到生成阶段才展开。
9.5 目标属性的条件设置
除了编译选项和链接库,生成器表达式还能用在 target_compile_definitions()、target_include_directories() 等命令中。
举个例子,我想在 Debug 模式下定义一个 _DEBUG 宏:
target_compile_definitions(myapp PRIVATE
$<$<CONFIG:Debug>:_DEBUG>
)
或者根据编译器版本决定是否启用某个特性:
target_compile_definitions(myapp PRIVATE
$<$<VERSION_GREATER:$<CXX_COMPILER_VERSION>,7.0>:HAS_CXX17>
)
这个 $<VERSION_GREATER:...> 可以比较版本号。如果编译器版本大于 7.0,就定义 HAS_CXX17 宏。
9.6 常用生成器表达式速查表
我把最常用的几个整理成了表格,方便你查阅:
| 表达式 | 说明 | 示例 |
|---|---|---|
$<CONFIG:cfg> |
当前配置是否为 cfg | $<$<CONFIG:Debug>:-g> |
$<PLATFORM_ID:platform> |
当前平台是否为 platform | $<$<PLATFORM_ID:Windows>:win> |
$<CXX_COMPILER_ID:id> |
编译器是否为 id | $<$<CXX_COMPILER_ID:MSVC>:/EHsc> |
$<TARGET_FILE:tgt> |
目标 tgt 的输出文件路径 | $<TARGET_FILE:myapp> |
$<TARGET_PROPERTY:tgt,prop> |
目标 tgt 的属性 prop 的值 | $<TARGET_PROPERTY:myapp,OUTPUT_NAME> |
$<JOIN:list,sep> |
用 sep 连接列表 | $<JOIN:$<TARGET_OBJECTS:myobj>,> |
$<LOWER_CASE:string> |
转小写 | $<LOWER_CASE:Hello> → hello |
$<UPPER_CASE:string> |
转大写 | $<UPPER_CASE:Hello> → HELLO |
9.7 生成器表达式的执行时机
理解执行时机很重要。我画了一张图,帮你理清 CMake 的整个流程:
从这张图可以看得很清楚:set()、message() 这些命令在配置阶段就执行了。而生成器表达式要等到生成阶段才展开。这就是为什么你不能在 message() 里打印 $<CONFIG> 的原因——那时候它还是个占位符。
9.8 避坑指南:我踩过的几个坑
最后分享几个我实际踩过的坑,希望能帮你省点时间。
坑一:忘记嵌套条件
我曾经写过这样的代码:
# 错误写法
target_compile_options(myapp PRIVATE
$<CONFIG:Debug>:-g
)
这不会报错,但也不会生效。正确的写法是嵌套一层:
# 正确写法
target_compile_options(myapp PRIVATE
$<$<CONFIG:Debug>:-g>
)
坑二:在配置阶段使用生成器表达式
前面已经提过了,message() 里不能用。另外 if() 判断里也不能用。如果你需要在配置阶段做判断,用常规的 if() 语法。
坑三:字符串比较时忘记引号
有时候生成器表达式展开后可能包含空格或特殊字符。保险起见,在 target_compile_options 里用引号包一下:
target_compile_options(myapp PRIVATE
"$<$<CONFIG:Debug>:-g -O0>"
)