50、现代C++工程实践:使用CMake与Conan管理项目,构建现代C++项目。
聊到现代C++工程,很多人第一反应是“语法新特性”。但说实话,一个项目能不能跑得稳、维护得久,往往不取决于你用了多少C++17/20的特性,而在于你的工程基础设施搭得怎么样。
我个人习惯,拿到一个新项目,第一件事不是写代码,而是先把CMake和Conan的架子搭好。为什么?因为依赖管理和构建系统,是C++项目最容易被忽视的“暗坑”。
为什么是CMake + Conan?
先说说CMake。它已经是C++生态的事实标准了。你想想看,从嵌入式到高性能计算,从游戏引擎到金融交易系统,几乎都在用CMake。它的跨平台能力、生成器抽象、以及现代CMake的“目标导向”设计,让项目结构变得清晰很多。
Conan呢?它是C/C++的包管理器。以前我们怎么处理第三方库?要么手动下载编译,要么用vcpkg。Conan的好处是:声明式依赖管理。你只需要在conanfile.txt里写清楚要什么库、什么版本,Conan会自动下载、编译、链接。
我在项目中遇到过最头疼的事:团队里有人用Boost 1.72,有人用1.78,结果链接阶段各种符号冲突。后来统一用Conan管理版本,再也没出过这种问题。
项目结构设计
一个典型的现代C++项目,我建议这样组织目录:
my_project/
├── CMakeLists.txt # 顶层CMake配置
├── conanfile.txt # Conan依赖声明
├── src/ # 源代码
│ ├── CMakeLists.txt
│ ├── main.cpp
│ └── core/
│ ├── CMakeLists.txt
│ ├── engine.cpp
│ └── engine.h
├── include/ # 公共头文件
│ └── my_project/
│ └── api.h
├── tests/ # 单元测试
│ ├── CMakeLists.txt
│ └── test_engine.cpp
├── third_party/ # 手动管理的第三方库(极少用)
├── cmake/ # 自定义CMake模块
│ └── FindSomething.cmake
└── build/ # 构建目录(不提交到版本控制)
嗯,这里要注意:不要把生成的文件混进源码目录。我见过太多人直接在源码根目录下mkdir build && cd build && cmake ..,结果一不小心rm -rf把源码也删了。建议把build/加到.gitignore里。
CMakeLists.txt 实战
现代CMake(3.16+)推荐使用目标导向的方式。说白了,就是每个库或可执行文件都是一个“目标”,你只需要声明它的源文件、头文件路径、链接库,CMake会自动处理传递依赖。
来看一个实际例子。顶层CMakeLists.txt:
cmake_minimum_required(VERSION 3.16)
project(MyProject VERSION 1.0.0 LANGUAGES CXX)
# 设置C++标准
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
# 开启测试
enable_testing()
# 添加子目录
add_subdirectory(src)
add_subdirectory(tests)
然后src/CMakeLists.txt:
# 创建一个库目标
add_library(core
core/engine.cpp
core/engine.h
)
# 指定头文件路径
target_include_directories(core
PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/core
)
# 链接Conan提供的依赖
target_link_libraries(core
PUBLIC
fmt::fmt
spdlog::spdlog
)
# 创建可执行文件
add_executable(my_app main.cpp)
target_link_libraries(my_app PRIVATE core)
你看,这样写的好处是:core库知道自己需要fmt和spdlog,任何链接core的目标都会自动获得这些依赖。这就是“传递依赖”的魅力。
PRIVATE、PUBLIC、INTERFACE来精确控制依赖的可见性。比如,如果core只在内部使用spdlog,对外不暴露,就用PRIVATE。
Conan 依赖管理
接下来是Conan。创建一个conanfile.txt:
[requires]
fmt/10.1.1
spdlog/1.12.0
nlohmann_json/3.11.2
[generators]
CMakeDeps
CMakeToolchain
[options]
fmt:shared=False
spdlog:shared=False
这里我用了CMakeDeps和CMakeToolchain两个生成器。前者会生成CMake的FindXXX.cmake文件,后者会生成工具链配置。然后构建时:
# 安装依赖
conan install . --output-folder=build --build=missing
# 配置CMake
cd build
cmake .. -DCMAKE_TOOLCHAIN_FILE=conan_toolchain.cmake
# 编译
cmake --build .
我曾经踩过一个坑:Conan默认会从远程仓库下载预编译的二进制包。但如果你的平台比较特殊(比如ARM交叉编译),预编译包可能不存在。这时候加上--build=missing,Conan会自动从源码编译。嗯,第一次编译可能会慢一些,但之后会缓存起来。
SVG:现代C++工程构建流程
下面这张图,展示了从源码到可执行文件的完整流程:
这个流程其实很清晰:源码 + CMake配置 + Conan依赖 → Conan安装依赖 → CMake配置 → 编译 → 输出。每一步都有明确的输入输出,出了问题也容易定位。
避坑指南
这些年我用CMake+Conan踩过不少坑,挑几个典型的说说:
- CMake版本不一致:团队里有人用3.10,有人用3.20。现代CMake的很多特性(如
target_precompile_headers)在旧版本里没有。建议在CMakeLists.txt里明确指定最低版本,并在CI中统一CMake版本。 - Conan的profile配置:不同平台、不同编译器需要不同的profile。我曾经在Windows上用MSVC编译的库,链接到Linux的GCC项目里,结果符号表对不上。后来统一用
conan profile detect生成默认profile,再根据项目需要微调。 - 不要把Conan生成的目录提交到Git:
build/、conaninfo.txt、conanbuildinfo.cmake这些都是生成文件,必须加到.gitignore里。否则每次提交都会产生大量无意义的diff。 - 静态库 vs 动态库:Conan默认可能下载动态库,但有些场景(比如嵌入式)需要静态链接。在
conanfile.txt里用[options]明确指定shared=False。
--build=missing让Conan从源码编译,或者自己写一个本地的recipe覆盖。
测试与持续集成
现代C++项目离不开测试。CMake原生支持CTest,配合Conan安装的测试框架(如Google Test、Catch2),可以很方便地集成。
在tests/CMakeLists.txt里:
find_package(GTest REQUIRED)
add_executable(test_engine test_engine.cpp)
target_link_libraries(test_engine
PRIVATE
core
GTest::GTest
GTest::Main
)
add_test(NAME EngineTest COMMAND test_engine)
然后在CI脚本里:
conan install . --output-folder=build --build=missing
cd build
cmake .. -DCMAKE_TOOLCHAIN_FILE=conan_toolchain.cmake
cmake --build .
ctest --output-on-failure
我个人习惯在CI里加上--output-on-failure,这样测试失败时能直接看到日志,不用再手动跑一遍。
总结
CMake + Conan这套组合,说白了就是给C++项目装上了“现代工程”的翅膀。它解决了三个核心问题:跨平台构建、依赖管理、可重复性。你想想看,一个新人加入团队,只需要git clone、conan install、cmake --build三步就能开始开发,这体验比十年前手动配置环境不知道好到哪里去了。
当然,工具只是工具。真正让项目成功的,是团队对工程规范的坚持。嗯,希望这篇文章能帮你少走一些弯路。