CppCon 2022 学习:import CMake CMake and C++20 Modules
CMake 工作流程 (CMake Workflow)
运行 CMake 的方式
- cmake-gui:Qt 图形界面
- ccmake:终端交互界面
- cmake:非交互式命令行
CMake 执行流程
- 读取缓存文件
- 如果存在
CMakeCache.txt,CMake 会读取用户上一次的配置
- 如果存在
- 用户编辑缓存(可选)
- 用户可以修改一些变量
- CMake 配置 (configure)
- 根据
CMakeLists.txt文件和用户设置,检查依赖、生成配置 - 如果配置不完整或需要修改,用户可以继续编辑
- 根据
- CMake 生成 (generate)
- 用户确认后,CMake 会生成 本机平台的构建系统文件(Makefile、Ninja 文件、Visual Studio 工程等)
流程示意:
- 用户确认后,CMake 会生成 本机平台的构建系统文件(Makefile、Ninja 文件、Visual Studio 工程等)
CMake reads CMakeCache.txt ---> User edits? ---> configure ---> generate
|
└--> CMake reads CMakeLists.txt
“Usage Requirements” / 现代 CMake (Modern CMake)
- 现代风格:以 target 为中心
target_include_directories(example PUBLIC "inc")
- 含义:
example目标自身以及链接到它的目标都会使用-Iinc- 每个 target 应该 完整描述如何使用它
- 内部和外部 target 的使用方式一致
目标依赖与使用范围
现代 CMake 用三种关键关键词控制依赖和可见性:
| 关键词 | 含义 | 使用场景 |
|---|---|---|
| PRIVATE | 只有本 target 使用 | 内部实现依赖 |
| INTERFACE | 只有依赖本 target 的目标使用 | 提供给外部使用的接口 |
| PUBLIC | 本 target 和依赖它的目标都使用 | 兼具 PRIVATE + INTERFACE 功能 |
特殊生成器表达式
$<BUILD_INTERFACE>:仅在构建目录使用$<INSTALL_INTERFACE>:仅在安装后的目标使用
示意结构
Root Directory
├── Executable
│ ├── Library A
│ └── Library B
├── Library A
└── Library B
- 每个库或可执行文件应 明确声明使用的 include、link 和依赖关系
- 通过 PUBLIC / PRIVATE / INTERFACE 来控制哪些 target 可见
总结理解: - 现代 CMake 核心思想:每个 target 自描述自己的依赖和使用方式
target_include_directories等命令替代了全局include_directories- PRIVATE / INTERFACE / PUBLIC 控制 编译可见性和依赖传播
$<BUILD_INTERFACE>/$<INSTALL_INTERFACE>控制 构建时 vs 安装时 的路径
Usage Requirements / target_link_libraries 示例 + Unity/Jumbo Builds 内容用整理理解:
Usage Requirements / target_link_libraries 示例
示例 1:PUBLIC 链接
target_link_libraries(trunk PUBLIC root)
target_link_libraries(leaf PUBLIC trunk)
- 含义:
trunk依赖root,并且 trunk 的依赖也会传递给依赖 trunk 的目标leaf依赖trunk,因为 trunk 是 PUBLIC,所以 leaf 自动也能访问 root
- 生成的命令:
/usr/bin/c++ -fPIC -shared -Wl,-soname,libleaf.so \
-o libleaf.so leaf.cxx.o libtrunk.so libroot.so
- 注意:编译 leaf 时,会同时链接 trunk 和 root
示例 2:PRIVATE 链接
target_link_libraries(trunk PRIVATE root)
target_link_libraries(leaf PUBLIC trunk)
- 含义:
trunk依赖root,但 这个依赖不会传递给依赖 trunk 的目标leaf依赖trunk,但不会看到 root
- 生成的命令:
/usr/bin/c++ -fPIC -shared -Wl,-soname,libleaf.so \
-o libleaf.so leaf.cxx.o libtrunk.so
- 注意:编译 leaf 时,只链接 trunk,不链接 root
总结: - PUBLIC:依赖会传递
- PRIVATE:依赖只在自身 target 可见,不传递
Unity / Jumbo Builds
- 概念:
- 将多个源文件合并成一个编译单元,以 减少编译开销
- 控制合并大小可以用:
CMAKE_UNITY_BUILD_BATCH_SIZE
- 注意:
- 对于 C++ 模块 (C++20 Modules),这种做法可能效果有限
- 优点:减少编译时间
- 缺点:可能增加单个编译单元的内存占用或调试难度
理解要点:
target_link_libraries的 PUBLIC/PRIVATE 决定 依赖传播- Unity / Jumbo Builds 是 编译优化手段,通过合并源文件减少总编译次数
- 对现代 CMake 项目,合理使用 PUBLIC/PRIVATE/INTERFACE + Unity Build 可以 优化依赖管理和编译速度
一个完整的 CMake + C++ 代码例子,演示 PUBLIC vs PRIVATE target_link_libraries 的区别。
目录结构
CMakeExample/
├── CMakeLists.txt
├── root/
│ ├── CMakeLists.txt
│ └── root.cpp
├── trunk/
│ ├── CMakeLists.txt
│ └── trunk.cpp
└── leaf/
├── CMakeLists.txt
└── leaf.cpp
root/root.cpp
#include <iostream>
void root_function() {
std::cout << "Hello from root!" << std::endl;
}
trunk/trunk.cpp
#include <iostream>
#ifdef USE_ROOT
extern void root_function();
#endif
void trunk_function() {
std::cout << "Hello from trunk!" << std::endl;
#ifdef USE_ROOT
root_function();
#endif
}
leaf/leaf.cpp
#include <iostream>
extern void trunk_function();
int main() {
trunk_function();
std::cout << "Hello from leaf!" << std::endl;
return 0;
}
root/CMakeLists.txt
add_library(root STATIC root.cpp)
trunk/CMakeLists.txt
PUBLIC 依赖 root
add_library(trunk STATIC trunk.cpp)
target_link_libraries(trunk PUBLIC root)
target_compile_definitions(trunk PUBLIC USE_ROOT)
PRIVATE 依赖 root
# add_library(trunk STATIC trunk.cpp)
# target_link_libraries(trunk PRIVATE root)
# target_compile_definitions(trunk PRIVATE USE_ROOT)
注:切换 PUBLIC / PRIVATE 看效果差异
leaf/CMakeLists.txt
add_executable(leaf leaf.cpp)
target_link_libraries(leaf PUBLIC trunk)
顶层 CMakeLists.txt
cmake_minimum_required(VERSION 3.20)
project(CMakePublicPrivateExample)
add_subdirectory(root)
add_subdirectory(trunk)
add_subdirectory(leaf)
效果对比
1⃣ 使用 PUBLIC
cmake -B build
cmake --build build
./build/leaf/leaf
输出:
Hello from trunk!
Hello from root!
Hello from leaf!
- leaf 能访问 root,因为 trunk 对 root 是 PUBLIC
2⃣ 使用 PRIVATE
# 修改 trunk/CMakeLists.txt 为 PRIVATE
cmake -B build -U *
cmake --build build
./build/leaf/leaf
- 编译 leaf 时 会报错,因为 leaf 看不到 root
- leaf 只能访问 trunk 自己的内容
总结 - PUBLIC:依赖传递给使用它的目标
- PRIVATE:依赖只对自身可见,不传递
target_link_libraries(leaf PUBLIC trunk)
[proc] Executing command: /usr/bin/cmake --build /home/xiaqiu/test/build --config Debug --target all --
[build] [3/6 16% :: 0.381] Building CXX object CppCon/day416/code/root/CMakeFiles/root.dir/root.cpp.o
[build] [4/6 33% :: 0.381] Building CXX object CppCon/day416/code/leaf/CMakeFiles/leaf.dir/leaf.cpp.o
[build] [4/6 50% :: 0.383] Building CXX object CppCon/day416/code/trunk/CMakeFiles/trunk.dir/trunk.cpp.o
[build] [4/6 66% :: 0.712] Linking CXX static library CppCon/day416/code/root/libroot.a
[build] [5/6 83% :: 0.759] Linking CXX static library CppCon/day416/code/trunk/libtrunk.a
[build] [6/6 100% :: 0.989] Linking CXX executable CppCon/day416/code/leaf/leaf
[driver] Build completed: 00:00:01.007
没发现什么区别
CMake Presets 理解
概念
- Presets(预设) 是一种 保存 CMake 常用配置的机制
- 可以把项目中经常使用的配置(变量、构建目录、生成器等)保存到 JSON 文件中,方便重复使用和共享
文件类型
- CMakePresets.json
- 版本控制(可以放到 Git 等仓库)
- 用于 团队共享
- 保存项目通用的配置,例如:
- 常用构建类型(Debug / Release)
- 编译器选择
- 构建目录
- CMakeUserPresets.json
- 不版本控制(仅在本地使用)
- 用于 机器或用户特定的设置
- 示例用途:
- 本地特殊路径
- 私有编译器配置
- 用户偏好选项
优势
- 统一配置:团队成员使用相同 preset,避免每人手动配置
- 易于切换:快速在不同构建配置间切换(Debug / Release / Custom)
- 可维护性:减少手动重复输入命令或修改缓存
理解口诀: - CMakePresets.json = 共享 / 版本控制
- CMakeUserPresets.json = 本地 / 用户专属
CMake Presets JSON 示例
{
"version": 3,
"configurePresets": [
{
"name": "ninja-debug",
"generator": "Ninja",
"binaryDir": "${sourceDir}/build/debug",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug"
}
}
]
}
字段解释
| 字段 | 含义 |
|---|---|
version |
Preset 文件的版本号,这里是 3 |
configurePresets |
一个数组,存放所有配置预设(configure presets) |
name |
预设名称,用于引用,比如 "ninja-debug" |
generator |
指定构建工具,这里用 "Ninja" |
binaryDir |
构建目录,这里 ${sourceDir}/build/debug 表示在项目源目录下创建 build/debug |
cacheVariables |
配置 CMake 缓存变量,比如:CMAKE_BUILD_TYPE 设置为 "Debug" |
理解
- 这个 preset 定义了一个 Debug 构建,使用 Ninja 构建工具
- 构建输出会放在
${sourceDir}/build/debug目录 - CMake 执行配置时,会自动把
CMAKE_BUILD_TYPE设置为Debug - 优点:
- 无需每次在命令行手动输入
-G Ninja -B build/debug -DCMAKE_BUILD_TYPE=Debug - 团队共享这个 preset,每个人都能统一构建环境
总结口诀:
- 无需每次在命令行手动输入
- name → 预设名字
- generator → 使用哪个构建工具
- binaryDir → 构建输出目录
- cacheVariables → 配置缓存变量(编译类型、路径等)
一个完整的 CMake Presets 示例,包含 Debug 和 Release 两个 preset,并展示如何快速使用它们构建项目。
CMakePresets.json 示例
{
"version": 3,
"configurePresets": [
{
"name": "ninja-debug",
"generator": "Ninja",
"binaryDir": "${sourceDir}/build/debug",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug",
"MY_CUSTOM_OPTION": "ON"
}
},
{
"name": "ninja-release",
"generator": "Ninja",
"binaryDir": "${sourceDir}/build/release",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Release",
"MY_CUSTOM_OPTION": "OFF"
}
}
]
}
说明
| 字段 | 解释 |
|---|---|
version |
Preset 文件版本 |
configurePresets |
配置预设数组 |
name |
预设名称,例如 "ninja-debug" 或 "ninja-release" |
generator |
指定构建工具,这里都使用 "Ninja" |
binaryDir |
构建输出目录,Debug 和 Release 分开 |
cacheVariables |
配置缓存变量,示例中除了 CMAKE_BUILD_TYPE 还有自定义选项 MY_CUSTOM_OPTION |
使用示例
- 选择 Debug 构建
cmake --preset=ninja-debug
cmake --build build/debug
- 选择 Release 构建
cmake --preset=ninja-release
cmake --build build/release
通过 preset,无需每次手动输入生成器、构建目录和缓存变量,直接快速切换配置。
总结
- Debug / Release 可以用不同的 preset 分开管理
- preset 可以保存生成器、构建目录、缓存变量等
- 方便团队共享或者个人快速切换构建环境
预编译头文件 (Precompiled Headers, PCH)
概念
- 预编译头文件:将一些常用的头文件提前编译成部分处理过的文件
- 目的:
- 避免每次编译都重复解析大量公共头文件
- 加快编译速度
使用方式 (CMake 示例)
add_library(leaf SHARED leaf.cxx)
target_precompile_headers(leaf
PRIVATE
<iostream>
<vector>
<unordered_map>
INTERFACE
"leaf.h"
)
字段解释
| 关键词 | 含义 |
|---|---|
target_precompile_headers |
给指定 target 设置预编译头文件 |
PRIVATE |
只有本 target 使用这些头文件进行预编译 |
INTERFACE |
依赖该 target 的其他目标也会使用这些头文件进行预编译 |
<iostream>, <vector>, <unordered_map> |
C++ 标准库头文件,PRIVATE,只在 leaf 自己编译时使用 |
"leaf.h" |
自定义头文件,INTERFACE,依赖 leaf 的其他 target 也会使用 |
理解要点
- 加速编译
- 常用头文件只编译一次
- 后续编译同样 target 或依赖它的 target 时直接使用预编译结果
- PRIVATE / INTERFACE 区别
- PRIVATE → 仅 leaf 使用
- INTERFACE → 依赖 leaf 的 target 也能使用
- 应用场景
- 大型项目中公共头文件(如 STL、第三方库)
- 头文件解析复杂、耗时的项目
总结口诀:
- PCH = 头文件提前编译 → 减少重复解析 → 提升速度
- PRIVATE = 自己用,INTERFACE = 传递给依赖者
写一个 CMake + C++ 小例子,演示 预编译头文件 (PCH) 的使用。
目录结构
PCHExample/
├── CMakeLists.txt
├── leaf/
│ ├── CMakeLists.txt
│ ├── leaf.h
│ └── leaf.cpp
└── main.cpp
leaf/leaf.h
#pragma once
#include <iostream>
#include <vector>
#include <unordered_map>
inline void leaf_function() {
std::cout << "Hello from leaf!" << std::endl;
}
leaf/leaf.cpp
#include "leaf.h"
// 可以添加更多实现
main.cpp
#include "leaf/leaf.h"
int main() {
leaf_function();
std::cout << "Hello from main!" << std::endl;
return 0;
}
leaf/CMakeLists.txt
add_library(leaf STATIC leaf.cpp)
# 设置预编译头文件
target_precompile_headers(leaf
PRIVATE
<iostream>
<vector>
<unordered_map>
INTERFACE
"leaf.h"
)
顶层 CMakeLists.txt
cmake_minimum_required(VERSION 3.24)
project(PCHExample)
add_subdirectory(leaf)
add_executable(main main.cpp)
target_link_libraries(main PRIVATE leaf)
效果说明
- leaf.cpp 编译时,
<iostream>,<vector>,<unordered_map>会被 预编译 - main.cpp 编译时,由于 INTERFACE 设置,
leaf.h也会使用预编译头文件,加快编译速度 - 输出:
Hello from leaf!
Hello from main!
总结
- PRIVATE 的头文件只用于 leaf 自己
- INTERFACE 的头文件会被依赖 leaf 的 target(main)使用
- 对大型项目,PCH 能显著减少头文件重复解析时间
目录内容
xiaqiu@xz:~/test/build/CppCon/day416/code/leaf/CMakeFiles/leaf.dir$ tree
.
├── cmake_pch.hxx
├── cmake_pch.hxx.cxx
├── cmake_pch.hxx.gch
└── leaf.cpp.o
1 directory, 4 files
xiaqiu@xz:~/test/build/CppCon/day416/code/leaf/CMakeFiles/leaf.dir$
文件解释
cmake_pch.hxx- 这是 CMake 生成的 统一预编译头文件(PCH)源文件)
- 它会包含你在
target_precompile_headers中指定的头文件 - 类似把
<iostream>,<vector>,<unordered_map>或"leaf.h"放进一个单独文件,供编译器预编译
cmake_pch.hxx.cxx- 实际的 编译单元源文件
- 编译器会把这个文件编译成 预编译头文件
- 相当于“中间文件”,将公共头文件解析一次,生成可重用的编译结果
cmake_pch.hxx.gch.gch文件是 GCC/Clang 生成的预编译头文件(GCC Precompiled Header)- 编译器在后续编译 leaf.cpp 时,会直接使用这个
.gch文件 - 避免每次都重新解析头文件,提高编译速度
leaf.cpp.o- leaf.cpp 编译后的目标文件
- 编译器在编译时使用了上面生成的 PCH 文件(cmake_pch.hxx.gch)
总结理解
- 作用链:
target_precompile_headers 指定的头文件
↓
cmake_pch.hxx.cxx → 编译生成 cmake_pch.hxx.gch (预编译头文件)
↓
leaf.cpp 编译 → 使用 cmake_pch.hxx.gch 加速编译 → 生成 leaf.cpp.o
- 好处:
- 避免重复解析
<iostream>,<vector>等头文件 - 对大型项目或依赖复杂头文件的 target,能显著提升编译速度
- 避免重复解析
Multi-Config Ninja(多配置 Ninja 构建)
概念
- Ninja 是一种快速的命令行构建工具
- Multi-Config 版本支持同时在一个构建目录里生成多种配置(Debug / Release 等),类似 Visual Studio 的多配置项目
使用示例
# 使用 ccache 加速编译
export CMAKE_CXX_COMPILER_LAUNCHER=ccache
# 配置构建目录,指定 Multi-Config Ninja
cmake -G "Ninja Multi-Config" -S ./src -B ./build
# 构建 Debug 配置
cmake --build ./build --config Debug -j10 -v
# 编译器实际调用
/usr/bin/ccache gcc ...
# 测试
ctest --build-config Debug -j10
要点
- CMAKE__COMPILER_LAUNCHER 可以指定编译器启动器,如
ccache,用来加速编译 - Multi-Config 构建目录可以同时支持 Debug 和 Release,无需重复生成
全平台安装系统(Install System)
概念
- CMake 提供统一的安装机制,可以跨平台安装目标文件、普通文件或整个目录
- 默认提供一些安装路径,但可以自定义规则
示例
add_library(leaf SHARED leaf.cxx)
install(TARGETS root trunk leaf parasite)
- 说明:
- 安装 target(库或可执行文件)
- 可以指定安装目录(CMAKE_INSTALL_PREFIX)
- 支持文件、目录安装
打包(Packaging)
概念
- CPack 是 CMake 自带的打包工具
- 可以创建 平台特定的安装程序(Installer),如:
- Windows
.msi或.exe - Linux
.deb/.rpm - macOS
.dmg
- Windows
特点
- 配置简单,命令式或脚本式
- 可以生成专业的安装包,方便分发
总结
- Multi-Config Ninja
- 支持 Debug / Release 共存
- 配合
ccache可加速编译 - 通过
--config指定构建类型
- Install System
- 安装库、可执行文件、文件夹
- 支持跨平台
- 默认路径 + 可自定义规则
- CPack
- CMake 自带打包工具
- 创建平台相关安装程序
- 简化发布流程
口诀理解:
- Ninja Multi-Config = 同一个目录多配置 + 快速编译
- install() = 安装目标 / 文件 / 目录
- CPack = 打包成平台安装器
CPack 功能特性(CPack Features)
- 支持项目类型
- CMake 项目 或 非 CMake 项目 都可以使用
- 跨平台支持的安装包类型
| 平台 | 支持的包类型 |
| ------------ | --------------------------------- |
| Unix / Linux |.tar.gz(TGZ),自解压 TGZ (STGZ) |
| Windows | WiX → MSI 安装包、NSIS / NSIS64 安装程序 |
| MacOS | Drag & Drop 安装、PackageMaker |
| Linux |.deb(Debian 包)、.rpm(RPM 包管理器) | - 总结:CPack 能根据不同平台生成对应的标准安装包,无需手动打包脚本
使用 CPack(Using CPack)
- 安装必要工具(Windows 示例):
- ZIP 命令行工具
- NSIS / NSIS64
- WiX 工具
- 设置项目支持 CPack:
- 先确保
make install工作正常 - 在 CMakeLists.txt 中使用
install(...)安装 target / 文件 / 目录
- 先确保
- 注意事项:
- 确保可执行文件能使用 相对路径,并可从任意目录运行
- 根据需要设置 CPack 选项变量,例如包名、版本号、安装目录等
- 在 CMakeLists.txt 中包含 CPack:
include(CPack)
- 这一步会启用 CPack 打包功能,并生成平台特定安装包命令
理解总结
- 功能强大:跨平台、支持多种安装包类型
- 使用方便:依赖 CMake 的
install()系统即可 - 配置灵活:可设置安装路径、包名、版本号等
- 流程:
CMake 配置 -> make install -> include(CPack) -> 生成安装包
口诀理解:
- Unix = TGZ / STGZ
- Windows = MSI / NSIS
- Mac = Drag&Drop / PackageMaker
- Linux = DEB / RPM
CMake + CPack 小项目示例,展示如何配置 install 并生成跨平台安装包。
目录结构
CPackExample/
├── CMakeLists.txt
├── main.cpp
└── include/
└── hello.h
include/hello.h
#pragma once
#include <iostream>
inline void say_hello() {
std::cout << "Hello from CPackExample!" << std::endl;
}
main.cpp
#include "hello.h"
int main() {
say_hello();
return 0;
}
CMakeLists.txt
cmake_minimum_required(VERSION 3.24)
project(CPackExample VERSION 1.0 LANGUAGES CXX)
# 添加可执行文件
add_executable(CPackExample main.cpp)
target_include_directories(CPackExample PUBLIC include)
# 安装目标
install(TARGETS CPackExample
RUNTIME DESTINATION bin) # Windows / Linux 可执行文件安装路径
install(DIRECTORY include/ DESTINATION include) # 安装头文件
# 配置 CPack
set(CPACK_PACKAGE_NAME "CPackExample")
set(CPACK_PACKAGE_VENDOR "xiaqiu")
set(CPACK_PACKAGE_VERSION_MAJOR ${PROJECT_VERSION_MAJOR})
set(CPACK_PACKAGE_VERSION_MINOR ${PROJECT_VERSION_MINOR})
set(CPACK_PACKAGE_VERSION_PATCH 0)
set(CPACK_GENERATOR "TGZ;ZIP") # 生成 .tar.gz 和 .zip 安装包
include(CPack)
使用流程
- 生成构建目录并编译
cmake -B build -S .
cmake --build build -j4
- 安装到临时目录(模拟安装)
cmake --install build --prefix ./install
- 可执行文件在
install/bin/CPackExample - 头文件在
install/include/hello.h
- 生成安装包
cmake --build build --target package
- 生成
CPackExample-1.0-Linux.tar.gz和CPackExample-1.0-Linux.zip(Linux 示例)
效果说明
- 你可以直接把生成的
.tar.gz或.zip分发给别人 - 包含可执行文件和头文件
- Windows 下可用 NSIS / MSI,Mac 下可用 Drag&Drop,只需修改
CPACK_GENERATOR
总结 install()→ 安装文件/目标CPack→ 打包安装目录成标准安装包- 配置
CPACK_PACKAGE_*→ 设置包名、版本、作者等信息 - 生成器 (
CPACK_GENERATOR) 决定输出格式
xiaqiu@xz:~/test/CppCon/day416/code$ cmake -B build -S .
-- The CXX compiler identification is GNU 13.3.0
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Check for working CXX compiler: /usr/bin/c++ - skipped
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Configuring done (0.8s)
-- Generating done (0.0s)
-- Build files have been written to: /home/xiaqiu/test/CppCon/day416/code/build
xiaqiu@xz:~/test/CppCon/day416/code$ cmake --build build -j4
[ 50%] Building CXX object CMakeFiles/CPackExample.dir/main.cpp.o
[100%] Linking CXX executable CPackExample
[100%] Built target CPackExample
xiaqiu@xz:~/test/CppCon/day416/code$ ls
CMakeLists.txt build include main.cpp
xiaqiu@xz:~/test/CppCon/day416/code$ cmake --install build --prefix ./install
-- Install configuration: ""
-- Installing: /home/xiaqiu/test/CppCon/day416/code/./install/bin/CPackExample
-- Installing: /home/xiaqiu/test/CppCon/day416/code/./install/include
-- Installing: /home/xiaqiu/test/CppCon/day416/code/./install/include/hello.h
xiaqiu@xz:~/test/CppCon/day416/code$ ls
CMakeLists.txt build include install main.cpp
xiaqiu@xz:~/test/CppCon/day416/code$ cmake --build build --target package
[100%] Built target CPackExample
Run CPack packaging tool...
CPack: Create package using TGZ
CPack: Install projects
CPack: - Run preinstall target for: CPackExample
CPack: - Install project: CPackExample []
CPack: Create package
CPack: - package: /home/xiaqiu/test/CppCon/day416/code/build/CPackExample-1.0.0-Linux.tar.gz generated.
CPack: Create package using ZIP
CPack: Install projects
CPack: - Run preinstall target for: CPackExample
CPack: - Install project: CPackExample []
CPack: Create package
CPack: - package: /home/xiaqiu/test/CppCon/day416/code/build/CPackExample-1.0.0-Linux.zip generated.
xiaqiu@xz:~/test/CppCon/day416/code$ ls
CMakeLists.txt build include install main.cpp
xiaqiu@xz:~/test/CppCon/day416/code$ cd build/
xiaqiu@xz:~/test/CppCon/day416/code/build$ ls
CMakeCache.txt CPackConfig.cmake CPackExample-1.0.0-Linux.tar.gz CPackSourceConfig.cmake _CPack_Packages install_manifest.txt
CMakeFiles CPackExample CPackExample-1.0.0-Linux.zip Makefile cmake_install.cmake
xiaqiu@xz:~/test/CppCon/day416/code/build$
CMake 测试 (Testing with CMake)
启用测试
- 必须先启用测试功能,通过以下任意一种方式:
include(CTest)
# 或者
enable_testing()
- 作用:告诉 CMake 项目里会有测试用例,生成测试相关的构建规则
添加测试
add_test(NAME testname
COMMAND exename arg1 arg2 ...)
- NAME:测试名称(testname)
- COMMAND:执行的可执行文件及参数
- 返回值:
- 可执行文件返回
0→ 测试通过 - 返回非 0 → 测试失败
- 可执行文件返回
CTest 工具
ctest是随 CMake 分发的命令行工具- 用于运行项目中的所有测试
- 用法:
# 在构建目录顶层运行所有测试
ctest
示例输出
$ ctest
Test project /tmp/example/bin
Start 1: case1
1/1 Test #1: case1 ............................. Passed 0.00 sec
Start 2: case2
2/2 Test #2: case2 ............................. Passed 0.00 sec
100% tests passed, 0 tests failed out of 2
Total Test time (real) = 0.01 sec
- 每个测试都会显示:
- 测试编号
- 测试名称
- 测试状态(Passed / Failed)
- 执行时间
- 总结信息:
- 测试总数
- 通过数量
- 失败数量
- 总耗时
理解总结
- 启用测试 →
include(CTest)或enable_testing() - 添加测试 →
add_test(NAME … COMMAND …) - 运行测试 →
ctest - 返回值判断:
0→ 通过- 非
0→ 失败
口诀理解:
- include/enable → add_test → ctest → 返回 0 通过
一个 完整的 CMake + CTest 小项目示例,演示如何配置测试、编译并运行测试。
目录结构
CTestExample/
├── CMakeLists.txt
├── main.cpp
└── tests/
├── CMakeLists.txt
├── test_add.cpp
└── test_subtract.cpp
main.cpp
#pragma once
int add(int a, int b) {
return a + b;
}
int subtract(int a, int b) {
return a - b;
}
tests/test_add.cpp
#include <cassert>
#include "../main.cpp"
int main() {
assert(add(2, 3) == 5); // 通过
assert(add(-1, 1) == 0); // 通过
return 0; // 返回 0 表示测试通过
}
tests/test_subtract.cpp
#include <cassert>
#include "../main.cpp"
int main() {
assert(subtract(5, 3) == 2); // 通过
assert(subtract(2, 2) == 0); // 通过
return 0; // 返回 0 表示测试通过
}
tests/CMakeLists.txt
# 添加测试可执行文件
add_executable(test_add test_add.cpp)
add_executable(test_subtract test_subtract.cpp)
# 添加 CTest 测试
add_test(NAME add_test COMMAND test_add)
add_test(NAME subtract_test COMMAND test_subtract)
顶层 CMakeLists.txt
cmake_minimum_required(VERSION 3.24)
project(CTestExample)
# 启用 CTest 测试功能
enable_testing()
# 或者 include(CTest)
# 添加 tests 子目录
add_subdirectory(tests)
使用流程
- 生成构建目录并编译
cmake -B build -S .
cmake --build build -j4
- 运行测试
ctest -C Debug -V
示例输出
Test project /path/to/CTestExample/build
Start 1: add_test
1/2 Test #1: add_test ....................... Passed 0.00 sec
Start 2: subtract_test
2/2 Test #2: subtract_test .................. Passed 0.00 sec
100% tests passed, 0 tests failed out of 2
Total Test time (real) = 0.01 sec
要点总结
- 启用测试 →
enable_testing()或include(CTest) - 添加测试可执行文件 →
add_executable(test_name …) - 注册测试 →
add_test(NAME … COMMAND …) - 运行测试 →
ctest - 通过/失败判断 → 可执行文件返回
0表示通过
扩展:
- 可以用
ctest -V查看详细输出 - 可以用
ctest -R <regex>只运行匹配的测试 - 未来可以加入 CTest 配合 CPack,打包时自动运行测试
GoogleTest 集成(GoogleTest integration)
步骤概览
- 引入 GoogleTest 模块
include(GoogleTest)
- 这个模块提供了 CMake 与 GoogleTest 的集成功能
- 添加测试可执行文件
add_executable(tests tests.cpp)
target_link_libraries(tests GTest::GTest)
add_executable:生成测试可执行文件target_link_libraries:链接 GoogleTest 库
- 使用 gtest_discover_tests 自动发现测试
gtest_discover_tests(tests)
- 作用:
- CMake 会调用测试可执行文件,让它列出所有 GoogleTest 的 test case
- 自动将这些测试注册到 CTest
- 无需手动写
add_test() - 可以发现新测试而 不需要重新运行 CMake
工作原理
tests (GoogleTest 可执行文件)
|
v
gtest_discover_tests
|
v
自动注册所有测试到 CTest
- 后续使用
ctest就可以直接运行这些测试 - 支持详细输出、单独运行某个测试、并行运行等 CTest 功能
理解总结
- include(GoogleTest) → 引入 GoogleTest 集成模块
- add_executable + target_link_libraries → 编译 GoogleTest 测试程序
- gtest_discover_tests → 自动发现 GoogleTest 测试,注册到 CTest,无需手动
add_test - 优势:
- 自动管理测试列表
- 新增测试无需重新配置 CMake
- 与 CTest 无缝集成
口诀理解:
- include → add_executable → link GTest → gtest_discover_tests → ctest 运行所有测试
一个 完整的 CMake + GoogleTest 小项目示例,演示如何自动发现测试并用 ctest 运行。
目录结构
GTestExample/
├── CMakeLists.txt
├── main.cpp
└── tests/
├── CMakeLists.txt
└── test_math.cpp
main.cpp
#pragma once
int add(int a, int b) {
return a + b;
}
int subtract(int a, int b) {
return a - b;
}
tests/test_math.cpp
#include <gtest/gtest.h>
#include "../main.cpp"
TEST(AddTest, PositiveNumbers) { EXPECT_EQ(add(2, 3), 5); }
TEST(AddTest, NegativeNumbers) { EXPECT_EQ(add(-1, -1), -2); }
TEST(SubtractTest, PositiveNumbers) { EXPECT_EQ(subtract(5, 3), 2); }
TEST(SubtractTest, ZeroResult) { EXPECT_EQ(subtract(2, 2), 0); }
int main(int argc, char **argv) {
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}
tests/CMakeLists.txt
# 添加测试可执行文件
add_executable(tests test_math.cpp)
# 链接 GoogleTest 库
target_link_libraries(tests GTest::GTest)
# 启用 GoogleTest 自动发现
include(GoogleTest)
gtest_discover_tests(tests)
顶层 CMakeLists.txt
cmake_minimum_required(VERSION 3.24)
project(GTestExample LANGUAGES CXX)
# 启用测试功能
enable_testing()
# 查找 GoogleTest
find_package(GTest REQUIRED)
# 添加 tests 子目录
add_subdirectory(tests)
使用流程
- 生成构建目录并编译
cmake -B build -S .
cmake --build build -j4
- 运行测试
cd build
ctest -V
示例输出
Test project /path/to/GTestExample/build
UpdateCTestConfiguration from :/path/to/GTestExample/build/DartConfiguration.tcl
Constructing a list of tests
Checking test dependency graph...
Checking test dependency graph end
1/4 Testing: tests/AddTest.PositiveNumbers
1/4 Test: tests/AddTest.PositiveNumbers ... Passed 0.00 sec
2/4 Testing: tests/AddTest.NegativeNumbers
2/4 Test: tests/AddTest.NegativeNumbers ... Passed 0.00 sec
3/4 Testing: tests/SubtractTest.PositiveNumbers
3/4 Test: tests/SubtractTest.PositiveNumbers ... Passed 0.00 sec
4/4 Testing: tests/SubtractTest.ZeroResult
4/4 Test: tests/SubtractTest.ZeroResult ... Passed 0.00 sec
100% tests passed, 0 tests failed out of 4
Total Test time (real) = 0.01 sec
要点总结
- 查找 GTest →
find_package(GTest REQUIRED) - 添加测试可执行文件 →
add_executable - 链接 GTest →
target_link_libraries(tests GTest::GTest) - 自动发现测试 →
gtest_discover_tests(tests) - 运行测试 →
ctest -V
好处:
- 新增 TEST 宏定义的测试无需修改 CMake
- 支持 CTest 命令行参数,例如只运行部分测试、并行运行等
好的,我来帮你把 CTest 多核测试(multi-core tests) 的内容用整理理解:
CTest 多核测试
概念
- PROCESSOR_AFFINITY
- 在支持的平台上,可以将测试进程绑定到指定的 CPU 核心
- 这样可以:
- 减少 CPU 上其他进程干扰
- 控制测试进程在固定核心运行
- 更稳定地测量性能
- 相关属性:
PROCESSOR_AFFINITY ON:启用 CPU 核心绑定PROCESSORS <核心列表或数量>:指定使用的核心
CMake 示例
# 添加测试
add_test(NAME myTest COMMAND myTestExecutable)
# 设置多核属性
set_tests_properties(myTest PROPERTIES
PROCESSOR_AFFINITY ON
PROCESSORS 4)
- 上述配置意思:
- 启用多核绑定
- 将
myTest测试绑定到 第 4 个核心(或者指定核心 0~3 共 4 核,具体平台实现可能不同)
使用场景
- 性能测试
- 测试程序对 CPU 核心敏感时,固定核心可以减少波动
- 并行测试
- 多个测试同时运行时,可以指定不同核心,提高并行度
注意事项
- 并非所有平台都支持 CPU 核心绑定
- Linux 上一般需要
sched_setaffinity支持 - Windows 上依赖进程亲和性(Processor Affinity)
理解总结:
PROCESSOR_AFFINITY ON → 启用多核绑定
PROCESSORS N → 指定使用第 N 核或 N 个核
作用:稳定测试性能、控制 CPU 分配
CDash https://my.cdash.org/
CDash 是一个开源的基于 Web 的软件测试服务器,用于聚合、分析和展示全球客户端提交的测试结果。它是 CMake、CTest 和 CPack 工具链的一部分,广泛应用于持续集成(CI)和质量监控流程中 ([cdash.org][1])。
CDash 的作用
- 集中展示测试结果:CDash 汇总来自多个平台和构建配置的测试结果,生成易于浏览的 Web 仪表盘。
- 支持多种构建系统:虽然与 CMake 紧密集成,但也支持非 CMake 项目的测试结果提交。
- 自动化质量监控:与 CTest 配合使用,可实现夜间构建、覆盖率分析、内存检查等功能。
- 跨平台支持:适用于 Linux、Windows、macOS 等多种操作系统。
如何使用 CDash
1. 创建 CDash 项目
访问 https://my.cdash.org,注册账户并创建一个新项目。在项目设置页面中,您将获得一个 CTestConfig.cmake 文件,其中包含项目名称、提交 URL 和夜间构建开始时间等信息。
2. 配置 CMake 项目
在项目的顶层 CMakeLists.txt 中,添加以下内容:
include(CTest)
这将启用 CTest 支持,并允许将测试结果提交到 CDash。
3. 配置 CTestConfig.cmake
将从 CDash 项目页面下载的 CTestConfig.cmake 文件放置在项目的顶层目录。该文件包含以下内容:
set(CTEST_PROJECT_NAME "YourProjectName")
set(CTEST_NIGHTLY_START_TIME "00:00:00 EST")
set(CTEST_DROP_METHOD "http")
set(CTEST_DROP_SITE "my.cdash.org")
set(CTEST_DROP_LOCATION "/submit.php?project=YourProjectName")
set(CTEST_DROP_SITE_CDASH TRUE)
请勿修改该文件,以确保 CTest 正确提交结果。
4. 提交测试结果
在构建目录中,运行以下命令以提交测试结果:
ctest -D Experimental
对于多配置生成器(如 Visual Studio),请指定配置类型:
ctest -C Debug -D Experimental
这将构建项目、运行测试,并将结果提交到 CDash。
CDash 仪表盘内容
CDash 仪表盘通常包括以下部分:
- Nightly:每日构建和测试的结果。
- Experimental:开发人员提交的实验性构建结果。
- Continuous:持续集成的构建和测试结果。
- Coverage:代码覆盖率分析结果。
- Dynamic Analysis:动态分析(如内存泄漏检查)结果。
每个部分都提供详细的构建和测试信息,帮助开发人员快速定位问题。
CDash 的优势
- 集中管理:所有测试结果集中在一个地方,便于查看和分析。
- 跨平台支持:支持多种操作系统和构建配置。
- 自动化:与 CTest 配合使用,实现自动化构建、测试和结果提交。
- 可扩展性:支持自定义构建和测试流程,满足不同项目的需求。
相关资源
- CDash 官方网站
- CMake 官方文档:CDash
- CDash GitHub 仓库
如果您希望我为您提供一个完整的示例,展示如何配置 CMake 项目以使用 CDash,请告诉我,我将很乐意为您提供帮助。
[1]: https://www.cdash.org/?utm_source=chatgpt.com “Accelerate your Development Process with CDash”
C++20 模块示例及问题
模块示例
// B.cpp
export module B;
export void b() { }
// A.cpp
export module A;
import B;
export void a() { b(); }
- 模块 B 导出函数
b - 模块 A 导入模块 B 并调用
b()
常见错误
cl -std:c++20 -interface -c A.cpp
A.cpp(2): error C2230: could not find module 'B'
A.cpp(3): error C3861: 'b': identifier not found
原因:
- 编译顺序很重要:模块 B 必须先生成模块接口文件(BMI)
- 缺少模块接口文件:A.cpp 编译时找不到 B 的 .ifc 文件
解决方法
- 先编译 B.cpp 接口
cl -std:c++20 -interface -c B.cpp
- 再编译 A.cpp
cl -std:c++20 -interface -c A.cpp
- 编译后生成的文件:
A.cpp A.ifc A.obj B.cpp B.ifc B.obj
.ifc→ MSVC 模块接口文件- GCC 对应
.gcm文件
模块的构建挑战
- C++ 模块引入新的编译模型:
- 传统头文件依赖只需解析一次,模块依赖可能需要在首次构建时动态发现
- 需要工具链和标准支持:
- 模块依赖不能完全靠构建系统推断
- 编译器需提供模块接口和依赖信息
CMake 对模块的经验
- Fortran 模块的经验:
- 2005 年:CMake 添加 Fortran 模块依赖扫描
- 2015 年:Ninja 支持 Fortran 模块依赖
- 2019 年:Ninja 合并对模块的支持,为 C++ 模块打基础
- 动态依赖解析:
- 模块依赖信息可能需要在首次构建时从源文件内容解析
- 传统头文件只在第二次及以后才需要
- CMake 的做法:
- Fortran 模块:基于
makedepf90的解析器 - Ninja 构建工具上游补丁支持动态依赖
- C++ 模块也需要类似解析和动态依赖信息
- Fortran 模块:基于
文档与标准
- P1838R0:模块用户词汇和文件扩展标准
- MSVC →
.ifc - GCC →
.gcm - 规范了模块接口文件、导入导出语法等
- MSVC →
- 参考资料:
- Kitware 对 Fortran 模块的经验可用于 C++ 模块
- HTML 版文档:Fortran Modules in CMake
理解总结
- 模块编译顺序很重要:
- 先编译依赖模块接口
- 再编译使用该模块的文件
- BMI(Built Module Interface):
- MSVC →
.ifc - GCC →
.gcm - 模块文件必须生成并可被依赖模块找到
- MSVC →
- CMake 经验:
- 先从 Fortran 模块开始支持
- Ninja 构建工具支持动态依赖
- C++ 模块需要类似机制,解析源文件确定依赖
- 工具链与标准协作:
- 编译器提供模块接口
- CMake/Ninja 处理依赖和构建顺序
核心理解:
- 模块不是普通头文件,必须按依赖顺序编译
- 首次构建需要解析源文件动态发现依赖
- CMake 已有 Fortran 模块经验,可迁移到 C++20 模块
好的,我来帮你把 Fortran 模块的构建工作原理 用整理理解。
Fortran 模块示例
module math
contains
function add(a,b)
real :: add
real, intent(in) :: a, b
add = a + b
end function
end module
program main
use math
print *, 'sum is', add(1.,2.)
end program
- 模块
math导出函数add - 编译
main时,需要math.mod文件 - 编译输出:
math.o→ 模块的目标文件math.mod→ 模块接口文件(编译器生成)
单目标构建图(单库/单模块)
假设有三个源文件 a1.F90, a2.F90, a3.F90:
- a1.F90 编译生成模块接口:
a1.F90 → preprocess → a1.pp.f90 → compile → math.mod + a1.o - a2.F90 依赖 math.mod:
a2.F90 → preprocess → a2.pp.f90 → compile → a2.o- 必须等待 a1.F90 生成 math.mod
- a3.F90 独立于 math.mod,可并行编译:
a3.F90 → preprocess → a3.pp.f90 → compile → a3.o
特点
- Ninja 构建系统会根据依赖生成 构建图
- 并行编译:
- a1.F90 和 a3.F90 可同时进行
- a2.F90 必须等待 math.mod
- 增量编译优化:
- 如果 a1.F90 修改后 math.mod 未变化,a2.F90 不会被重复编译
- 新增依赖会被动态发现,通过 A.dd 文件更新构建图
多目标构建(多个库/模块)
假设有库 A、B、C:
- 每个库生成
.ddi文件(direct dependency info,直接依赖信息) - 构建步骤:
- 预处理 → 编译 → 收集依赖 → 链接
- 模块间依赖:
- 库 B 中的模块可以被库 C 中的源文件使用
- Ninja 根据
.dd文件正确处理依赖 - 不会破坏增量编译效率
示意
Library B:
b1.ddi → preprocess → compile → collate → B.dd
b2.ddi → ...
b3.ddi → ...
Library A:
a1.ddi → preprocess → compile → collate → A.dd
a2.ddi → ...
a3.ddi → ...
Library C:
c1.ddi → preprocess → compile → collate → C.dd
c2.ddi → ...
c3.ddi → ...
.ddi和.dd文件记录依赖信息- 支持 跨库模块依赖,确保模块先生成再被依赖文件使用
- Ninja 根据依赖图处理增量编译和并行编译
理解总结
- 模块依赖关系:
- 模块必须先生成
.mod(或.ifc)文件 - 依赖该模块的源文件必须等待
- 模块必须先生成
- 增量编译优化:
- 如果模块未变化,依赖它的文件不会被重复编译
- 新增依赖会动态更新构建图
- 多库模块支持:
- 使用
.ddi/.dd文件保存模块依赖 - 支持跨库模块使用
- 构建系统(如 Ninja)能正确处理并行和增量编译
- 使用
- 核心思想:
- 动态发现模块依赖 → 构建图更新 → 并行 + 增量编译
这段关于 C++20 模块当前支持情况(MSVC / GCC / CMake) 用整理理解:
当前模块支持现状
1. Visual Studio 2022 Preview
- 支持模块扫描(module scanning)
- 使用
/scanDependencies参数生成 JSON 依赖文件 - 示例:
cl -std:c++20 -scanDependencies A.json -c A.cpp - 输出 JSON 文件:
{ "version": 1, "rules": [ { "primary-output": "A.obj", "outputs": ["A.json", "A.ifc"], "provides": [{"logical-name": "A", "source-path": "A.cpp"}], "requires": [{"logical-name": "B"}] } ] } - 解释:
provides→ 当前编译单元提供的模块(A)requires→ 当前编译单元依赖的模块(B)
- 同样 B.cpp JSON 输出没有
requires,表示它没有依赖其他模块
- 使用
2. GCC(带补丁)
- 目前 支持命名模块(named modules),但需要补丁:
- GitHub 补丁仓库:mathstuf/gcc/p1689r5
- 使用
-fmodules-ts及依赖扫描生成.ddi文件 - 示例:
c++ -std=gnu++20 -MD -MF A.cpp.o.ddi.d -E -x c++ A.cpp \ -MT A.cpp.o.ddi -fmodules-ts -fdep-file=A.cpp.o.ddi \ -fdep-output=A.cpp.o -fdep-format=trtbd -o A.cpp.o.ddi.i - 输出 JSON 类似:
{ "rules": [ { "primary-output": "A.cpp.o", "provides": [{"logical-name": "A"}], "requires": [{"logical-name": "B"}] } ] } - 解释:
- 与 MSVC 类似,GCC JSON 文件记录了模块提供和依赖关系
- B.cpp 的 JSON 文件
requires为空,表示无依赖
3. CMake 对模块的支持
- CMake 主分支(master)已经 开始支持 C++20 模块
- 借鉴了 Fortran 模块的动态依赖机制
- 构建系统可利用:
- JSON / DDI 文件进行依赖解析
- 保证模块接口生成顺序正确
- 支持增量构建和并行编译
理解总结
- 模块扫描
- MSVC →
/scanDependencies生成 JSON - GCC →
-fmodules-ts+-fdep-file生成 DDI/JSON - JSON 文件记录
provides(提供模块)和requires(依赖模块)
- MSVC →
- 命名模块支持
- VS 2022 Preview 支持
- GCC 需补丁支持
- 构建顺序和依赖管理
- 构建系统(如 CMake + Ninja)可以利用扫描信息
- 确保模块先生成接口文件,再编译依赖模块
- 支持增量构建和并行编译
- 结论
- 当前基本支持命名模块
- 构建工具链正在逐步完善对 C++20 模块的完整支持
理解口诀:
A depends on B
先生成 B.ifc
再编译 A.cpp
MSVC / GCC 都有扫描 JSON
CMake 可自动解析依赖
CMake 3.23+ 的 FILE_SET 功能 用整理理解:
FILE_SET 概念
- FILE_SET 是 CMake 3.23 引入的新特性
- 作用:把一组文件(通常是头文件或 C++ 模块)组织到目标(target)中,便于管理和安装
语法
target_sources(<target>
[<INTERFACE|PUBLIC|PRIVATE>]
[FILE_SET <set> [TYPE <type>] [BASE_DIRS <dirs>...] [FILES <files>...]]...)
<target>→ 目标名(库或可执行文件)<INTERFACE|PUBLIC|PRIVATE>→ 文件集的作用域FILE_SET <set>→ 文件集名称TYPE→ 文件类型,目前只支持:HEADERS→ 头文件CXX_MODULES→ C++ 模块
BASE_DIRS→ 文件所在目录FILES→ 文件列表
注意:目标不能是 custom target 或 framework target。
使用 FILE_SET 管理头文件
旧方式(没有 FILE_SET)
set(Eigen_headers
src/eigen.h
src/vector.h
src/matrix.h
)
add_library(Eigen INTERFACE ${Eigen_headers})
target_include_directories(Eigen INTERFACE
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/src>
$<INSTALL_INTERFACE:include/Eigen>
)
install(TARGETS Eigen EXPORT eigenExport)
install(EXPORT eigenExport NAMESPACE Upstream:: DESTINATION lib/cmake/Eigen)
install(FILES ${Eigen_headers} DESTINATION include/Eigen)
add_executable(exe1 exe1.cpp)
target_link_libraries(exe1 Eigen)
- 需要单独调用
install(FILES ...)来安装头文件 - 代码稍繁琐
新方式(使用 FILE_SET)
add_library(Eigen INTERFACE)
target_sources(Eigen INTERFACE
FILE_SET HEADERS
BASE_DIRS src
FILES src/eigen.h src/vector.h src/matrix.h
)
install(TARGETS Eigen EXPORT eigenExport
FILE_SET HEADERS DESTINATION include/Eigen
)
install(EXPORT eigenExport NAMESPACE Upstream:: DESTINATION lib/cmake/Eigen)
add_executable(exe1 exe1.cpp)
target_link_libraries(exe1 Eigen)
- 优点:
- 文件集(FILE_SET)直接关联到库目标
- 安装时可直接指定
FILE_SET HEADERS DESTINATION - 更加整洁和可维护
安装执行
cmake --install . --prefix $HOME/Work/cxxmodules/file_set/b/inst
ninja install
- 输出示例:
Installing: .../inst/include/Eigen/eigen.h
Installing: .../inst/include/Eigen/vector.h
Installing: .../inst/include/Eigen/matrix.h
Installing: .../inst/lib/cmake/Eigen/eigenExport.cmake
- 头文件和 CMake 配置文件自动安装
FILE_SET 的扩展
- 文件类型 TYPE 目前可选:
HEADERS→ 普通头文件CXX_MODULES→ C++20 模块文件
- 使用 FILE_SET 可以方便管理 头文件 + 模块文件,尤其适合 header-only 库 或 模块库
总结
- FILE_SET = 目标的文件集合(头文件或 C++ 模块)
- 作用:
- 管理目标源文件
- 简化安装(install)步骤
- 方便导出到 CMake 包
- 使用方法:
target_sources()指定 FILE_SETinstall(TARGETS ... FILE_SET ...)安装文件集
- 类型:
- HEADERS → 头文件
- CXX_MODULES → C++ 模块
理解口诀:
库有头文件?FILE_SET HEADERS
库有模块文件?FILE_SET CXX_MODULES
target_sources + install 一起用
安装更方便,代码更整洁
C++20 模块 + CMake FILE_SET + 构建流程 内容用整理和理解:
1. FILE_SET 与 C++20 模块
- CMake 3.23 引入
FILE_SET支持模块文件(CXX_MODULES) - 示例:
cmake_minimum_required(VERSION 3.23)
project(simple CXX)
set(CMAKE_CXX_STANDARD 20)
add_library(simple)
target_sources(simple
PRIVATE
FILE_SET cxx_modules TYPE CXX_MODULES FILES
A.cpp B.cpp
)
A.cpp和B.cpp被作为 C++模块文件加入目标simple- 注意:C++20 模块支持在 CMake 中仍是 实验性,通过
CMAKE_EXPERIMENTAL_CXX_MODULE_DYNDEP激活
2. MSVC 编译模块流程
- 使用 ninja + CMake 构建
- 核心步骤:
- 编译每个单元(TU)生成
.obj并扫描依赖cl.exe /std:c++20 -interface -scanDependencies A.cpp cl.exe /std:c++20 -interface -scanDependencies B.cpp cmake_ninja_dyndep生成 dyndep 文件 (CXX.dd)- 再根据依赖顺序编译生成
.obj,最后生成.lib或.exe
- 编译每个单元(TU)生成
- 文件说明:
.ddi→ 单个 TU 的 JSON 依赖文件(MSVC:.obj.ddi)CXX.dd→ 合并所有 TU 的动态依赖文件TU.modmap→ 编译器使用的 BMI(Built Module Interface)映射CXXModules.json→ CMake 可读版本
3. g++ 编译模块流程(带 patch)
- 类似 MSVC,但使用 GCC 的模块扫描与 BMI 生成
- 核心命令:
c++ -std=gnu++20 -fmodules-ts -fdep-file=... -fdep-output=... -fmodule-mapper=... - Ninja + CMake 处理依赖顺序
- 最终生成
.o和静态库.a
4. 模块类型概念
- Module Unit
- 含有
module声明的单元(TU)
- 含有
- Named Module
- 同一
module-name的 所有模块单元集合
- 同一
- Module Interface Unit
- 以
export module ...开头的模块单元 - 对外提供模块接口
- 以
- Module Implementation Unit
- 没有
export的模块单元 - 提供实现,不对外导出
- 没有
- Module Partition
- 模块内部划分单元
module <name>:<partition>- 接口单元或实现单元都可以是 partition
- 规则:
- Interface Unit partition 必须被主模块接口导出
- Partition 仅能被同模块的其他 TU 导入
5. 模块示例
假设模块 A 有 4 个单元:
// Translation unit #1: primary interface
export module A;
export import :Foo;
export int baz();
// Translation unit #2: interface partition
export module A:Foo;
import :Internals;
export int foo() { return 2*(bar()+1); }
// Translation unit #3: implementation partition
module A:Internals;
int bar();
// Translation unit #4: implementation unit
module A;
import :Internals;
int bar() { return baz()-10; }
int baz() { return 30; }
- 结构:
- 主模块接口单元 (
A) - 分区接口单元 (
A:Foo) - 分区实现单元 (
A:Internals) - 实现单元(提供
bar和baz)
- 主模块接口单元 (
- 外部只看得到主接口和接口分区,内部实现不可被外部直接导入
6. CMake + FILE_SET + 模块的优势
- 将模块文件作为 FILE_SET 类型 CXX_MODULES 添加到库目标
- Ninja + CMake 能自动:
- 扫描依赖
- 排序编译顺序
- 并行编译
- 支持 MSVC 和 GCC(带补丁)
- 可管理大型模块库,保证正确依赖关系
总结口诀
module unit = TU里有module
named module = 同名模块集合
interface unit = export module
implementation unit = 非export模块
partition = module内部划分单元
CMake FILE_SET + ninja dyndep自动处理依赖
CMake + C++20 模块 (STD example & Named Modules) 内容整理成理解,重点讲清楚核心概念、问题和解决方案。
1. 标准示例:CMake FILE_SET + 模块
初始写法(有问题)
add_library(std_module_example)
target_link_libraries(std_module_example t3lib)
target_sources(std_module_example
PUBLIC
FILE_SET cxx_modules TYPE CXX_MODULES FILES
t1.cxx t2.cxx t3.cxx t4.cxx
)
add_executable(main main.cxx)
target_link_libraries(main std_module_example)
- 问题:
t4.cxx并没有导出任何模块,但被放在CXX_MODULES文件集中 - GCC 构建失败:
CMake Error: Output t4.cxx.o is of type `CXX_MODULES` but does not provide a module
修正方法
把非模块文件单独处理:
add_library(std_module_example)
target_link_libraries(std_module_example t3lib)
target_sources(std_module_example
PRIVATE
t4.cxx # 普通源文件
PUBLIC
FILE_SET cxx_modules TYPE CXX_MODULES FILES
t1.cxx t2.cxx t3.cxx
)
- 规则:FILE_SET cxx_modules 只能包含模块文件
- 普通
.cpp/.cxx文件不能放进去
2. MSVC 内部模块修复(VS Dev17.3 Preview)
- CL.exe 现在正确输出依赖信息
/scanDependencies有 ISO 标准模式和 MS 扩展模式- 输出 JSON 中增加
is-interface字段 - 可以在 CMake 中集成,只在 MSVC 扩展模式下需要标记源文件
3. 模块拆分跨库
示例:将 t3.cxx 拆到单独库 t3lib
add_library(t3lib)
target_sources(t3lib
PUBLIC
FILE_SET cxx_modules_internals TYPE CXX_MODULE FILES
t3.cxx
)
add_library(std_module_example)
target_link_libraries(std_module_example t3lib)
target_sources(std_module_example
PRIVATE t4.cxx
PUBLIC FILE_SET cxx_modules TYPE CXX_MODULES FILES t1.cxx t2.cxx
)
add_executable(main main.cxx)
target_link_libraries(main std_module_example)
- 好处:
- 可以把模块内部实现单独放库
- 主库只暴露模块接口和其他模块文件
4. Named Modules 示例
模块结构:
- mymodule.cpp:模块接口单元,导出
mod_f,导入内部 partition - mymodule_impl.cpp:模块实现单元,实现
mod_g和part_g - mymodule_part.cpp:模块分区,导出
part_f、part_g、part_g_impl - mymodule_part_impl.cpp:实现分区函数
part_g_impl - mymodule_part_internal.cpp:内部 partition,不导出,只模块内部使用
main.cpp导入模块使用:
import MyModule;
int main() {
mod_f();
mod_g();
part_f();
part_g();
part_g_impl();
return 0;
}
CMake 配置:
add_library(with_named_modules)
target_sources(with_named_modules
PRIVATE mymodule_impl.cpp mymodule_part_impl.cpp main.cpp
PUBLIC FILE_SET cxx_modules TYPE CXX_MODULES FILES
mymodule_part.cpp
mymodule.cpp
mymodule_part_internal.cpp
)
5. 安装和导出
install(TARGETS with_named_modules
EXPORT with_named_modules
RUNTIME DESTINATION "bin" COMPONENT "runtime"
ARCHIVE DESTINATION "lib" COMPONENT "development"
LIBRARY DESTINATION "lib" COMPONENT "runtime"
FILE_SET cxx_modules DESTINATION "include/cxx/cxx-modules-examples" COMPONENT "development"
FILE_SET cxx_modules_internals DESTINATION "include/cxx/cxx-modules-examples" COMPONENT "development"
CXX_MODULES_BMI DESTINATION "lib/${CMAKE_CXX_COMPILER_ID}-${CMAKE_CXX_COMPILER_VERSION}/$<CONFIG>" COMPONENT "development"
)
export(EXPORT with_named_modules
NAMESPACE NS::
FILE "${CMAKE_BINARY_DIR}/lib/cmake/cxx-modules-examples/with_named_modules-targets.cmake"
CXX_MODULES_DIRECTORY "with_named_modules-cxx-modules"
)
- 会安装模块源文件、BMI 文件、CMake Targets
- Windows 和 Linux/GCC 都可执行
6. CMake 开启实验模块支持
GCC
set(CMAKE_EXPERIMENTAL_CXX_MODULE_CMAKE_API "uuid")
set(CMAKE_EXPERIMENTAL_CXX_MODULE_DYNDEP 1)
set(CMAKE_CXX_STANDARD 20)
include(gcc_modules.cmake)
MSVC
set(CMAKE_EXPERIMENTAL_CXX_MODULE_CMAKE_API "uuid")
set(CMAKE_EXPERIMENTAL_CXX_MODULE_DYNDEP 1)
set(CMAKE_CXX_STANDARD 20)
include(msvc_modules.cmake)
- 通过
cmake_ninja_dyndep或 MSVC/scanDependencies扫描模块依赖 - 支持 FILE_SET 类型模块文件
7. 当前状态
- CMake 对模块 仍是实验性支持
- MSVC 2022 Preview + patched GCC 可以使用 p1689r5 依赖扫描
- Clang 暂未支持完整依赖扫描
- Header Units(头文件单元)支持有限
- 可以跨库拆分模块,并自动处理依赖顺序和 BMI 文件
核心原则总结
FILE_SET cxx_modules只能放 模块文件- 非模块源文件必须单独
PRIVATE/PUBLIC - 模块拆分可分库管理(接口库 + 内部库)
- CMake experimental API + dyndep 扫描依赖
- 安装模块时同时安装源文件和 BMI 文件
更多推荐



所有评论(0)