16、生成器表达式:Generator Expressions的语法与使用场景,条件编译、配置相关路径

生成器表达式,说白了就是CMake在构建时才会计算的「魔法变量」。

我刚开始接触这东西时,也觉得它有点绕——明明有普通的变量,为什么还要搞一套新语法?直到我在一个跨平台项目里被路径问题折磨了一整天,才真正体会到它的价值。

什么是生成器表达式?

普通变量在cmake配置阶段就求值了。而生成器表达式要等到构建阶段(也就是makeninja运行时)才展开。

你想想看,这意味着什么?意味着你可以根据当前的构建配置、编译器、目标平台,动态地决定某些值。

基本语法长这样:

$<condition:value>

如果condition为真,就返回value,否则返回空字符串。

核心区别:普通变量是「配置时确定」,生成器表达式是「构建时确定」。

语法速览

我个人习惯把生成器表达式分成三类,这样记起来比较清楚:

类别 示例 说明
条件判断 $<$<CONFIG:Debug>:-g> Debug模式下返回-g
查询信息 $<TARGET_FILE:mylib> 获取目标mylib的文件路径
字符串操作 $<JOIN:list,;> 用分号拼接列表

嗯,这里要注意:生成器表达式可以嵌套。比如$<$<CONFIG:Debug>:-O0>,意思是「如果是Debug配置,就返回-O0」。这种写法在设置编译选项时特别常用。

使用场景一:条件编译

我在项目中遇到过最典型的场景,就是不同平台需要不同的宏定义。

target_compile_definitions(myapp PRIVATE
    $<$<PLATFORM_ID:Windows>:_WIN32>
    $<$<PLATFORM_ID:Linux>:_LINUX>
    $<$<PLATFORM_ID:Darwin>:_MACOS>
)

这段代码的意思是:在Windows上定义_WIN32,在Linux上定义_LINUX,在macOS上定义_MACOS

你可能会问:「用if(WIN32)不也能实现吗?」

没错,但if是配置时判断,而生成器表达式是构建时判断。如果你的项目支持多配置生成器(比如Visual Studio的Debug/Release),用if就只能选一个,而生成器表达式可以同时保留所有配置的信息。

小技巧:我一般把平台相关的宏定义用生成器表达式写,把项目逻辑相关的宏定义用if写。这样既清晰又灵活。

使用场景二:配置相关路径

这个场景我印象特别深。有一次我需要根据构建类型链接不同的库文件——Debug版链接mylib_d.lib,Release版链接mylib.lib

target_link_libraries(myapp PRIVATE
    $<$<CONFIG:Debug>:${CMAKE_CURRENT_SOURCE_DIR}/lib/debug/mylib_d.lib>
    $<$<CONFIG:Release>:${CMAKE_CURRENT_SOURCE_DIR}/lib/release/mylib.lib>
)

这样写,CMake会在构建时自动选择正确的库文件路径。你不需要在配置时手动切换,也不会因为忘记改路径而链接错库。

类似的,头文件路径也可以这样处理:

target_include_directories(myapp PRIVATE
    $<$<CONFIG:Debug>:${CMAKE_CURRENT_SOURCE_DIR}/include/debug>
    $<$<CONFIG:Release>:${CMAKE_CURRENT_SOURCE_DIR}/include/release>
)

使用场景三:目标属性查询

生成器表达式还能帮你获取目标的各种属性。比如你想知道某个库编译后的文件名:

add_custom_command(TARGET myapp POST_BUILD
    COMMAND ${CMAKE_COMMAND} -E copy
        $<TARGET_FILE:mylib>
        ${CMAKE_CURRENT_BINARY_DIR}/output/
)

这段代码会在构建完成后,把mylib库文件复制到output目录。不管mylib是静态库还是动态库,不管它在哪个平台,$<TARGET_FILE:mylib>都能给出正确的路径。

常用的目标查询表达式:

  • $<TARGET_FILE:tgt> — 目标文件的完整路径
  • $<TARGET_FILE_NAME:tgt> — 目标文件名
  • $<TARGET_FILE_DIR:tgt> — 目标文件所在目录
  • $<TARGET_LINKER_FILE:tgt> — 链接时使用的文件(比如.def文件)

避坑指南

我曾经踩过的坑:生成器表达式不能用在所有地方。比如set()if()foreach()这些命令里,生成器表达式是不会被展开的。它们只会在支持生成器表达式的上下文中生效,比如target_*系列命令、add_custom_commandinstall等。

另一个容易犯的错误是忘记转义。生成器表达式里如果包含><,在有些上下文中需要转义。比如在add_custom_commandCOMMAND里,你可能需要用引号包起来。

知识体系图

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

生成器表达式 条件判断 $<$<CONFIG:Debug>:flag> $<$<PLATFORM_ID:Linux>:def> $<$<COMPILE_LANGUAGE:CXX>:opt> 查询信息 $<TARGET_FILE:tgt> $<TARGET_FILE_NAME:tgt> $<TARGET_FILE_DIR:tgt> 字符串操作 $<JOIN:list,sep> $<LOWER_CASE:text> $<UPPER_CASE:text> 条件编译 配置路径 目标属性查询 构建时求值 · 支持嵌套 · 用于 target_* 系列命令

总结

生成器表达式是CMake里一个非常强大的工具。它让构建系统变得更灵活,也让你能写出更通用的CMakeLists.txt。

我个人建议:刚开始用的时候,先从简单的条件判断入手,比如根据配置类型设置编译选项。等熟悉了语法,再尝试查询目标属性、处理路径这些高级用法。

记住一点:如果你发现某个值需要在构建时才能确定,那大概率就该用生成器表达式了。

最后一个小建议:写生成器表达式时,尽量保持简洁。嵌套太多层的话,可读性会急剧下降。如果逻辑太复杂,不如拆成几个步骤,用普通变量先算一部分。


公众号:蓝海资料掘金营,微信deep3321