Conan + CMake + VS2022
[TOC]
概述
1. C++ 为什么如此“麻烦”?
Python、Java、C# 这类语言的跨平台性很大程度上得益于它们的虚拟机 (VM) 或解释器。这就像一个“同声传译”,在代码和不同操作系统之间做翻译。我们只需要写一份代码,这个“翻译官”会负责在 Windows、Linux、macOS 上用它们各自听得懂的方式去执行。我们关注的主要是代码逻辑。而 C++ 则完全不同。它被设计用来与硬件进行最直接的交互,追求极致的性能。因此,C++ 代码会被直接编译成特定目标平台的原生机器码 (.exe, .dll, .so 等)。
这就引出了一个核心问题:编译过程与环境高度相关。一个编译好的 C++ 程序,它的二进制文件就像一把“定制的钥匙”,这把钥匙的形状由以下多个因素共同决定:
- 操作系统 (OS):Windows 和 Linux 的可执行文件格式 (PE vs ELF)、系统调用完全不同。
- CPU 架构 (Architecture):为 x64 (64位) 编译的指令,x86 (32位) 的 CPU 看不懂。同理,ARM 架构 (如苹果 M 系列芯片、很多移动设备) 的指令也完全不同。
- 编译器 (Compiler):MSVC (Visual Studio 的编译器), GCC, Clang。即使是同样的代码,不同编译器生成的机器码也可能不同。
- 编译器版本 (Compiler Version):比如 Visual Studio 2022 和 2019 的编译器,它们对 C++ 标准的实现、内部数据结构、名称修饰 (Name Mangling) 规则可能有差异。
- 构建类型 (Build Type):Debug 和 Release。
- C++ 运行时库 (C++ Runtime Library):静态链接 (/MT) 还是动态链接 (/MD)。
2. 二进制不兼容 (The Binary Incompatibility Problem)
上面这些因素组合在一起,就构成了所谓的 ABI (Application Binary Interface),即应用程序二进制接口。只要这些因素中任何一个不匹配,二进制文件就可能不兼容,导致链接错误或运行时崩溃。
比如 Debug 和 Release 模式,它们是为不同目的而生的两种构建模式,因此二进制产物完全不兼容。
- Debug (调试) 模式:
- 目标: 方便程序员查找和修复 Bug。
- 特点:
- 不优化代码:代码执行逻辑和我们写的 C++ 代码高度一致,方便单步调试。
- 包含大量调试信息:将变量名、函数名、行号等信息嵌入到产物中,这样调试器才能告诉我们当前运行到哪一行,变量的值是多少。
- 包含额外的运行时检查:例如,STL 库会检查迭代器是否失效、数组是否越界等,一旦发现问题就会立即报错,帮助我们尽早发现问题。
- 结果: 程序体积大、运行速度慢。
- Release (发布) 模式:
- 目标: 追求极致的运行性能和最小的体积,交付给最终用户。
- 特点:
- 高度优化代码:编译器会进行函数内联、循环展开等各种操作,可能会打乱原始代码结构以换取速度。
- 移除调试信息:产物体积大大减小。
- 关闭所有运行时检查:为了性能,不再进行额外的安全检查。
- 结果: 程序体积小、运行速度快。
又比如不同版本的 VS 及其对应的编译器工具集:
- v142 是 Visual Studio 2019 的编译器工具集 (MSVC v14.2x)。
- v143 是 Visual Studio 2022 的编译器工具集 (MSVC v14.3x)。
它们之间通常也是二进制不兼容的。虽然微软努力在某些更新中保持兼容性,但最佳实践是:绝对不要混合使用由不同主版本工具集编译出的二进制文件。用 v143 编译的主程序去链接一个用 v142 编译的库,极有可能导致链接失败或难以察觉的运行时错误。
这就是“在我电脑上能跑,在别人电脑上跑不起来”的根源。因为我们们的编译器版本、构建类型、或者依赖库的编译环境可能存在细微的差异。
3. 解决方案:分而治之
为了解决这个“C++ 依赖地狱”,社区发展出了 Conan 和 CMake 这对“黄金搭档”。
CMake: 通用的蓝图
CMake 并不直接编译代码,它读取一个名为
CMakeLists.txt的“项目蓝图”文件。这个蓝图只描述了项目的逻辑结构:- 项目叫什么名字?
- 有哪些源文件?
- 需要链接哪些第三方库?
- 最终要生成一个可执行文件还是一个库?
然后,CMake 会根据我们当前的平台环境(例如“我要在 Windows 上用 VS 2022 编译”),将这份通用蓝图翻译成特定构建系统能看懂的“施工方案”(例如
MyProject.sln解决方案文件)。如果我们在 Linux 上,它就会生成 Makefiles。这使得我们的项目定义与特定平台解耦,实现了构建过程的跨平台。Conan: 通用的仓库管理员
如果说 CMake 解决了构建过程的跨平台问题,那么 Conan 就解决了依赖库的二进制兼容性问题。 Conan 是一个 C++ 包管理器,但它远比其他语言的包管理器要复杂和强大。它的核心是管理预编译好的二进制包。
它的工作流程是:
- 识别环境:Conan 会检查我们当前的完整环境,生成一个唯一的配置哈希(包含我们上面提到的所有因素:OS、架构、编译器、版本、构建类型等)。
- 查找匹配的二进制包:我们只需要在
conanfile.txt中声明我们需要spdlog/1.14.1。Conan 会带着我们的环境配置哈希,去远程仓库(如 ConanCenter)寻找一个完全匹配的、已经编译好的spdlog二进制包。 - 下载或本地编译:
- 如果找到了,它会直接下载这个即用型的二进制包。这非常快。
- 如果远程仓库没有为我们这个“奇特”的环境(比如一个非常新的编译器预览版)预编译的包,Conan 会自动下载
spdlog的源码,在我们的电脑上用我们当前的编译器和设置,现场为我们编译一份,然后存放在本地缓存中供项目使用。
通过这种方式,Conan 保证了我们的项目链接到的所有第三方库,都拥有和我们项目本身完全一致的 ABI,从根本上消除了二进制不兼容的问题。
4. 最终的集成:VS2022 + CMake + Conan
最后,Visual Studio 2022 扮演了“集大成者”的角色。它不再仅仅是一个 IDE,更是一个强大的集成开发环境。
- 原生 CMake 支持:我们不需要手动去生成
.sln文件再打开。我们直接“打开文件夹”,VS 就会自动识别CMakeLists.txt,在幕后调用 CMake,为我们管理好一切。 - CMakePresets 集成:VS 能识别 Conan 生成的
CMakePresets.json文件,将conan-debug和conan-release这样的配置直接显示在我们的UI界面上,让我们一键切换,无缝开发。
可以把整个开发过程想象成做一道复杂的菜:
- 我们 (The Chef):负责编写菜谱的核心步骤(我们的
main.cpp和业务逻辑代码)。 CMakeLists.txt(The Master Recipe Card):这是我们的主菜谱,它定义了这道菜叫什么,需要哪些主要步骤,以及需要用到哪些“半成品调料”(第三方库)。conanfile.txt(The Shopping List):这是我们的购物清单,上面写着我们需要“罗勒酱 v1.14.1”。- Conan (The Smart Shopping Assistant):我们把购物清单交给这个助手。他会查看我们的厨房配置(Windows, VS2022, Release模式),然后跑到全球最大的超市(ConanCenter),帮我们买回一瓶完全符合我们厨房标准的罗勒酱。如果超市没有,他甚至会买回新鲜罗勒叶和松子,用我们的设备现场给我们做一瓶!
- CMake (The Sous-Chef / Kitchen Manager):他拿到主菜谱和助手买回来的所有调料,为我们规划好整个厨房的工作流,生成一份详细的步骤分解图(
.sln文件)。 - Visual Studio (The High-Tech Kitchen):这是一个集成了所有工具的顶级厨房。我们在这里编写菜谱,通过一个按钮就能让助手去购物,让副厨规划流程,并最终启动全自动烹饪设备(编译器),做出完美的成品(
MyApp.exe)。
Conan + CMake + VS2022 C++ 项目开发示例
我们通过一个简单的例子来整理使用 Conan、CMake 和 Visual Studio 2022 构建 C++ 项目的典型开发流程。从一个简单的“Hello, World!”项目开始,然后学习如何添加第三方库和新的源文件来扩展项目功能。
准备工作
在开始之前,确保开发环境中已经安装了以下软件:
- Visual Studio 2022: 确保在安装时勾选了“使用 C++ 的桌面开发”工作负载,并包含了 CMake 工具。
- Conan: C/C++ 包管理器。
- Python: Conan 运行需要 Python 环境。
第一步:项目初始化
创建项目目录: 首先,为项目创建一个新的文件夹,例如
MyProject。创建
CMakeLists.txt文件: 在项目根目录下,创建一个名为CMakeLists.txt的文件。这是 CMake 的构建脚本文件。1
2
3
4
5
6
7cmake_minimum_required(VERSION 3.15)
project(MyProject LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
add_executable(MyApp src/main.cpp)这个简单的
CMakeLists.txt文件定义了项目的基本信息,并指定生成一个名为MyApp的可执行文件,其源文件为src/main.cpp。创建
conanfile.txt文件: 在项目根目录下,创建一个名为conanfile.txt的文件。这个文件用于定义项目的依赖项。1
2
3
4
5[requires]
[generators]
CMakeDeps
CMakeToolchain目前,我们的项目还没有任何依赖,所以
[requires]部分是空的。[generators]部分告诉 Conan 生成与 CMake 集成所需的文件。创建源文件: 在项目根目录下创建一个名为
src的文件夹,并在其中创建main.cpp文件。1
2
3
4
5
6
int main() {
std::cout << "Hello, World!" << std::endl;
return 0;
}安装依赖并生成构建文件: 打开命令行终端,进入项目根目录,然后运行以下命令:
1
conan install . --output-folder=build --build=missing
这个命令会读取
conanfile.txt,安装任何依赖(我们目前没有),并在build文件夹中生成 CMake 集成文件。在 Visual Studio 2022 中打开项目: 启动 Visual Studio 2022,选择“打开本地文件夹”,然后选择项目根目录
MyProject。Visual Studio 会自动检测到CMakeLists.txt并配置项目。构建并运行项目: 在 Visual Studio 的解决方案资源管理器中,能看到
CMakeLists.txt和源文件。在顶部菜单中,选择“生成” > “全部生成”来构建项目。构建成功后,可以在“调试”菜单中选择“开始执行(不调试)”来运行“Hello, World!”程序。
第二步:增加功能和第三方库
现在,为项目添加一个流行的日志库 spdlog 来增强其功能。
在
conanfile.txt中添加依赖: 打开conanfile.txt文件,并在[requires]部分添加spdlog:1
2
3
4
5
6[requires]
spdlog/1.14.1
[generators]
CMakeDeps
CMakeToolchain更新依赖: 返回命令行终端,进入项目根目录,然后再次运行
conan install命令来下载spdlog库并更新 CMake 集成文件:1
conan install . --output-folder=build --build=missing
修改
CMakeLists.txt以链接库: 打开CMakeLists.txt并进行以下修改,以查找并链接spdlog库:1
2
3
4
5
6
7
8
9
10
11
12
13cmake_minimum_required(VERSION 3.15)
project(MyProject LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# 查找 Conan 生成的包
find_package(spdlog CONFIG REQUIRED)
add_executable(MyApp src/main.cpp)
# 链接 spdlog 库
target_link_libraries(MyApp PRIVATE spdlog::spdlog)在代码中使用新库: 修改
src/main.cpp文件,使用spdlog来输出日志信息:1
2
3
4
5
6
7
8
9
10
int main() {
spdlog::info("Welcome to spdlog!");
spdlog::error("Some error message with arg: {}", 1);
std::cout << "Hello, World!" << std::endl;
return 0;
}重新构建并运行: 返回 Visual Studio,它可能会提示重新加载 CMake 项目。同意重新加载后,再次“全部生成”并运行项目。现在能在控制台看到
spdlog输出的日志信息。
第三步:增加新的源文件
随着项目变得越来越复杂,我们需要将代码组织到多个文件中。
创建新的类: 在
src文件夹下,创建两个新文件:Greeter.h和Greeter.cpp。Greeter.h:1
2
3
4
5
6
7
8
class Greeter {
public:
void greet(const std::string& name);
};Greeter.cpp:1
2
3
4
5
6
void Greeter::greet(const std::string& name) {
spdlog::info("Hello, {}!", name);
}更新
CMakeLists.txt: 将新的源文件Greeter.cpp添加到add_executable命令中:1
2
3
4
5
6
7
8# ... (前面的内容保持不变)
add_executable(MyApp
src/main.cpp
src/Greeter.cpp
)
target_link_libraries(MyApp PRIVATE spdlog::spdlog)在
main.cpp中使用新类: 修改src/main.cpp来使用Greeter类:1
2
3
4
5
6
7
8
9
10
11
12
13
int main() {
spdlog::info("Welcome to spdlog!");
Greeter greeter;
greeter.greet("Developer");
std::cout << "Hello, World!" << std::endl;
return 0;
}构建并运行: 最后,在 Visual Studio 中再次构建并运行项目。将看到
Greeter类通过spdlog输出的问候信息。
补充内容
Debug 和 Release
Debug 和 Release 两套编译模式,是非常不一样的,这可以体现在三个层级上。
首先,
最后,就是库代码本身可能就是不一样的。虽然我们只会编写一套统一的源代码,但是源代码可以通过宏的形式来实现在不同的编译模式下不同的编译表现。
Conan 1.x vs 2.x
Conan 1.x 的工作范式:与 CMakeLists.txt 紧密集成
在 Conan 1.x 中,典型的做法是在 CMakeLists.txt 文件中显式地调用 Conan 相关的函数来安装依赖和设置构建环境。这通常需要借助 conan.cmake 这个辅助文件。
工作流程简述:
创建
conanfile.txt或conanfile.py:定义项目所需的依赖。1
2
3
4
5
6# conanfile.txt
[requires]
fmt/9.1.0
[generators]
cmake在
CMakeLists.txt中嵌入 Conan:- 引入
conan.cmake文件(通常需要手动下载或通过 CMake 的FetchContent获取)。 - 调用
conan_cmake_run来执行conan install命令。 - 包含 Conan 生成的
conanbuildinfo.cmake文件,这个文件会设置很多变量,以便find_package能够找到依赖。 - 使用
find_package来查找你需要的库。
- 引入
示例 CMakeLists.txt (Conan 1.x):
1 | cmake_minimum_required(VERSION 3.15) |
在 Conan 1.x 的典型工作流中,我们不需要手动执行 conan install,整个工作流程是:
- 直接运行
cmake <source_dir>或在 IDE (如 Visual Studio) 中点击 "生成 CMake 缓存"。 - CMake 开始解析
CMakeLists.txt文件。 - 当它执行到
include(conan.cmake)和conan_cmake_run(...)这几行时,CMake 会暂停自己的工作。 conan_cmake_run函数会在后台调用conan install命令。- Conan 命令执行完毕,生成了
conanbuildinfo.cmake等文件。 - CMake 接着执行,包含(include)由 Conan 生成的文件,然后继续完成后续的配置过程。
整个流程的问题是依赖管理(Conan 的工作)和项目构建配置(CMake 的工作)被混合在了一起,导致耦合度很高。
Conan 2.x 的工作范式:与 CMakeLists.txt 解耦
Conan 2.x 推广了更为现代和模块化的集成方式,其核心思想是让 Conan 在构建系统(CMake)外部工作,为构建系统准备好所有需要的信息,而构建系统本身则保持原生、简洁。 这主要通过 CMakeToolchain 和 CMakeDeps 这两个生成器来实现。
工作流程简述:
创建
conanfile.txt或conanfile.py:定义依赖和新的生成器。1
2
3
4
5
6
7
8
9
10# conanfile.txt
[requires]
fmt/9.1.0
[generators]
CMakeDeps
CMakeToolchain
[layout]
cmake_layout执行
conan install:这是一个独立于 CMake 的步骤,在配置项目之前手动执行。1
2
3# 这会在 build/Release (或 build/Debug) 目录下生成 conan_toolchain.cmake
# 和 fmt-config.cmake 等文件
conan install . --output-folder=build/Release --build=missing -s build_type=Release编写原生的
CMakeLists.txt:这个文件看起来就和一个不使用 Conan 的普通 CMake 项目一样。1
2
3
4
5
6
7
8
9
10
11
12cmake_minimum_required(VERSION 3.15)
project(MyProject CXX)
# 没有任何 conan 相关的代码!
add_executable(MyApp main.cpp)
# 直接使用 find_package,就像库是系统安装的一样
find_package(fmt REQUIRED)
# 链接库
target_link_libraries(MyApp PRIVATE fmt::fmt)配置和构建 CMake 项目:在调用
cmake命令时,通过-DCMAKE_TOOLCHAIN_FILE参数告诉 CMake 使用 Conan 生成的工具链文件。现代的 Conan 2.x 甚至不需要我们手动指定工具链文件了,而是 Conan 会自动生成两个1
2
3
4
5
6
7
8# 进入构建目录
cd build/Release
# 调用 cmake,并指定 conan 生成的工具链文件
cmake ../.. -DCMAKE_TOOLCHAIN_FILE=conan_toolchain.cmake -DCMAKE_BUILD_TYPE=Release
# 构建项目
cmake --build .Presets.json文件:build/CMakePresets.json(核心预设文件):这是由 Conan 生成的主要预设文件。它包含了所有实际的配置信息,比如指定哪个generator(Ninja, Visual Studio, etc.),定义CMAKE_BUILD_TYPE是 Debug 还是 Release,以及最重要的——通过toolchainFile变量指向同在build目录下的conan_toolchain.cmake文件。它被放置在build目录中,因为它本身就是构建过程的一部分,并且它引用的其他文件(如工具链文件)也在同一个目录下,这使得路径管理非常简单和健壮。这个文件可以被认为是“构建产物”之一。CMakeUserPresets.json(根目录的“引导”文件):现代 IDE (Visual Studio, VS Code, CLion) 和 CMake 命令行工具在打开一个项目时,会首先在项目的根目录寻找CMakePresets.json或CMakeUserPresets.json。Conan 在根目录生成这个文件,正是为了被这些工具发现。它可以进行一些配置,并默认告诉 CMake 工具:“真正的预设配置不在这里,请到build/CMakePresets.json文件里去找。” 这样就成功地将 IDE/工具链的视线引导到了构建目录中。所以使用 Conan 2.x 的工作流程大概就是:
- 运行
conan install,它在build目录生成了所有构建所需的文件,包括conan_toolchain.cmake和build/CMakePresets.json。 - 同时,为了方便,它在项目根目录创建或更新了
CMakeUserPresets.json。 - 使用 Visual Studio 打开项目根文件夹。
- Visual Studio 发现了根目录的
CMakeUserPresets.json。 - 它解析该文件,看到了
include指令,于是接着去加载build/CMakePresets.json里的内容。 - 所有在
build/CMakePresets.json中定义的配置预设(presets)都成功加载到了 Visual Studio 的界面中。 - 选择一个预设进行构建,Visual Studio 会根据该预设的定义(包括使用正确的工具链文件),完美地完成配置和构建。
Conan 2.x 工作范式的特点是:
- 非侵入性:
CMakeLists.txt非常干净,完全不感知 Conan 的存在,专注于描述项目的构建逻辑。 这使得项目更容易被不使用 Conan 的人理解和构建(只要他们能手动满足依赖)。 - 职责分离:依赖管理(Conan)和项目构建(CMake)是两个独立的步骤,流程更清晰。
- 现代 CMake 实践:使用工具链文件 (
-DCMAKE_TOOLCHAIN_FILE) 是现代 CMake 推荐的用于交叉编译和配置构建环境的方式,Conan 2.x 的做法正符合这一最佳实践。
- 运行
CMakeDeps 和 CMakeToolchain
CMakeDeps 和 CMakeToolchain 是 Conan2 的两个核心的生成器(generators),它们协同工作,以现代且强大的方式将 Conan 管理的依赖项与 CMake构 建系统集成。
CMakeToolchain 会生成一个名为 conan_toolchain.cmake 的文件。 当通过 -DCMAKE_TOOLCHAIN_FILE 参数将其传递给CMake时,它会设置许多关键的CMake变量,包括:
- 编译器和工具链设置:根据 Conan profile 文件,设置
CMAKE_C_COMPILER、CMAKE_CXX_COMPILER等。 - 构建类型:设置
CMAKE_BUILD_TYPE(例如,Release或Debug)。 - C++标准:根据需要定义C++标准。
- Visual Studio运行时库:在Windows上,正确设置MSVC的运行时库(例如
MD或MT)。 - 其他平台特定设置:例如,处理macOS上的RPATHS。
通过使用CMakeToolchain,可以确保CMake在配置项目时,使用的构建环境与Conan管理依赖项时所用的环境完全一致,从而避免了许多常见的构建问题。
CMakeDeps 为conanfile.py中列出的每个依赖项生成对应的CMake配置文件。 这些文件通常是:
XXX-config.cmake或XXXConfig.cmake:这是现代CMake中find_package()在"Config"模式下查找包的标准方式。CMakeDeps会为每个依赖项创建一个目标(target),例如fmt::fmt,可以在target_link_libraries中直接使用。FindXXX.cmake:在某些情况下(例如,当依赖项的package_info中特别指定时),它也可以生成find模块文件。
重要的是,CMakeDeps是多配置的,这意味着它可以同时为Release和Debug等多种配置生成文件,这对于像Visual Studio这样的IDE非常有用。
CMakeDeps 和 CMakeToolchain 被设计为协同工作。 当使用 CMakeToolchain 时,它会自动设置 CMAKE_PREFIX_PATH 和 CMAKE_MODULE_PATH CMake变量,指向 CMakeDeps 生成文件的目录。
这就像一个两步过程: 1. CMakeDeps 将依赖项信息“翻译”成CMake能理解的 *-config.cmake 文件。 2. CMakeToolchain 告诉CMake去哪里“阅读”这些翻译好的文件。
如果不使用 CMakeToolchain,需要手动将 CMAKE_PREFIX_PATH 指向 CMakeDeps 的输出目录,这会比较繁琐且容易出错。因此,将它们一起使用是Conan官方推荐的最佳实践。
cmake-gui 中的 "Configure" 和 "Generate" 是 CMake 构建过程中的两个独立步骤,它们有明确的区别。在 Visual Studio 中,这两个步骤通常是自动连续执行的。
cmake-gui 中 Configure 和 Generate
在 cmake-gui 中,"Configure" 和 "Generate" 是两个独立的按钮,代表了 CMake 构建系统生成过程中的两个主要阶段:
- Configure (配置):
- 读取和解析
CMakeLists.txt文件:此步骤会读取项目中的CMakeLists.txt文件,并根据其中的指令解析项目结构、依赖项和编译选项。 - 环境侦测:CMake 会检查系统环境,寻找指定的编译器、库文件和头文件。
- 创建内部数据结构:CMake 会在内存中创建一个项目的内部表示。
- 填充缓存 (Cache):这个阶段会生成一个
CMakeCache.txt文件,其中包含了项目中的各种变量和选项(例如,库的路径、编译开关等)。 在cmake-gui界面上,这些变量会显示出来,通常新出现的或者有变化的变量会以红色高亮显示,允许用户查看和修改。 - 重复配置:可能需要多次点击 "Configure" 按钮,直到没有红色高亮条目,这表示所有必需的变量都已设置并且没有错误。
- 读取和解析
- Generate (生成):
- 生成原生构建文件:此步骤会使用 "Configure" 阶段生成的内部数据结构和最终确定的缓存变量,来创建所选择的构建系统(即 "Generator")的原生项目文件。
- 例如:如果选择的生成器是 "Visual Studio 17 2022",点击 "Generate" 后,CMake 会在指定的构建目录下创建
.sln和.vcxproj等 Visual Studio 解决方案和项目文件。
在 Visual Studio 中打开一个包含 CMakeLists.txt 文件的项目时,它会自动执行 "Configure" 和 "Generate" 这两个步骤。
- 当打开一个 CMake 项目文件夹时,Visual Studio 会自动运行 CMake 来配置项目。 这个过程相当于执行了 "Configure" 步骤,以便理解项目结构,为 IntelliSense 和其他语言服务提供支持。
- 这个配置步骤会生成 CMake 缓存。 随后,Visual Studio 会使用这些信息来生成其内部可用的项目结构,这本质上完成了 "Generate" 的工作。
- 当在 Visual Studio 中选择构建(Build)时,它实际上是在执行构建步骤(例如调用
cmake --build),而不是配置或生成步骤。
Debug、Release、以及项目构建
在