C++ CMake 进阶:Generator Expressions 与配置管理

好的,咱们今天来聊聊 CMake 的进阶玩法:Generator Expressions 和配置管理。这俩玩意儿就像 CMake 这辆战车里的涡轮增压和高级定制,能让你更好地掌控项目的构建过程,玩出更多花样。

啥是 Generator Expressions?

Generator Expressions,中文可以叫“生成器表达式”,听着有点玄乎,其实就是 CMake 在生成构建系统时(比如 Makefile、Ninja 文件等)才会计算的表达式。你可以把它理解成一个占位符,在 CMakeLists.txt 里定义好规则,然后在构建系统生成的时候,CMake 会根据具体情况替换成不同的值。

这玩意儿为啥有用?因为很多时候,我们需要根据不同的构建配置(比如 Debug、Release),不同的目标平台(比如 Windows、Linux),甚至是不同的编译器来调整编译选项、链接库等等。如果手动写一堆 if...else... 来判断,那 CMakeLists.txt 很快就会变成一坨意大利面,难以维护。

Generator Expressions 就像一个智能开关,可以根据不同的条件自动切换到不同的值,让你的 CMakeLists.txt 保持简洁和可读性。

Generator Expressions 的几种类型

Generator Expressions 主要分为以下几类:

  • 逻辑表达式(Logical Expressions): 用于判断条件是否成立,返回 0 或 1。
  • 信息表达式(Information Expressions): 用于获取目标、源文件或构建系统的属性。
  • 输出表达式(Output Expressions): 用于根据条件生成不同的字符串。

咱们一个一个来看。

1. 逻辑表达式

这类表达式用于进行逻辑判断,常见的有:

  • $<BOOL:bool>:如果 bool 为真(非 0),则返回 1,否则返回 0。
  • $<AND:op1,op2,...>:所有操作数都为真时返回 1,否则返回 0。
  • $<OR:op1,op2,...>:只要有一个操作数为真就返回 1,否则返回 0。
  • $<NOT:op>:对操作数取反,真变假,假变真。
  • $<STREQUAL:string1,string2>:如果两个字符串相等则返回 1,否则返回 0。
  • $<TARGET_PROPERTY:target,property>:如果目标 target 存在且属性 property 不为空,则返回 1,否则返回 0。
  • $<CONFIG:config>:如果当前配置是 config,则返回 1,否则返回 0。

举个例子,假设我们想根据构建类型(Debug 或 Release)来设置不同的优化级别:

set(CMAKE_CXX_FLAGS_DEBUG "-O0 -g") # Debug 模式下不优化,方便调试
set(CMAKE_CXX_FLAGS_RELEASE "-O3") # Release 模式下最高级别优化

add_executable(my_app main.cpp)

target_compile_options(my_app PRIVATE
  "$<$<CONFIG:Debug>:${CMAKE_CXX_FLAGS_DEBUG}>"
  "$<$<CONFIG:Release>:${CMAKE_CXX_FLAGS_RELEASE}>"
)

这段代码的意思是:

  • 如果当前构建类型是 Debug,则使用 -O0 -g 编译选项。
  • 如果当前构建类型是 Release,则使用 -O3 编译选项。

target_compile_options 命令用于给目标 my_app 添加编译选项。PRIVATE 关键字表示这些选项只对 my_app 自身有效,不会传递给其他依赖它的目标。

再来一个例子,根据目标平台设置不同的编译器标志:

add_executable(my_app main.cpp)

if (CMAKE_SYSTEM_NAME MATCHES "Windows")
  target_compile_options(my_app PRIVATE /W4)  # Windows 平台开启更严格的警告
elseif (CMAKE_SYSTEM_NAME MATCHES "Linux")
  target_compile_options(my_app PRIVATE -Wall -Wextra) # Linux 平台开启所有警告
endif()

使用generator expression改写:

add_executable(my_app main.cpp)

target_compile_options(my_app PRIVATE
  "$<$<PLATFORM_ID:Windows>:/W4>"
  "$<$<PLATFORM_ID:Linux>:-Wall -Wextra>"
)

这个代码更简洁,也更易读。

2. 信息表达式

