4、目标与属性:target的概念、target属性管理、编译选项、链接库
好,我们进入第四章。这一章我打算聊聊 CMake 里最核心的概念——目标(target)。说实话,很多初学者把 CMake 当成一个高级的“变量赋值器”,写出来的 CMakeLists.txt 又臭又长。我个人觉得,没理解 target,就等于没入门 CMake 的模块化设计。
4.1 什么是 target?
target 说白了,就是 CMake 里的一个“构建单元”。它可以是一个可执行文件、一个静态库、一个动态库,甚至是一个自定义的命令。每个 target 都有自己的属性,比如编译选项、链接库、头文件路径等等。
我刚开始用 CMake 时,习惯把所有东西都塞到全局变量里。比如这样:
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -O2")
include_directories(${PROJECT_SOURCE_DIR}/include)
link_directories(${PROJECT_SOURCE_DIR}/lib)
嗯,这写法在小型项目里还能凑合。但项目一复杂,你就会发现——全局变量是魔鬼。你想想看,A 模块需要 -O2,B 模块需要 -O0 调试,全局设置直接让所有目标都“一刀切”。
target 的出现,就是为了解决这个问题。每个 target 独立管理自己的属性,互不干扰。这才是模块化设计的基石。
4.2 target 属性管理
CMake 提供了两个核心命令来管理 target 的属性:
set_target_properties:设置属性get_target_property:获取属性
先看一个简单的例子:
add_executable(my_app main.cpp)
set_target_properties(my_app PROPERTIES
CXX_STANDARD 17
CXX_STANDARD_REQUIRED ON
OUTPUT_NAME "my_custom_app_name"
)
get_target_property(app_name my_app OUTPUT_NAME)
message(STATUS "应用输出名称: ${app_name}")
这里我设置了三个属性:C++ 标准、是否强制要求该标准、输出文件名。然后通过 get_target_property 把属性读出来打印。你想想看,这种写法比全局设置清晰多了吧?
我在项目中遇到过一个问题:两个模块需要不同的 C++ 标准。一个用 C++17,一个用 C++11。如果用全局变量,根本没法搞。但用 target 属性,每个模块独立设置,完美解决。
常用 target 属性一览
| 属性名 | 说明 | 示例值 |
|---|---|---|
| CXX_STANDARD | C++ 标准版本 | 11, 14, 17, 20 |
| CXX_STANDARD_REQUIRED | 是否强制要求该标准 | ON / OFF |
| OUTPUT_NAME | 输出文件名称 | my_app |
| POSITION_INDEPENDENT_CODE | 是否生成位置无关代码 | ON / OFF |
| VISIBILITY_INLINES_HIDDEN | 内联函数是否隐藏符号 | ON / OFF |
小技巧:我个人习惯把 target 属性集中写在 add_executable 或 add_library 之后,不要分散在文件各处。这样别人看你的 CMakeLists.txt,一眼就能知道这个 target 的“性格”。
4.3 编译选项:target_compile_options
编译选项的管理,是 target 属性的一个典型应用场景。用 target_compile_options 可以精确控制每个 target 的编译标志。
基本语法:
target_compile_options(<target> <INTERFACE|PUBLIC|PRIVATE> <options>...)
这里有个关键点:访问修饰符。PRIVATE 表示只影响当前 target;PUBLIC 表示影响当前 target 和依赖它的 target;INTERFACE 表示只影响依赖它的 target,不影响自己。
举个例子:
add_library(my_lib STATIC my_lib.cpp)
# 只对 my_lib 生效
target_compile_options(my_lib PRIVATE -Wall -Wextra)
# 对 my_lib 和链接它的目标都生效
target_compile_options(my_lib PUBLIC -O2)
# 只对链接 my_lib 的目标生效,my_lib 自己不用
target_compile_options(my_lib INTERFACE -fPIC)
为什么会这样设计?你想想看,一个库可能内部需要某些编译选项,但对外暴露的接口又需要不同的选项。这种“内外有别”的设计,正是模块化思想的体现。
我曾经在一个大型项目中,因为全局设置了 -Werror,导致第三方库的警告直接编译失败。后来改用 target_compile_options 只对自有代码启用 -Werror,问题迎刃而解。
注意:target_compile_options 和 set_target_properties 里的 COMPILE_FLAGS 属性功能类似,但前者更推荐。因为 target_compile_options 支持生成器表达式,可以做到更精细的控制。
4.4 链接库:target_link_libraries
链接库的管理,是 target 的另一个核心能力。用 target_link_libraries 可以指定 target 依赖哪些库。
基本语法:
target_link_libraries(<target> <PRIVATE|PUBLIC|INTERFACE> <library>...)
同样有 PRIVATE、PUBLIC、INTERFACE 三个修饰符。它们的含义和编译选项完全一致:
- PRIVATE:只链接到当前 target
- PUBLIC:链接到当前 target,并且传递给依赖它的 target
- INTERFACE:不链接到当前 target,只传递给依赖它的 target
看个实际例子:
add_library(core core.cpp)
add_library(utils utils.cpp)
add_executable(my_app main.cpp)
# core 链接 utils,但外部不需要知道 utils 的存在
target_link_libraries(core PRIVATE utils)
# my_app 链接 core,同时也会继承 core 的 PUBLIC 依赖
target_link_libraries(my_app PRIVATE core)
这里有个设计原则:最小暴露原则。如果一个库的内部实现依赖了另一个库,那就用 PRIVATE。只有接口中暴露了依赖,才用 PUBLIC。
我记得有一次,团队里有人把所有依赖都写成 PUBLIC,结果导致一个简单的改动引发了连锁编译错误。嗯,这就是滥用 PUBLIC 的代价。
链接库的三种形式
| 形式 | 示例 | 说明 |
|---|---|---|
| 目标名称 | target_link_libraries(my_app PRIVATE my_lib) | 直接使用 CMake target 名称,最推荐 |
| 库文件路径 | target_link_libraries(my_app PRIVATE /path/to/libfoo.a) | 指定绝对或相对路径 |
| 查找模块 | target_link_libraries(my_app PRIVATE ${Boost_LIBRARIES}) | 通过 find_package 获取的变量 |
最佳实践:尽量使用目标名称来链接。因为 CMake 会自动处理目标之间的依赖关系、头文件路径、编译选项等。用路径或变量,这些“福利”就没了。
4.5 综合示例:一个模块化的项目结构
最后,我放一个完整的例子,把今天讲的内容串起来:
# 根 CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
project(MyProject VERSION 1.0.0 LANGUAGES CXX)
# 添加子目录
add_subdirectory(src/core)
add_subdirectory(src/app)
# src/core/CMakeLists.txt
add_library(core STATIC core.cpp)
set_target_properties(core PROPERTIES
CXX_STANDARD 17
CXX_STANDARD_REQUIRED ON
POSITION_INDEPENDENT_CODE ON
)
target_compile_options(core PRIVATE -Wall -Wextra -Wpedantic)
target_include_directories(core PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include)
# src/app/CMakeLists.txt
add_executable(my_app main.cpp)
target_link_libraries(my_app PRIVATE core)
set_target_properties(my_app PROPERTIES
CXX_STANDARD 17
CXX_STANDARD_REQUIRED ON
OUTPUT_NAME "my_app_v${PROJECT_VERSION}"
)
你看,每个 target 都独立管理自己的属性、编译选项和链接库。模块之间通过 PUBLIC/PRIVATE 控制依赖的传递。这种结构,项目再大也能保持清晰。
嗯,这一章就到这里。下一章我们会深入 target 的另一个重要特性——生成器表达式。这东西能让你的 CMake 代码变得极其灵活,敬请期待。