第10章:生成器表达式:Generator Expression基础、条件编译、不同配置下的不同行为
生成器表达式,说白了就是CMake在构建时才会求值的「魔法字符串」。我刚开始接触这玩意儿的时候,也觉得它有点绕——明明写的是字符串,怎么还能根据配置变来变去?但用熟了之后,你会发现它简直是CMake里最灵活的工具之一。
10.1 什么是生成器表达式
生成器表达式(Generator Expression)是CMake中一种特殊的语法,用$<...>包裹。它不是在CMakeLists.txt解析时求值,而是要等到生成构建系统(比如Makefile或Ninja文件)时才计算。
为什么要等到生成时才求值?因为有些信息在CMake配置阶段根本不知道。比如:
- 当前是Debug还是Release配置?
- 目标平台是32位还是64位?
- 某个编译器特性是否支持?
这些信息,只有到了生成阶段才能确定。生成器表达式就是为这种场景设计的。
核心概念:生成器表达式是「延迟求值」的。它把决策推迟到构建系统生成的那一刻。
10.2 基本语法与类型
生成器表达式的语法很简单,就是$<expression>。但里面的内容可以很复杂。我把它分成几类:
10.2.1 布尔条件表达式
这类表达式返回真或假,常用于条件判断:
# 如果配置是Debug,则添加-DDEBUG宏
target_compile_definitions(myapp PRIVATE
$<$<CONFIG:Debug>:DEBUG>
)
# 如果平台是Windows,则链接ws2_32库
target_link_libraries(myapp PRIVATE
$<$<PLATFORM_ID:Windows>:ws2_32>
)
这里有个嵌套结构:$<$<条件>:值>。条件为真时,整个表达式替换为「值」;条件为假时,替换为空字符串。
小技巧:我个人习惯把这种嵌套表达式读作「如果...那么...」。比如$<$<CONFIG:Debug>:DEBUG>就是「如果是Debug配置,那么添加DEBUG宏」。
10.2.2 字符串查询表达式
这类表达式直接返回字符串信息:
# 获取目标myapp的输出名称
$<TARGET_FILE_NAME:myapp>
# 获取目标myapp的完整路径
$<TARGET_FILE:myapp>
# 获取当前编译器的ID
$<CMAKE_CXX_COMPILER_ID>
嗯,这里要注意:TARGET_FILE这类表达式只能在生成器表达式的上下文中使用,不能直接用在set()命令里。
10.2.3 数值比较表达式
有时候你需要比较版本号或数字:
# 如果C++标准大于等于17
$<$<VERSION_GREATER_EQUAL:$<CMAKE_CXX_STANDARD>,17>:CXX17_AVAILABLE>
# 如果配置是Debug或Release
$<$<OR:$<CONFIG:Debug>,$<CONFIG:Release>>:OPTIMIZED_BUILD>
我记得有一次,我需要根据Qt版本决定是否启用某个特性。用VERSION_GREATER_EQUAL一行就搞定了,比写一堆if/else清爽得多。
10.3 条件编译实战
条件编译是生成器表达式最常见的应用场景。说白了,就是让同一个源码在不同配置下编译出不同的行为。
10.3.1 配置相关的宏定义
add_executable(myapp main.cpp)
target_compile_definitions(myapp PRIVATE
$<$<CONFIG:Debug>:DEBUG_MODE>
$<$<CONFIG:Release>:NDEBUG>
$<$<CONFIG:RelWithDebInfo>:DEBUG_MODE;NDEBUG>
)
这样,在Debug模式下,源码里可以这样用:
#ifdef DEBUG_MODE
std::cout << "当前是调试模式,输出详细日志" << std::endl;
#endif
10.3.2 平台相关的编译选项
我在项目中遇到过跨平台编译的问题。Windows用MSVC,Linux用GCC,macOS用Clang。每个平台的编译选项都不一样:
target_compile_options(myapp PRIVATE
$<$<PLATFORM_ID:Windows>:/W4 /utf-8>
$<$<PLATFORM_ID:Linux>:-Wall -Wextra -Wpedantic>
$<$<PLATFORM_ID:Darwin>:-Wall -Wextra -stdlib=libc++>
)
你想想看,如果没有生成器表达式,你得写三个if(WIN32)、if(UNIX)、if(APPLE)块,代码立马膨胀三倍。
10.4 不同配置下的不同行为
生成器表达式不仅能控制宏定义和编译选项,还能控制链接、源文件、包含路径等几乎所有构建方面。
10.4.1 链接不同的库
target_link_libraries(myapp PRIVATE
$<$<CONFIG:Debug>:debug_lib>
$<$<CONFIG:Release>:release_lib>
common_lib
)
Debug链接调试库,Release链接发布库,common_lib则始终链接。这种模式在大型项目中很常见。
10.4.2 选择不同的源文件
target_sources(myapp PRIVATE
main.cpp
$<$<PLATFORM_ID:Windows>:win32_utils.cpp>
$<$<PLATFORM_ID:Linux>:linux_utils.cpp>
$<$<PLATFORM_ID:Darwin>:macos_utils.cpp>
)
这样,每个平台只编译自己需要的源文件,不会出现「Windows的代码在Linux上编译报错」的情况。
10.4.3 控制包含路径
target_include_directories(myapp PRIVATE
$<$<CONFIG:Debug>:${CMAKE_SOURCE_DIR}/debug_headers>
$<$<CONFIG:Release>:${CMAKE_SOURCE_DIR}/release_headers>
)
避坑指南:我曾经在target_include_directories里用生成器表达式控制路径,结果忘了加PRIVATE关键字,导致头文件泄露给了依赖目标。记住:生成器表达式只控制「是否添加」,不控制「作用域」。
10.5 生成器表达式的组合与嵌套
生成器表达式可以组合使用,实现复杂的逻辑判断。我画了一张图来展示它们的关系:
从图中可以看到,生成器表达式分为三大类,最终都服务于同一个目标:让构建系统根据环境自动调整行为。
10.6 常见陷阱与避坑指南
生成器表达式虽然强大,但用不好也会踩坑。我把自己踩过的坑分享给你:
陷阱1:在非生成器表达式上下文中使用
我曾经在set()命令里直接写set(MY_VAR $<CONFIG:Debug>),结果MY_VAR的值就是字符串$<CONFIG:Debug>,根本没求值。记住:生成器表达式只在target_*命令、add_custom_command等特定上下文中有效。
陷阱2:嵌套过多导致可读性下降
我见过有人写出这样的代码:$<$<AND:$<CONFIG:Debug>,$<PLATFORM_ID:Windows>>:WIN_DEBUG>。虽然功能正确,但读起来很费劲。我的建议是:如果条件太复杂,先在CMakeLists.txt里用set()定义中间变量,再在生成器表达式里引用。
避坑指南:我曾经在调试一个跨平台项目时,发现Linux上编译正常,Windows上却报链接错误。查了半天,原来是一个生成器表达式里用了PLATFORM_ID:Windows,但Windows上的CMake返回的是Windows(注意大小写)。生成器表达式是大小写敏感的!
10.7 实战:一个完整的例子
最后,我给你看一个我实际项目中用过的例子。这个项目需要支持Windows、Linux、macOS三个平台,同时有Debug和Release两种配置:
add_executable(myapp main.cpp)
# 平台相关的源文件
target_sources(myapp PRIVATE
$<$<PLATFORM_ID:Windows>:win32_main.cpp>
$<$<PLATFORM_ID:Linux>:linux_main.cpp>
$<$<PLATFORM_ID:Darwin>:macos_main.cpp>
)
# 配置相关的宏定义
target_compile_definitions(myapp PRIVATE
$<$<CONFIG:Debug>:DEBUG_BUILD>
$<$<CONFIG:Release>:RELEASE_BUILD>
)
# 平台+配置组合的编译选项
target_compile_options(myapp PRIVATE
$<$<AND:$<PLATFORM_ID:Windows>,$<CONFIG:Debug>>:/Od /Zi>
$<$<AND:$<PLATFORM_ID:Windows>,$<CONFIG:Release>>:/O2 /GL>
$<$<AND:$<PLATFORM_ID:Linux>,$<CONFIG:Debug>>:-O0 -g>
$<$<AND:$<PLATFORM_ID:Linux>,$<CONFIG:Release>>:-O2 -DNDEBUG>
)
# 链接不同的库
target_link_libraries(myapp PRIVATE
common_lib
$<$<PLATFORM_ID:Windows>:ws2_32;iphlpapi>
$<$<PLATFORM_ID:Linux>:pthread;dl>
$<$<PLATFORM_ID:Darwin>:pthread>
)
这个例子几乎涵盖了生成器表达式的所有常见用法。你想想看,如果没有生成器表达式,这些逻辑得用多少if/elseif/endif来实现?而且每次新增一个平台或配置,都得改一大片代码。
生成器表达式让CMake代码更简洁、更可维护。我个人觉得,它是CMake里最值得花时间掌握的特性之一。刚开始可能会觉得有点绕,但用上几次之后,你就会发现它的妙处了。
公众号:蓝海资料掘金营,微信deep3321