使用C++的单一代码库:代码组织与最佳实践
大型C++项目的开发常常面临代码碎片化的问题:库分散在不同的代码库中,版本不同步,集成变成令人头疼的事情。解决方案是单一代码库(monorepo)。这是一种将所有代码(包括库、工具和测试)存储在一个代码库中的方法。在本文中,我们将讨论如何正确组织C++项目的单一代码库,使用现代工具(CMake、Conan、Git)并避免常见错误。
1. 目录结构:秩序的基础
第一步是创建一个统一、清晰的文件夹结构。以下是C++单一代码库的推荐方案:
my-monorepo/├── cmake/ # 通用CMake模块├── libs/ # 库│ ├── core/ # 核心(日志、分配器)│ ├── math/ # 数学库│ └── network/ # 网络库├── apps/ # 可执行应用程序│ ├── server/ # 服务器应用程序│ └── client/ # 客户端应用程序├── tests/ # 集成测试├── scripts/ # 构建和部署脚本├── third_party/ # 外部依赖(子模块)├── CMakeLists.txt # 根CMake文件├── conanfile.txt # 依赖管理器(可选)└── .github/ # CI/CD(GitHub Actions)为什么这样有效?
- libs/ — 所有库都在一个地方,易于重用。
- apps/ — 依赖于libs的应用程序。
- third_party/ — 外部依赖(通过git submodules或CMake FetchContent)。
- cmake/ — 自定义函数(例如
add_library_with_tests)。
2. CMake:配置根项目
CMake是单一代码库中C++构建的标准。配置根CMakeLists.txt以包含所有子项目:
cmake_minimum_required(VERSION 3.20)project(MyMonorepo LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 20)set(CMAKE_CXX_STANDARD_REQUIRED ON)
# 通用选项option(BUILD_TESTS "Build tests" ON)
# 包含库add_subdirectory(libs/core)add_subdirectory(libs/math)add_subdirectory(libs/network)
# 包含应用程序add_subdirectory(apps/server)add_subdirectory(apps/client)
# 包含测试if(BUILD_TESTS) enable_testing() add_subdirectory(tests)endif()重要事项:
- 使用
target_link_libraries明确指定库之间的依赖关系。 - 对于外部依赖(例如Boost、fmt),使用
FetchContent或Conan。 - 避免全局
include_directories— 使用带有PUBLIC或PRIVATE的target_include_directories。
3. 管理单一代码库内的依赖关系
单一代码库的主要问题之一是循环依赖。解决方案:严格的层次结构。例如:
- core(不依赖任何东西,除了标准库)
- math(依赖于core)
- network(依赖于core和math)
- server(依赖于core、math、network)
库math的CMakeLists.txt示例:
add_library(math src/vector.cpp src/matrix.cpp include/math/vector.h include/math/matrix.h)
target_include_directories(math PUBLIC include PRIVATE src)
target_link_libraries(math PUBLIC core # 明确指定依赖关