23 生成器表达式

生成器表达式

[TOC]

1. 前言

1.1 CMake 工作流程

​ CMake 工作流程分为两步,配置阶段和生成阶段。我们在命令行中输入cmake ..的时候,可以在输出内容的最后看到这样的两行话

-- Configuring done
-- Generating done

​ 分别指代了这两个阶段

Configure 阶段

​ 在这个阶段,CMake 会读取你的CMakeLists.txt文件,并执行其中的命令。这些命令可能会检查系统的特性(例如,查找库或者编译器特性),设置变量,或者定义构建目标(例如库、可执行文件,或者其他伪目标)。这个阶段的主要目的是确定构建过程需要的所有信息。

Generate 阶段

​ 在此阶段 CMake 会生成实际的建构档。这些文件包含了实际编译和链接你的代码所需要的命令,包括

  • 各种编译选项

  • 与目标相关的操作

​ 比方说

target_include_directories(
demo
PUBLIC include
)

target_link_libraries(
demo
PRIVATE core
)

​ 这两个命令均生效于生成阶段。而后文准备介绍的生成器表达式,就作用于生成阶段。也正因如此,只有能够在生成阶段生效的命令才能使用生成器表达式,而在配置阶段生效的命令,例如用于设置变量的 set 命令并不支持生成器表达式。

1.2 CMake 的生成器表达式是什么

​ 生成器表达式在生成构建系统期间进行计算,以生成特定于每个生成配置的信息。他们有如下形式:$<...>

可以将其看作是一个if语句,或者说是一个三元表达式。

1.3 为什么要引入生成器表达式

​ 在设置某些编译选项的时候,如果当条件较多,要制作的编译选项版本也相应增多。这时可以使用生成器表达式语法,让一部分编译信息在生成阶段自动完成,无需在配置阶段设置。

​ 下面是一个示例,演示了在生成阶段代替配置阶段的表达式:

# Configure 阶段:给Debug和Release模式指定编译条件
list(APPEND CMAKE_C_FLAGS_DEBUG "-g -O0")
list(APPEND CMAKE_CXX_FLAGS_DEBUG "-g -O0")
list(APPEND CMAKE_C_FLAGS_RELEASE "-O2")
list(APPEND CMAKE_CXX_FLAGS_RELEASE "-O2")

# 使用生成器表达式简化如下
add_compile_options("$<$<CONFIG:Debug>:-g;-O0>")
add_compile_options("$<$<CONFIG:Release>:-O2>")

​ 这里的add_compile_options就属于在生成阶段生效的命令。以上代码属于条件定义,即在不同的条件下生成不同的变量、宏等内容。其中这段代码使用生成器表达式的效果是,在不同的构建类型下(Debug、Release),会生成不同的被添加进编译选项的字符串。

2. 常用生成器表达式

2.1 布尔生成器表达式

2.1.1 逻辑运算符

  1. $<BOOL:string>

    如果字符串为空、0、不区分大小写的FALSEOFFNNOIGNORENOTFOUND,或者区分大小写的以-NOTFOUND结尾的字符串,则表达式的值为0,否则为1

    add_library(myLib ${SOURCES})

    target_compile_definitions(myLib
    PUBLIC $<$<BOOL:${USE_OTHERLIB}>:USE_OTHERLIB>
    )

    在这个例子中,$<BOOL:${USE_OTHERLIB}> 会检查变量 USE_OTHERLIB 是否为假。如果 USE_OTHERLIB 为假,那么这个表达式就会返回 0,否则返回 1。然后,$<1:USE_OTHERLIB> 会在前面的表达式为真时添加一个编译定义 USE_OTHERLIB

  2. $<AND:conditions>:逻辑与

    conditons是以逗号分割的条件列表,一般来说,条件是列表的,都是使用逗号进行分割,后文不再赘述。逻辑运算条件成立,表达式的值为0,否则为1,后文不再赘述。

    add_library(myLib ${SOURCES})

    target_compile_definitions(myLib
    PUBLIC $<$<AND:$<CONFIG:Debug>,$<TARGET_EXISTS:otherlib>>:USE_OTHERLIB>
    )

    在这个例子中,$<AND:$<CONFIG:Debug>,$<TARGET_EXISTS:otherlib>>会在当前配置为Debug并且目标 otherlib 存在时返回1,否则返回0。然后,$<1:USE_OTHERLIB> 会在前面的表达式为真时添加一个编译定义 USE_OTHERLIB

  3. $<OR:conditions>:逻辑或

    在给定的任何条件为真时返回1,否则返回0

    在这个例子中,$<OR:$<CONFIG:Debug>,$<TARGET_EXISTS:otherlib>>会在当前配置为 Debug 或者目标 otherlib 存在时返回 1,否则返回 0。然后,$<1:USE_OTHERLIB> 会在前面的表达式为真时添加一个编译定义 USE_OTHERLIB

  4. $<NOT:condition>:逻辑非

    在给定的条件为假时返回1,否则返回0

    在这个例子中,$<NOT:$<TARGET_EXISTS:otherlib>>会在目标otherlib不存在时返回1,否则返回0。然后,$<1:USE_OTHERLIB> 会在前面的表达式为真时添加一个编译定义 USE_OTHERLIB

2.1.2 字符串比较

  1. $<STREQUAL:string1,string2>

    判断字符串是否相等

  2. $<EQUAL:value1,value2>

    判断数值是否相等

  3. $<IN_LIST:string,list>

    判断string是否包含在list中,list使用分号分割

#注意事项

这里的list是在逗号后面的列表,即${...}所表示的内容,因此其内容需要使用分号分割。

2.1.3 变量查询

  1. $<TARGET_EXISTS:target>

    判断目标是否存在

  2. $<CONFIG:cfgs>

    判断编译类型配置是否包含在cfgs列表(比如"Release,Debug")中;不区分大小写

  3. $<PLATFORM_ID:platform_ids>

    判断CMake定义的平台ID是否包含在platform_ids列表中

  4. $<COMPILE_LANGUAGE:languages>

    判断编译语言是否包含在languages列表中

2.2 字符串生成器表达式

使用生成器表达式的主要目的可能就是因为此功能了。比如 CMake 官方的例子:基于编译器 ID 指定include目录:

include_directories("/usr/include/$<CXX_COMPILER_ID>/")

根据编译器的类型,$<CXX_COMPILER_ID>会被替换成对应的ID(比如“GNU”、“Clang”)。

2.2.1 条件表达式

  • $<condition:true_string>

    如果条件为真,则结果为true_string,否则为空

  • $<IF:condition,str1,str2>

    如果条件为真,则结果为str1,否则为str2

2.2.2 字符串转换

  • $<LOWER_CASE:string>

    将字符串转为小写

    add_library(mylib ${SOURCES})

    set(TARGET_NAME "MYLIB")

    target_compile_definitions(mylib PUBLIC
    TARGET_NAME=$<LOWER_CASE:${TARGET_NAME}>
    )

    在这个例子中, $<LOWER_CASE:${TARGET_NAME}>会将TARGET_NAME变量的值转换为小写。然后,这个小写的值会被赋给编译定义 TARGET_NAME

    所以,mylib 会被编译时定义 TARGET_NAMEmylib

  • $<UPPER_CASE:string>,将字符串转为大写

生成器表达式种类繁多,这里不再一一列举

参考资料

【09】CMake:生成器表达式-哔哩哔哩视频 (bilibili.com)