CMake构建C++20 Module实例(使用MSVC)
提醒: 本文中的例子是在 MSVC(Microsoft Visual Studio 2022 Preview)编译环境上面测试通过的, 截止文章更新时, 没有在 Clang/GCC 上面验证通过.
背景
在传统的 C++ 编译过程中, 代码的构建通常分为三个主要步骤:
- 预处理: 处理 #include 等指令, 将头文件内容展开并插入到代码中.
- 编译: 将预处理后的代码转换为机器可以理解的目标文件.
- 链接: 将目标文件链接成完整的可执行程序.
这种流程对于小型项目非常高效, 但随着项目规模的扩大, 其局限性逐渐显现. 比如:
- 冗长的预处理: 头文件中的嵌套依赖会导致编译器在每次编译时都需要重复展开大量文件, 极大地浪费了时间和资源.
- 容易发生宏冲突: 宏仅仅是文本替换, 缺乏 C++ 的语义支持. 当不同的头文件中存在相同的宏定义时, 极易导致难以调试的问题.
- 头文件管理复杂: 开发者不得不通过 #pragma once 或头文件保护机制来避免重复定义问题, 这无形中增加了代码维护的复杂性.
为了解决这些问题, C++20 引入了**模块(Module)**特性, 这是一种替代传统头文件的新方案. 模块具备以下显著优势:
- 高效编译: 模块只会被编译一次, 避免了头文件的重复展开.
- 防止冲突: 模块内容被隔离, 不同模块间的名称不会互相干扰.
- 清晰组织: 模块明确区分哪些内容是导出的, 哪些仅在模块内部使用, 从而更好地表达代码逻辑.
通过模块, C++ 不仅提升了编译效率, 还为大型项目提供了更现代化的代码组织方式. 在本文中, 我们将通过多个实例, 带你逐步掌握 C++20 模块的使用方法, 帮助你从传统的头文件世界迈入模块化的新时代.
编译器对模块的支持
目前 MSVC 对 C++20 模块的支持最完善, 而 GCC 和 Clang 尚处于完善阶段, 在实际开发中需要格外注意平台间的兼容性.
本文构建环境
本文中的例子均在 MSVC (Microsoft Visual Studio 2022 Preview) 上测试通过. 尚未在 Clang 和 GCC 上全面验证, 请根据实际需要选择合适的编译器和工具链.
源码链接.
模块基础
模块声明
export
命令
有三种方式使用export
:
导出声明
导出组
导出命名空间
简单示例
模块文件 simple.ixx
export module simple;
是一个模块声明, 它声明了一个模块, 模块名为simple
.
在模块中, 我们可以使用export
关键字导出函数, 命名空间, 类, 变量等.
这样的实体可被其他模块导入.
主程序 simple_main.cpp
在使用的时候, 使用import simple;
导入模块.
CMakeList.txt
复杂示例
模块文件
请注意如果模块需要引入传统头文件, 需要使用如下写法.
程序主文件
cmake 设置
模块接口
当模块变得越来越大时, 可能需要把模块的接口和实现分开. 这样可以更好地组织代码, 并且提高代码的可读性.
模块接口单元
模块实现单元
主程序
cmake 设置如下:
子模块
对于大的模块可以将其分割为多个子模块. 然后设置一个主模块文件, 导入并导出子模块.
这里的每一个子模块都是一个独立的模块, 可以单独存在.
模块主文件 sort.ixx
设想一下我们目前有一个排序算法库, 包含几种不同的排序算法, 每种排序算法都可以独立使用.
子模块文件
程序主文件
cmake 设置
模块分区
模块分区与子模块类似, 唯一的区别是分区模块不能作为一个独立模块存在.
模块主文件
子分区文件
cmake 设置
总结
模块是 C++20 的新特性, 它可以解决头文件的低效问题. 导入模块的代价非常低, 并且导入顺序不重要. 模块还可以解决名称冲突问题.
模块由模块接口单元和模块实现单元组成. 必须有一个导出模块声明的模块接口单元, 以及任意多个模块实现单元. 在模块接口中没有导出的名称具有模块链接, 不能在模块外部使用.
模块可以有头文件, 也可以导入和重新导出其它模块.
C++20 标准库没有模块化. 使用 C++20 构建模块化的软件系统是一项具有挑战性的任务.
为了构建大型软件系统, 模块提供了两种方法: 子模块和模块分区. 与分区不同, 子模块可以独立存在.
由于头文件单元的存在, 可以用导入语句替换包含语句, 编译器会自动生成一个模块.
参考链接
Tags: