13、生成器表达式:生成器表达式语法、条件表达式、目标相关表达式、编译特性表达式
生成器表达式,说白了就是 CMake 在生成构建系统(比如 Makefile 或 Ninja 文件)时才去求值的一种特殊语法。它不像普通的变量,在 CMakeLists.txt 解析阶段就被替换掉了。生成器表达式是延迟求值的,这意味着它能拿到构建时的上下文信息。
我个人习惯把生成器表达式看作是 CMake 里的「三目运算符」加「反射机制」的结合体。你想想看,它既能做条件判断,又能获取目标的各种属性,是不是很强大?
13.1 生成器表达式语法
生成器表达式的语法非常固定,就是用 $<...> 包裹起来。里面可以放各种关键字和表达式。最基本的格式就两种:
# 简单表达式
$<KEYWORD>
# 带参数的表达式
$<KEYWORD:value>
嗯,这里要注意:生成器表达式只能在支持它的命令中使用。比如 target_include_directories()、target_link_libraries()、add_compile_definitions() 这些命令都支持。但如果你在 message() 里直接写 $<CONFIG>,那它只会被当成普通字符串打印出来,不会求值。
set() 变量赋值中使用。我曾经踩过这个坑,在 set() 里写了半天生成器表达式,结果发现根本没生效。
13.2 条件表达式
条件表达式是生成器表达式里最常用的功能。它的语法是:
$<IF:condition,true_value,false_value>
如果 condition 为真,就返回 true_value,否则返回 false_value。这里的 condition 可以是一个布尔类型的生成器表达式,也可以是一个简单的 0 或 1。
举个例子,我在项目中经常需要根据不同的构建类型添加不同的编译选项:
target_compile_options(myapp PRIVATE
$<$<CONFIG:Debug>:-g -O0>
$<$<CONFIG:Release>:-O3 -DNDEBUG>
)
这里 $<CONFIG:Debug> 本身就是一个生成器表达式,如果当前构建配置是 Debug,它就求值为 1,否则为 0。然后外层的 $<...> 根据这个 1 或 0 来决定是否添加后面的编译选项。
常用的条件表达式还有:
| 表达式 | 说明 |
|---|---|
$<CONFIG:cfg> |
当前构建配置是否为 cfg |
$<PLATFORM_ID:platform> |
当前平台是否为 platform(如 Linux、Windows) |
$<COMPILE_LANGUAGE:lang> |
当前编译语言是否为 lang(如 CXX、C) |
$<VERSION_GREATER:v1,v2> |
版本号 v1 是否大于 v2 |
$<STREQUAL:s1,s2> |
字符串 s1 和 s2 是否相等 |
$<$<CONFIG:Debug>:-g> 这种写法,其实就是把 $<CONFIG:Debug> 作为外层 IF 的条件。我个人习惯把这种嵌套叫作「洋葱表达式」,剥开一层还有一层。
13.3 目标相关表达式
目标相关表达式可以获取某个目标(target)的属性。这是生成器表达式最强大的地方,因为它能让你在构建时动态地引用其他目标的信息。
基本语法是:
$<TARGET_PROPERTY:target,property>
$<TARGET_FILE:target>
$<TARGET_LINKER_FILE:target>
$<TARGET_SONAME_FILE:target>
举个例子,假设你有一个库目标 mylib,你想在可执行文件 myapp 的编译选项中包含这个库的头文件路径:
add_library(mylib STATIC mylib.cpp)
target_include_directories(mylib PUBLIC include)
add_executable(myapp main.cpp)
target_include_directories(myapp PRIVATE
$<TARGET_PROPERTY:mylib,INTERFACE_INCLUDE_DIRECTORIES>
)
不过说实话,这种写法有点绕。更常见的做法是用 target_link_libraries() 直接传递依赖,CMake 会自动处理这些属性传递。但如果你需要更精细的控制,目标相关表达式就派上用场了。
常用的目标相关表达式:
| 表达式 | 说明 |
|---|---|
$<TARGET_FILE:tgt> |
目标 tgt 的可执行文件或库文件的完整路径 |
$<TARGET_FILE_NAME:tgt> |
目标 tgt 的文件名(不含路径) |
$<TARGET_FILE_DIR:tgt> |
目标 tgt 的文件所在目录 |
$<TARGET_LINKER_FILE:tgt> |
目标 tgt 的链接器文件(.a 或 .lib)路径 |
$<TARGET_PROPERTY:tgt,prop> |
目标 tgt 的属性 prop 的值 |
$<TARGET_OBJECTS:tgt> |
目标 tgt 的对象文件列表(用于 OBJECT 库) |
if() 语句里直接使用它们。我曾经试图在 if($<TARGET_FILE:myapp> STREQUAL ...) 里做判断,结果 CMake 直接报错了。记住,生成器表达式不是变量,它不能用于控制流。
13.4 编译特性表达式
编译特性表达式是 CMake 3.x 引入的,用于检查编译器是否支持某些 C++ 标准特性。这在跨平台项目中特别有用,因为不同编译器对 C++ 标准的支持程度不一样。
基本语法是:
$<COMPILE_FEATURES:feature1,feature2,...>
如果编译器支持列出的所有特性,表达式求值为 1,否则为 0。举个例子:
target_compile_definitions(myapp PRIVATE
$<$<COMPILE_FEATURES:cxx_constexpr>:HAS_CONSTEXPR>
$<$<COMPILE_FEATURES:cxx_lambda_init_captures>:HAS_INIT_CAPTURES>
)
这段代码的意思是:如果编译器支持 constexpr,就定义 HAS_CONSTEXPR 宏;如果支持带初始化捕获的 lambda,就定义 HAS_INIT_CAPTURES 宏。这样你的源代码就可以根据这些宏来做条件编译。
常用的编译特性名称:
| 特性名称 | 说明 |
|---|---|
cxx_auto_type |
C++11 auto 类型推导 |
cxx_constexpr |
C++11 constexpr |
cxx_lambda_init_captures |
C++14 lambda 初始化捕获 |
cxx_generic_lambdas |
C++14 泛型 lambda |
cxx_if_constexpr |
C++17 constexpr if |
cxx_fold_expressions |
C++17 折叠表达式 |
cxx_concepts |
C++20 概念 |
cmake_minimum_required(VERSION 3.8) 或更高版本。另外,记得用 target_compile_features() 来声明你的目标需要哪些特性,这样 CMake 会自动检查编译器是否支持。
13.5 知识体系总览
下面这张图总结了生成器表达式的核心知识结构,方便你快速回顾:
生成器表达式是 CMake 进阶的必备技能。它让构建脚本变得更灵活、更智能。我个人建议你在实际项目中多尝试使用条件表达式和目标相关表达式,这两个用得最多。编译特性表达式虽然强大,但一般只在需要精细控制 C++ 标准兼容性时才用得上。
好了,这一章的内容就到这里。记住,生成器表达式的核心就是「延迟求值」这四个字。理解了这一点,你就能明白它和普通变量的本质区别了。