8、生成器表达式:生成器表达式基础、条件编译、配置相关逻辑、调试与发布区分
生成器表达式,说白了就是 CMake 在生成构建系统(比如 Makefile 或 Ninja 文件)时才去求值的一种特殊语法。它不像普通变量那样在 CMakeLists.txt 解析阶段就被替换掉,而是把求值时机推迟到了最后一步。
我个人习惯把生成器表达式比作「延迟加载」。你想想看,很多时候我们在写 CMake 时,并不知道最终构建的是 Debug 还是 Release,也不知道目标平台是哪个。这时候就需要一种机制,让这些决策在真正构建时再确定。生成器表达式就是干这个的。
8.1 生成器表达式基础
生成器表达式的语法很简单,用 $<...> 包裹起来。最基本的用法就是条件判断和变量取值。
# 基本语法
$<condition:value> # 如果 condition 为真,则返回 value
$<condition:value1:value2> # 如果 condition 为真,返回 value1,否则返回 value2
我在项目中遇到过不少新手,直接把生成器表达式当普通变量用,结果死活拿不到值。记住,生成器表达式只能在支持它的上下文中使用,比如 target_link_libraries、target_include_directories、target_compile_definitions 等命令的参数里。
核心要点:生成器表达式只在生成阶段求值,不能在 if() 或 set() 等命令中直接使用。
来看几个常用的生成器表达式:
| 表达式 | 含义 | 示例 |
|---|---|---|
$<CONFIG> |
当前构建配置名称 | Debug, Release, RelWithDebInfo |
$<PLATFORM_ID> |
当前平台标识 | Windows, Linux, Darwin |
$<TARGET_FILE:tgt> |
目标 tgt 的输出文件路径 | /path/to/libfoo.so |
$<BOOL:value> |
将 value 转为布尔值 | ON, OFF, 1, 0 |
小技巧:可以用 $<CONFIG> 来调试生成器表达式是否生效。比如在编译命令里加个 -DCURRENT_CONFIG=$<CONFIG>,然后在代码里打印出来看看。
8.2 条件编译与逻辑运算
条件编译是生成器表达式最常用的场景。你可以根据配置、平台、编译器等信息,动态决定编译哪些代码、链接哪些库。
# 只在 Debug 模式下添加调试符号
target_compile_definitions(myapp PRIVATE
$<$<CONFIG:Debug>:DEBUG_MODE>
)
# 根据平台链接不同的库
target_link_libraries(myapp PRIVATE
$<$<PLATFORM_ID:Windows>:ws2_32>
$<$<PLATFORM_ID:Linux>:pthread>
)
生成器表达式支持逻辑运算,可以组合多个条件:
# 逻辑与:Debug 且 Windows
$<$<AND:$<CONFIG:Debug>,$<PLATFORM_ID:Windows>>:WIN_DEBUG>
# 逻辑或:Debug 或 Release
$<$<OR:$<CONFIG:Debug>,$<CONFIG:Release>>:OPTIMIZED_OR_DEBUG>
# 逻辑非:不是 Debug
$<$<NOT:$<CONFIG:Debug>>:NOT_DEBUG>
我曾经在一个跨平台项目里,需要根据不同的编译器启用不同的警告选项。用生成器表达式写起来特别清爽:
target_compile_options(myapp PRIVATE
$<$<CXX_COMPILER_ID:MSVC>:/W4>
$<$<CXX_COMPILER_ID:GNU>:-Wall -Wextra>
$<$<CXX_COMPILER_ID:Clang>:-Wall -Weverything>
)
8.3 配置相关逻辑
配置相关逻辑,说白了就是根据不同的构建类型(Debug、Release 等)做不同的事情。这是生成器表达式最拿手的领域。
常见的配置相关表达式:
# 根据配置设置不同的输出目录
set_target_properties(myapp PROPERTIES
RUNTIME_OUTPUT_DIRECTORY_DEBUG "${CMAKE_BINARY_DIR}/debug/bin"
RUNTIME_OUTPUT_DIRECTORY_RELEASE "${CMAKE_BINARY_DIR}/release/bin"
)
# 或者用生成器表达式统一处理
set_target_properties(myapp PROPERTIES
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/$<CONFIG>/bin"
)
注意:属性名中的 _DEBUG、_RELEASE 后缀是 CMake 的约定,大小写敏感。如果你自定义了配置类型,比如 Profile,就需要用 _PROFILE 后缀。
我个人习惯把配置相关的逻辑集中管理,而不是散落在各个地方。比如这样:
# 集中定义配置相关的编译选项
target_compile_definitions(myapp PRIVATE
$<$<CONFIG:Debug>:ENABLE_LOGGING>
$<$<CONFIG:Debug>:ENABLE_ASSERTS>
$<$<CONFIG:Release>:NDEBUG>
$<$<CONFIG:RelWithDebInfo>:NDEBUG;ENABLE_PROFILING>
)
你想想看,如果不用生成器表达式,你可能得写三个不同的 target_compile_definitions 调用,或者用 if() 判断。用生成器表达式,一行搞定,多清爽。
8.4 调试与发布区分
调试和发布的区分,是生成器表达式最经典的应用场景。我见过不少项目,在 Debug 和 Release 下链接不同的库、使用不同的编译选项、甚至包含不同的源文件。
# 区分调试库和发布库
target_link_libraries(myapp PRIVATE
debug mylib_d
optimized mylib
)
# 等价于用生成器表达式
target_link_libraries(myapp PRIVATE
$<$<CONFIG:Debug>:mylib_d>
$<$<NOT:$<CONFIG:Debug>>:mylib>
)
这里有个坑,我曾经踩过。用 debug 和 optimized 关键字时,CMake 只识别 Debug 和 Release 这两个标准配置。如果你自定义了 Profile 配置,它会被当作 optimized 处理。用生成器表达式就没这个问题,你可以精确控制每个配置的行为。
最佳实践:尽量用生成器表达式替代 debug/optimized 关键字,更灵活、更可控。
再来看一个实际例子,区分调试和发布的编译选项:
# 调试版本:开启所有警告,关闭优化
# 发布版本:开启优化,关闭调试信息
target_compile_options(myapp PRIVATE
$<$<CONFIG:Debug>:-O0 -g -Wall -Wextra>
$<$<CONFIG:Release>:-O3 -DNDEBUG>
)
# 或者更细粒度地控制
target_compile_options(myapp PRIVATE
$<$<AND:$<CONFIG:Debug>,$<CXX_COMPILER_ID:GNU>>:-fno-omit-frame-pointer>
$<$<AND:$<CONFIG:Release>,$<CXX_COMPILER_ID:MSVC>>:/O2 /GL>
)
嗯,这里要注意,生成器表达式虽然强大,但也不是万能的。如果你需要做复杂的条件判断,比如根据多个变量的组合来决定行为,可能还是用 if() 更清晰。生成器表达式适合做「条件简单、场景固定」的判断。
8.5 知识体系总览
下面这张图总结了生成器表达式的核心知识体系,你可以对照着回顾一下:
生成器表达式是 CMake 进阶的必备技能。它让我们的构建脚本更灵活、更简洁。刚开始用的时候可能会觉得语法有点绕,但用习惯了就会发现,很多以前需要写一堆 if() 判断的场景,用生成器表达式几行就搞定了。
我的建议:先从简单的 $<CONFIG> 和 $<PLATFORM_ID> 开始用,慢慢再尝试逻辑组合。别一上来就想写复杂的嵌套表达式,容易把自己绕晕。
好了,生成器表达式的内容就讲到这里。记住它的核心思想:延迟求值、条件判断、配置感知。掌握了这三点,你就能在 CMake 里游刃有余地处理各种构建场景了。
公众号:蓝海资料掘金营,微信deep3321