这类表达式用于获取目标、源文件或构建系统的属性,常见的有:

  • $<TARGET_FILE:target>:目标 target 的完整路径。
  • $<TARGET_NAME:target>:目标 target 的名字。
  • $<TARGET_PROPERTY:target,property>:目标 target 的属性 property 的值。
  • $<SOURCE_FILE:source>:源文件 source 的完整路径。
  • $<CONFIG>:当前构建类型的名字(Debug、Release 等)。
  • $<PLATFORM_ID>: 平台ID (Windows, Linux, Darwin, etc.)
  • $<C_COMPILER_ID>: C 编译器ID (GNU, Clang, MSVC, AppleClang, etc.)
  • $<CXX_COMPILER_ID>: C++ 编译器ID (GNU, Clang, MSVC, AppleClang, etc.)

举个例子,假设我们想在编译时打印出目标文件的路径:

add_executable(my_app main.cpp)

add_custom_command(
  TARGET my_app
  POST_BUILD
  COMMAND ${CMAKE_COMMAND} -E echo "Target file: $<TARGET_FILE:my_app>"
)

这段代码会在每次构建 my_app 之后,打印出 my_app 可执行文件的完整路径。

再来一个例子,根据不同的编译器设置不同的编译选项:

add_executable(my_app main.cpp)

target_compile_options(my_app PRIVATE
  "$<$<CXX_COMPILER_ID:GNU>:-std=c++17>"   # GNU 编译器使用 C++17 标准
  "$<$<CXX_COMPILER_ID:Clang>:-std=c++17>" # Clang 编译器也使用 C++17 标准
  "$<$<CXX_COMPILER_ID:MSVC>:/std:c++17>"  # MSVC 编译器使用 C++17 标准
)

3. 输出表达式

这类表达式用于根据条件生成不同的字符串,最常用的就是 $<...>.

  • $<CONDITION:true_string,false_string>:如果 CONDITION 为真,则返回 true_string,否则返回 false_string
  • $<JOIN:list,string>:将列表 list 中的元素用字符串 string 连接起来。
  • $<TARGET_FILE_PREFIX:target>:目标 target 的文件名前缀(比如 lib)。
  • $<TARGET_FILE_SUFFIX:target>:目标 target 的文件后缀(比如 .exe.so)。

举个例子,假设我们想根据是否启用某个特性来添加不同的宏定义:

option(ENABLE_FEATURE "Enable the cool feature" OFF)

add_executable(my_app main.cpp)

target_compile_definitions(my_app PRIVATE
  "$<$<BOOL:${ENABLE_FEATURE}>:ENABLE_FEATURE>" # 如果 ENABLE_FEATURE 为真,则定义宏 ENABLE_FEATURE
)

这段代码的意思是:如果 ENABLE_FEATURE 选项被启用(在 CMake GUI 或命令行中设置为 ON),则会定义一个名为 ENABLE_FEATURE 的宏。

再来一个例子,将一个字符串列表连接成一个用分号分隔的字符串:

set(MY_LIST "a;b;c")

add_executable(my_app main.cpp)

target_compile_definitions(my_app PRIVATE
  "MY_STRING=$<JOIN:${MY_LIST},;>" # 定义宏 MY_STRING 的值为 "a;b;c"
)

Generator Expressions 的嵌套使用

Generator Expressions 可以嵌套使用,让你可以构建更复杂的逻辑。比如:

add_executable(my_app main.cpp)

target_compile_options(my_app PRIVATE
  "$<$<AND:$<CONFIG:Debug>,$<PLATFORM_ID:Windows>>:-DDEBUG_WINDOWS>" # Debug 模式且 Windows 平台下定义宏 DEBUG_WINDOWS
)

这段代码的意思是:只有在 Debug 模式且目标平台是 Windows 的情况下,才会定义一个名为 DEBUG_WINDOWS 的宏。

Generator Expressions 的注意事项

  • Generator Expressions 只能用在特定的 CMake 命令中,比如 target_compile_optionstarget_link_librariesadd_custom_command 等。
  • Generator Expressions 的计算发生在构建系统生成时,而不是 CMakeLists.txt 解析时。
  • Generator Expressions 的语法比较特殊,需要仔细阅读 CMake 的文档。

配置管理:多姿多彩的构建类型

CMake 默认支持 Debug 和 Release 两种构建类型,但有时候我们需要更多的构建类型,比如 RelWithDebInfo(Release 模式但包含调试信息)、MinSizeRel(Release 模式但优化代码大小)等等。

CMake 允许我们自定义构建类型,并通过 CMAKE_BUILD_TYPE 变量来控制当前使用的构建类型。

