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管理版本,再也没出过这种问题。

核心思想:CMake负责“怎么构建”,Conan负责“用什么构建”。两者配合,形成一个完整的现代C++工程流水线。

项目结构设计

一个典型的现代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库知道自己需要fmtspdlog,任何链接core的目标都会自动获得这些依赖。这就是“传递依赖”的魅力。

小技巧:PRIVATEPUBLICINTERFACE来精确控制依赖的可见性。比如,如果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

这里我用了CMakeDepsCMakeToolchain两个生成器。前者会生成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++工程构建流程

下面这张图,展示了从源码到可执行文件的完整流程:

现代C++工程构建流程 源代码 src/*.cpp, include/*.h CMakeLists.txt 构建规则配置 conanfile.txt 依赖声明 Conan Install 下载/编译依赖库 CMake Configure 生成构建系统(Makefile/Ninja) CMake Build 编译、链接、生成可执行文件 可执行文件 my_app

这个流程其实很清晰:源码 + 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生成的目录提交到Gitbuild/conaninfo.txtconanbuildinfo.cmake这些都是生成文件,必须加到.gitignore里。否则每次提交都会产生大量无意义的diff。
  • 静态库 vs 动态库:Conan默认可能下载动态库,但有些场景(比如嵌入式)需要静态链接。在conanfile.txt里用[options]明确指定shared=False
重要提醒:Conan的远程仓库(ConanCenter)里的包质量参差不齐。有些包的recipe写得不好,可能编译不过。遇到这种情况,可以尝试指定--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 cloneconan installcmake --build三步就能开始开发,这体验比十年前手动配置环境不知道好到哪里去了。

当然,工具只是工具。真正让项目成功的,是团队对工程规范的坚持。嗯,希望这篇文章能帮你少走一些弯路。


专注资料整理