18、CMake子目录与嵌套项目:add_subdirectory、子项目隔离、target_scope

大型项目怎么组织?

我见过不少团队,把所有源文件塞进一个目录,CMakeLists.txt 写到上千行。结果呢?编译慢、耦合高、新人看不懂。说白了,这就是「大泥球」架构。

CMake 给了我们一套优雅的解决方案——子目录与嵌套项目。今天我就带你把它吃透。

18.1 为什么需要子目录?

你想想看,一个项目如果包含:

  • 核心算法库
  • 网络通信模块
  • UI 界面
  • 单元测试
  • 第三方依赖

全写在一个 CMakeLists.txt 里,维护成本会爆炸。我个人习惯是:每个模块一个子目录,每个子目录有自己的 CMakeLists.txt。这样职责清晰,也方便复用。

核心原则:每个子目录只做一件事,做好一件事。

18.2 add_subdirectory 的基本用法

这是最常用的命令。语法很简单:

add_subdirectory(source_dir [binary_dir] [EXCLUDE_FROM_ALL])

参数说明:

参数 含义 是否必填
source_dir 子目录路径(相对于当前 CMakeLists.txt)
binary_dir 构建输出目录(默认与 source_dir 同名)
EXCLUDE_FROM_ALL 排除默认构建目标

举个例子:

# 顶层 CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
project(MyApp)

add_subdirectory(src/core)
add_subdirectory(src/network)
add_subdirectory(src/ui)
add_subdirectory(tests EXCLUDE_FROM_ALL)

每个子目录里再写自己的 CMakeLists.txt:

# src/core/CMakeLists.txt
add_library(core
  algorithm.cpp
  data_structures.cpp
)

target_include_directories(core PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})

小技巧:我习惯在 binary_dir 参数里用 ${CMAKE_CURRENT_BINARY_DIR}/子目录名,这样构建产物不会乱飞。

18.3 子项目隔离:别让依赖乱串

这是很多新手会踩的坑。

默认情况下,add_subdirectory 会把子目录里的所有目标暴露给父目录。这意味着:

  • 子目录 A 可以链接子目录 B 的库
  • 父目录可以直接引用子目录的头文件
  • 所有目标都在同一个「命名空间」里

听起来方便?但我在项目中遇到过这样的问题:两个子目录定义了同名的宏,结果编译时冲突了。排查了整整半天。

避坑指南:我曾经因为子目录隔离没做好,导致一个静态库被重复链接了三次。链接器报了一堆 duplicate symbol 错误。后来我养成了习惯——每个子目录明确声明自己的 PUBLIC/PRIVATE/INTERFACE 范围。

隔离的最佳实践:

  1. 使用 target_include_directories 的 PRIVATE 关键字——内部头文件不对外暴露
  2. 使用 target_link_libraries 的 PRIVATE 关键字——内部依赖不传递
  3. 考虑使用 OBJECT 库——编译但不链接,由父目录统一处理
# src/network/CMakeLists.txt
add_library(network
  tcp_client.cpp
  http_parser.cpp
)

# 内部使用的头文件,不对外暴露
target_include_directories(network PRIVATE
  ${CMAKE_CURRENT_SOURCE_DIR}/internal
)

# 对外公开的头文件
target_include_directories(network PUBLIC
  ${CMAKE_CURRENT_SOURCE_DIR}/include
)

# 内部依赖,不传递出去
target_link_libraries(network PRIVATE
  core
)

18.4 target_scope:控制可见性的三把锁

CMake 3.0 之后引入了 target_scope 的概念。说白了就是三个关键字:

关键字 对当前目标 对链接者 对链接者的链接者
PRIVATE ✅ 生效 ❌ 不生效 ❌ 不生效
PUBLIC ✅ 生效 ✅ 生效 ❌ 不生效
INTERFACE ❌ 不生效 ✅ 生效 ❌ 不生效