举个例子,假设我们想添加一个名为 Profile 的构建类型,用于性能分析:

  1. 设置构建类型: 在 CMakeLists.txt 中添加以下代码:

    set(CMAKE_BUILD_TYPE "Release" CACHE STRING
      "Choose the type of build, options are: Debug Release RelWithDebInfo MinSizeRel Profile."
      FORCE)
    set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS
      "Debug" "Release" "RelWithDebInfo" "MinSizeRel" "Profile")

    这段代码首先设置 CMAKE_BUILD_TYPE 变量的默认值为 "Release",并将其缓存起来,这样用户可以通过 CMake GUI 或命令行来修改这个值。

    然后,我们使用 set_property 命令来限制 CMAKE_BUILD_TYPE 变量的可选值,使其只能是 "Debug"、"Release"、"RelWithDebInfo"、"MinSizeRel" 和 "Profile" 中的一个。

  2. 添加构建类型相关的配置: 在 CMakeLists.txt 中添加以下代码:

    add_executable(my_app main.cpp)
    
    target_compile_options(my_app PRIVATE
      "$<$<CONFIG:Profile>:-O2 -pg>" # Profile 模式下开启性能分析
    )
    
    target_link_libraries(my_app PRIVATE
      "$<$<CONFIG:Profile>:-lprofiler>" # Profile 模式下链接性能分析库
    )

    这段代码的意思是:

    • 如果当前构建类型是 Profile,则使用 -O2 -pg 编译选项,开启性能分析。
    • 如果当前构建类型是 Profile,则链接 libprofiler 库,用于收集性能数据。
  3. 构建项目: 在命令行中执行以下命令:

    cmake -DCMAKE_BUILD_TYPE=Profile ..
    make

    或者,你也可以使用 CMake GUI 来选择 "Profile" 构建类型。

配置管理的进阶技巧

  • 使用 CMAKE_CXX_FLAGS_<CONFIG> 变量: CMake 提供了 CMAKE_CXX_FLAGS_<CONFIG> 变量,用于设置特定构建类型的编译选项。比如,CMAKE_CXX_FLAGS_Debug 用于设置 Debug 模式下的编译选项,CMAKE_CXX_FLAGS_Release 用于设置 Release 模式下的编译选项。

    set(CMAKE_CXX_FLAGS_DEBUG "-O0 -g") # Debug 模式下不优化,方便调试
    set(CMAKE_CXX_FLAGS_RELEASE "-O3") # Release 模式下最高级别优化
    set(CMAKE_CXX_FLAGS_PROFILE "-O2 -pg") # Profile 模式下开启性能分析
    
    add_executable(my_app main.cpp)
  • 使用 CMAKE_EXE_LINKER_FLAGS_<CONFIG>CMAKE_SHARED_LINKER_FLAGS_<CONFIG> 变量: 这两个变量分别用于设置可执行文件和共享库在特定构建类型下的链接选项。

    set(CMAKE_EXE_LINKER_FLAGS_DEBUG "-g") # Debug 模式下生成调试信息
    set(CMAKE_SHARED_LINKER_FLAGS_RELEASE "-s") # Release 模式下去除符号信息
    
    add_executable(my_app main.cpp)
    add_library(my_lib SHARED my_lib.cpp)
  • 使用 install 命令: install 命令用于将构建好的文件安装到指定目录。你可以根据不同的构建类型安装不同的文件。

    install(TARGETS my_app
      RUNTIME DESTINATION bin
      CONFIGURATIONS Release RelWithDebInfo Profile) # 只在 Release、RelWithDebInfo 和 Profile 模式下安装可执行文件

Generator Expressions 和配置管理:珠联璧合,天下无敌

Generator Expressions 和配置管理是 CMake 中非常强大的两个特性,它们可以一起使用,让你更好地控制项目的构建过程。

通过 Generator Expressions,你可以根据不同的构建类型、目标平台、编译器等条件来动态地调整编译选项、链接库等等。

通过配置管理,你可以自定义构建类型,并为每种构建类型设置不同的编译选项、链接选项和安装规则。

这两个特性结合起来,可以让你构建出更加灵活、可定制的项目。

实际案例:跨平台库的构建

假设我们正在开发一个跨平台的库,需要在 Windows、Linux 和 macOS 三个平台上构建。并且,我们希望在 Windows 平台上使用 MSVC 编译器,在 Linux 和 macOS 平台上使用 GCC 或 Clang 编译器。

cmake_minimum_required(VERSION 3.15)
project(MyLibrary)

