提醒: 本文中的例子是在 MSVC(Microsoft Visual Studio 2022 Preview)编译环境上面测试通过的, 截止文章更新时, 没有在 Clang/GCC 上面验证通过全部case.
本文所有的源代码以及工程配置均可下载.
因为个人精力有限, 后续更新主要会在个人博客: CMake构建C++20 Module实例
背景
在传统的 C++ 编译过程中, 代码的构建通常分为三个主要步骤:
- 预处理: 处理 #include 等指令, 将头文件内容展开并插入到代码中.
- 编译: 将预处理后的代码转换为机器可以理解的目标文件.
- 链接: 将目标文件链接成完整的可执行程序.
这种流程对于小型项目非常高效, 但随着项目规模的扩大, 其局限性逐渐显现. 比如:
- 冗长的预处理: 头文件中的嵌套依赖会导致编译器在每次编译时都需要重复展开大量文件, 极大地浪费了时间和资源.
- 容易发生宏冲突: 宏仅仅是文本替换, 缺乏 C++ 的语义支持. 当不同的头文件中存在相同的宏定义时, 极易导致难以调试的问题.
- 头文件管理复杂: 开发者不得不通过 #pragma once 或头文件保护机制来避免重复定义问题, 这无形中增加了代码维护的复杂性.
为了解决这些问题, C++20 引入了**模块(Module)**特性, 这是一种替代传统头文件的新方案. 模块具备以下显著优势:
- 高效编译: 模块只会被编译一次, 避免了头文件的重复展开.
- 防止冲突: 模块内容被隔离, 不同模块间的名称不会互相干扰.
- 清晰组织: 模块明确区分哪些内容是导出的, 哪些仅在模块内部使用, 从而更好地表达代码逻辑.
通过模块, C++ 不仅提升了编译效率, 还为大型项目提供了更现代化的代码组织方式. 在本文中, 我们将通过多个实例, 带你逐步掌握 C++20 模块的使用方法, 帮助你从传统的头文件世界迈入模块化的新时代.
编译器对模块的支持
工具 | 最低版本要求 |
---|---|
CMake | 3.28 |
GCC | 14.0 |
Clang/LLVM | 16.0 |
MSVC | 2022 (cl17.4) |
目前 MSVC 对 C++20 模块的支持最完善, 而 GCC 和 Clang 尚处于完善阶段, 在实际开发中需要格外注意平台间的兼容性.
本文构建环境
本文中的例子均在 MSVC (Microsoft Visual Studio 2022 Preview) 上测试通过.
尚未在 Clang 和 GCC 上全面验证, 请根据实际需要选择合适的编译器和工具链.
样例 | GCC 14.2 | Clang 18 | MSVC 2022 |
---|---|---|---|
简单示例 | ✔ | ✔ | ✔ |
复杂示例 | ✘ | ✔ | ✔ |
模块接口 | ✘ | ✘ | ✔ |
子模块 | ✘ | ✔ | ✔ |
模块分区 | ✘ | ✔ | ✔ |
源码链接.
模块基础
模块声明
// global module fragment
module;
// 传统头文件, 比如
#include <vector>
#include <string>
// module declaration; starts the module purview
export module module_name;
// 导入其他模块
import std.core;
// 本模块可见的变量, 函数, 类等
int local_var = 42;
// 导出的命名空间
export namespace sample {
// 导出的名字
export constexpr int foo = 42;
export void bar();
export class Baz{};
} // namespace sample
export
命令
有三种方式使用export
:
导出声明
export module name;
export int foo(int fir, int sec);
export void bar();
导出组
export module name;
export {
int foo(int fir, int sec);
void bar();
}
导出命名空间
export module name;
export namespace name {
int foo(int fir, int sec);
void bar();
} // namespace name
简单示例
模块文件 simple.ixx
export module simple; // 模块声明
// 该函数将被导出
export int add(int a, int b) { return a + b; }
export module simple;
是一个模块声明, 它声明了一个模块, 模块名为simple
.
在模块中, 我们可以使用export
关键字导出函数, 命名空间, 类, 变量等.
这样的实体可被其他模块导入.
主程序 simple_main.cpp
在使用的时候, 使用import simple;
导入模块.
import simple; // 导入模块
#include <iostream>
int main() {
std::cout << "simple add(1, 2)=" << add(1, 2) << "\n";
return 0;
}
CMakeList.txt
add_library(simple_module)
target_sources(simple_module
PUBLIC
FILE_SET CXX_MODULES FILES
simple.ixx
)
add_executable(simple_demo simple_main.cpp)
target_link_libraries(simple_demo simple_module)
复杂示例
模块文件
请注意如果模块需要引入传统头文件, 需要使用如下写法.
module;
#include <string>
#include <utility>
export module complex;
export namespace complex {
class Person {
public:
Person(unsigned age, std::string name) : age_(age), name_(std::move(name)) {}
[[nodiscard]] std::string Name() const { return name_; }
[[nodiscard]] unsigned Age() const { return age_; }
private:
unsigned age_;
std::string name_;
};
} // namespace complex
程序主文件
import complex;
#include <iostream>
int main() {
complex::Person joy(18, "Joy");
std::cout << "Name: " << joy.Name() << "\n";
std::cout << "Age : " << joy.Age() << "\n";
return 0;
}
cmake 设置
add_library(complex_module)
target_sources(complex_module
PUBLIC
FILE_SET CXX_MODULES FILES
complex.ixx
)
add_executable(complex_demo complex_main.cpp)
target_link_libraries(complex_demo complex_module)
模块接口
当模块变得越来越大时, 可能需要把模块的接口和实现分开. 这样可以更好地组织代码, 并且提高代码的可读性.
模块接口单元
module;
#include <vector>
export module demo;
export namespace demo {
int sum(const std::vector<int>& v);
int prod(const std::vector<int>& v);
} // namespace demo
模块实现单元
module demo;
#include <numeric>
#include <vector>
namespace demo {
int sum(const std::vector<int>& v) {
return std::accumulate(v.begin(), v.end(), 1, std::plus{});
}
int prod(const std::vector<int>& v) {
return std::accumulate(v.begin(), v.end(), 1, std::multiplies{});
}
} // namespace demo
主程序
import demo;
#include <iostream>
#include <vector>
int main() {
std::vector vec{1, 2, 3, 4, 5};
std::cout << "sum : " << demo::sum(vec) << std::endl;
std::cout << "prod : " << demo::prod(vec) << std::endl;
}
cmake 设置如下:
add_library(separate_module)
target_sources(separate_module
PUBLIC
FILE_SET CXX_MODULES FILES
separate_interface.ixx
)
target_sources(separate_module PUBLIC separate_impl.ixx)
add_executable(separate separate_main.cpp)
target_link_libraries(separate separate_module)
子模块
对于大的模块可以将其分割为多个子模块. 然后设置一个主模块文件, 导入并导出子模块.
这里的每一个子模块都是一个独立的模块, 可以单独存在.
模块主文件 sort.ixx
设想一下我们目前有一个排序算法库, 包含几种不同的排序算法, 每种排序算法都可以独立使用.
module;
export module sort;
export import sort.bubble_sort;
export import sort.insert;
export import sort.quick;
子模块文件
module;
#include <utility>
#include <vector>
export module sort.bubble_sort;
export namespace sort {
void bubble_sort(std::vector<int>& arr) {
if (arr.size() < 2) return;
for (int i = 0; i < arr.size() - 1; i++) {
auto swapped = false;
for (int j = 1; j < arr.size() - i; j++) {
if (arr[j - 1] > arr[j]) {
std::swap(arr[j - 1], arr[j]);
swapped = true;
}
}
if (!swapped) break;
}
}
} // namespace sort
module;
#include <vector>
export module sort.insert;
export namespace sort {
void insertion_sort(std::vector<int>& arr) {
for (int i = 1; i < arr.size(); i++) {
auto k = arr[i];
int j = i - 1;
while (j >= 0 && arr[j] > k) {
arr[j + 1] = arr[j];
j--;
}
arr[j + 1] = k;
}
}
} // namespace sort
程序主文件
import sort;
#include <algorithm>
#include <iostream>
#include <vector>
int main() {
std::vector v{5, 4, 3, 2, 1};
sort::bubble_sort(v);
std::cout << std::boolalpha << std::is_sorted(v.begin(), v.end(), std::less{})
<< std::endl;
}
cmake 设置
add_library(sort_module)
target_sources(sort_module
PUBLIC
FILE_SET CXX_MODULES FILES
sort.ixx
sort_bubble.ixx
sort_quick.ixx
sort_insert.ixx
)
add_executable(sort_module_demo sort_main.cpp)
target_link_libraries(sort_module_demo sort_module)
模块分区
模块分区与子模块类似, 唯一的区别是分区模块不能作为一个独立模块存在.
模块主文件
module;
export module partition;
export import :part1;
export import :part2;
子分区文件
module;
#include <utility>
#include <vector>
export module partition:part1;
export namespace sort {
void bubble_sort(std::vector<int>& arr) {
if (arr.size() < 2) return;
for (int i = 0; i < arr.size() - 1; i++) {
auto swapped = false;
for (int j = 1; j < arr.size() - i; j++) {
if (arr[j - 1] > arr[j]) {
std::swap(arr[j - 1], arr[j]);
swapped = true;
}
}
if (!swapped) break;
}
}
} // namespace sort
module;
#include <vector>
export module partition:part2;
export namespace sort {
void insertion_sort(std::vector<int>& arr) {
for (int i = 1; i < arr.size(); i++) {
auto k = arr[i];
int j = i - 1;
while (j >= 0 && arr[j] > k) {
arr[j + 1] = arr[j];
j--;
}
arr[j + 1] = k;
}
}
} // namespace sort
cmake 设置
add_library(partition_module)
target_sources(partition_module
PUBLIC
FILE_SET CXX_MODULES FILES
partition.ixx
partition_part1.ixx
partition_part2.ixx
)
add_executable(partition_module_demo partition_main.cpp)
target_link_libraries(partition_module_demo partition_module)
总结
-
模块是 C++20 的新特性, 它可以解决头文件的低效问题. 导入模块的代价非常低, 并且导入顺序不重要.
模块还可以解决名称冲突问题. -
模块由模块接口单元和模块实现单元组成. 必须有一个导出模块声明的模块接口单元, 以及任意多个模块实现单元.
在模块接口中没有导出的名称具有模块链接, 不能在模块外部使用. -
模块可以有头文件, 也可以导入和重新导出其它模块.
-
C++20 标准库没有模块化. 使用 C++20 构建模块化的软件系统是一项具有挑战性的任务.
-
为了构建大型软件系统, 模块提供了两种方法: 子模块和模块分区. 与分区不同, 子模块可以独立存在.
-
由于头文件单元的存在, 可以用导入语句替换包含语句, 编译器会自动生成一个模块.