嗯,这里要注意:INTERFACE 比较特殊。它只对使用者生效,对自身不生效。常用于纯头文件库或者接口定义。

# 纯头文件库,用 INTERFACE
add_library(interface_lib INTERFACE)
target_include_directories(interface_lib INTERFACE
  ${CMAKE_CURRENT_SOURCE_DIR}/include
)

# 使用方
target_link_libraries(my_app PRIVATE interface_lib)
# 这样 my_app 就能找到 interface_lib 的头文件了

我的经验:PUBLIC 要慎用。我见过一个项目,所有依赖都用 PUBLIC,结果编译时传递了整整五层依赖。每次改一个底层库,上层全部要重新编译。后来我改成:能用 PRIVATE 绝不用 PUBLIC,能用 INTERFACE 绝不用 PRIVATE。

18.5 嵌套项目的实战结构

来看一个真实项目的目录布局:

my_project/
├── CMakeLists.txt          # 顶层
├── src/
│   ├── CMakeLists.txt      # 聚合子目录
│   ├── core/
│   │   ├── CMakeLists.txt
│   │   ├── include/
│   │   └── src/
│   ├── network/
│   │   ├── CMakeLists.txt
│   │   └── src/
│   └── ui/
│       ├── CMakeLists.txt
│       └── src/
├── third_party/
│   ├── CMakeLists.txt
│   ├── json/
│   └── log/
└── tests/
    ├── CMakeLists.txt
    └── test_core.cpp

顶层 CMakeLists.txt 这样写:

cmake_minimum_required(VERSION 3.16)
project(MyProject VERSION 1.0.0 LANGUAGES CXX)

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

# 添加子目录
add_subdirectory(src)
add_subdirectory(third_party)
add_subdirectory(tests EXCLUDE_FROM_ALL)

src/CMakeLists.txt 作为中间层:

# 只做聚合,不定义目标
add_subdirectory(core)
add_subdirectory(network)
add_subdirectory(ui)

这样设计的好处:

  • 顶层只关心「有哪些模块」
  • 中间层只做「模块聚合」
  • 底层只关心「模块内部实现」

建议:每个子目录的 CMakeLists.txt 控制在 50 行以内。如果超过,说明这个模块太复杂了,该拆分了。

18.6 知识体系图

下面这张图展示了本章的核心逻辑:

CMake 子目录与嵌套项目知识体系 add_subdirectory 子目录结构设计 子项目隔离 target_scope 模块化拆分 职责单一 避免依赖泄露 防止符号冲突 PRIVATE PUBLIC / INTERFACE 核心原则:高内聚、低耦合、控制可见性

18.7 常见问题与避坑

Q:子目录里的 CMakeLists.txt 可以定义 project() 吗?

可以,但不建议。我试过在子目录里定义 project(),结果版本号、语言设置全乱了。除非你确实需要独立的项目上下文,否则别这么干。

Q:EXCLUDE_FROM_ALL 有什么用?

测试目录、示例代码、文档生成这些非核心目标,用 EXCLUDE_FROM_ALL 排除掉。这样默认构建时不会编译它们,节省时间。需要时手动指定目标名即可。

Q:子目录之间怎么共享变量?

用 set(... CACHE INTERNAL "") 或者使用目录属性。我个人更推荐用 target 属性来传递,而不是全局变量。全局变量多了,你都不知道哪里改了什么。

又一个坑:我曾经在子目录里用 set(CMAKE_CXX_FLAGS ...) 修改了全局编译选项,结果影响了其他子目录。后来我改用 target_compile_options,只对当前目标生效。记住:能用 target_ 开头的命令,就别用全局的。

好了,子目录与嵌套项目的内容就这些。记住三个关键词:add_subdirectory 组织结构、子项目隔离防耦合、target_scope 控可见性。把这三点用好,你的项目结构会清晰很多。

公众号:蓝海资料掘金营,微信 deep3321