# 设置 C++ 标准
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# 添加源文件
add_library(MyLibrary SHARED MyLibrary.cpp)

# 根据平台设置不同的编译选项
target_compile_options(MyLibrary PRIVATE
  "$<$<PLATFORM_ID:Windows>:/W4 /EHsc>" # Windows 平台开启更严格的警告和异常处理
  "$<$<PLATFORM_ID:Linux,Darwin>:-Wall -Wextra -fPIC>" # Linux 和 macOS 平台开启所有警告和位置无关代码
)

# 根据编译器设置不同的编译选项
target_compile_options(MyLibrary PRIVATE
  "$<$<CXX_COMPILER_ID:MSVC>:/D_WINDOWS>" # MSVC 编译器定义宏 _WINDOWS
  "$<$<CXX_COMPILER_ID:GNU,Clang>:-D_UNIX>" # GCC 和 Clang 编译器定义宏 _UNIX
)

# 根据构建类型设置不同的编译选项
target_compile_options(MyLibrary PRIVATE
  "$<$<CONFIG:Debug>:-g>" # Debug 模式下生成调试信息
  "$<$<CONFIG:Release>:-O3>" # Release 模式下最高级别优化
)

# 设置安装目录
set(CMAKE_INSTALL_PREFIX ${CMAKE_BINARY_DIR}/install)

# 安装库文件
install(TARGETS MyLibrary
  DESTINATION lib)

# 安装头文件
install(FILES MyLibrary.h
  DESTINATION include)

在这个例子中,我们使用了 Generator Expressions 来根据不同的平台、编译器和构建类型设置不同的编译选项。这样,我们就可以轻松地构建出适用于不同平台的库文件。

总结

Generator Expressions 和配置管理是 CMake 中非常重要的两个特性,它们可以让你更好地掌控项目的构建过程,构建出更加灵活、可定制的项目。

希望今天的讲解对你有所帮助。记住,熟能生巧,多写代码,多尝试,你也能成为 CMake 大师!
接下来咱们用表格来总结一下:

特性 描述 示例
Generator Expressions 在构建系统生成时才计算的表达式,用于根据不同的条件动态地调整编译选项、链接库等等。 target_compile_options(my_app PRIVATE "$<$<CONFIG:Debug>:-g>")
逻辑表达式 用于判断条件是否成立,返回 0 或 1。 $<BOOL:bool>, $<AND:op1,op2,...>, $<OR:op1,op2,...>, $<NOT:op>, $<STREQUAL:string1,string2>, $<TARGET_PROPERTY:target,property>, $<CONFIG:config>
信息表达式 用于获取目标、源文件或构建系统的属性。 $<TARGET_FILE:target>, $<TARGET_NAME:target>, $<TARGET_PROPERTY:target,property>, $<SOURCE_FILE:source>, $<CONFIG>, $PLATFORM_ID, $C_COMPILER_ID, $CXX_COMPILER_ID
输出表达式 用于根据条件生成不同的字符串。 $<CONDITION:true_string,false_string>, $<JOIN:list,string>, $<TARGET_FILE_PREFIX:target>, $<TARGET_FILE_SUFFIX:target>
配置管理 允许自定义构建类型,并通过 CMAKE_BUILD_TYPE 变量来控制当前使用的构建类型。 set(CMAKE_BUILD_TYPE "Release" CACHE STRING "Choose the type of build, options are: Debug Release RelWithDebInfo MinSizeRel Profile." FORCE), target_compile_options(my_app PRIVATE "$<$<CONFIG:Profile>:-O2 -pg>")
CMAKE_CXX_FLAGS_<CONFIG> 用于设置特定构建类型的编译选项。 set(CMAKE_CXX_FLAGS_DEBUG "-O0 -g")
CMAKE_EXE_LINKER_FLAGS_<CONFIG>CMAKE_SHARED_LINKER_FLAGS_<CONFIG> 分别用于设置可执行文件和共享库在特定构建类型下的链接选项。 set(CMAKE_EXE_LINKER_FLAGS_DEBUG "-g")
install 用于将构建好的文件安装到指定目录,可以根据不同的构建类型安装不同的文件。 install(TARGETS my_app RUNTIME DESTINATION bin CONFIGURATIONS Release RelWithDebInfo Profile)

希望这个表格能帮助你更好地理解 Generator Expressions 和配置管理。记住,实践是检验真理的唯一标准,多多练习,你就能掌握这些强大的工具,让你的 CMake 代码更加优雅和高效!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注