C++20 四大特性之一:Module 特性详解
liuian 2025-07-07 20:09 119 浏览
C++20 最大的特性是什么?
最大的特性是迄今为止没有哪一款编译器完全实现了所有特性。
文章来源:网易云信
有人认为 C++20 是 C++11 以来最大的一次改动,甚至比 C++11 还要大。本文仅介绍 C++20 四大特性当中的 Module 部分,分为三部分:
探究 C++ 编译链接模型的由来以及利弊
介绍 C++20 Module 机制的使用姿势
总结 Module 背后的机制、利弊、以及各大编译器的支持情况
C++ 是兼容 C 的,不但兼容了 C 的语法,也兼容了 C 的编译链接模型。1973年初,C 语言基本定型:有了预处理、支持结构体;编译模型也基本定型为:预处理、编译、汇编、链接四个步骤并沿用至今;1973年,K&R 二人使用 C 语言重写了 Unix 内核。
为何要有预处理?为何要有头文件?在 C 诞生的年代,用来跑 C 编译器的计算机 PDP-11 的硬件配置是这样的:内存:64 KiB 硬盘:512 KiB。编译器无法把较大的源码文件放入狭小的内存,故当时 C 编译器的设计目标是能够支持模块化编译,即将源码分成多个源码文件、挨个编译,以生成多个目标文件,最后整合(链接)成一个可执行文件。
C 编译器分别编译多个源码文件的过程,实际上是一个 One pass compile 的过程,即:从头到尾扫描一遍源码、边扫描边生成目标文件、过眼即忘(以源码文件为单位)、后面的代码不会影响编译器前面的决策,该特性导致了 C 语言的以下特征:
结构体必须先定义再使用,否则无法知道成员的类型以及偏移,就无法生成目标代码。
局部变量先定义再使用,否则无法知道变量的类型以及在栈中的位置,且为了方便编译器管理栈空间,局部变量必须定义在语句块的开始处。
外部变量只需要知道类型、名字(二者合起来便是声明)即可使用(生成目标代码),外部变量的实际地址由连接器填写。
外部函数只需知道函数名、返回值、参数类型列表(函数声明)即可生成调用函数的目标代码,函数的实际地址由连接器填写。
头文件和预处理恰好满足了上述要求,头文件只需用少量的代码,声明好函数原型、结构体等信息,编译时将头文件展开到实现文件中,编译器即可完美执行 One pass comlile 过程了。
至此,我们看到的都是头文件的必要性和益处,当然,头文件也有很多负面影响:
低效:头文件的本职工作是提供前置声明,而提供前置声明的方式采用了文本拷贝,文本拷贝过程不带有语法分析,会一股脑将需要的、不需要的声明全部拷贝到源文件中。
传递性:最底层的头文件中宏、变量等实体的可见性,可以通过中间头文件“透传”给最上层的头文件,这种透传会带来很多麻烦。
降低编译速度:加入 a.h 被三个模块包含,则 a 会被展开三次、编译三次。
顺序相关:程序的行为受头文件的包含顺影响,也受是否包含某一个头文件影响,在 C++ 中尤为严重(重载)。
不确定性:同一个头文件在不同的源文件中可能表现出不同的行为,导致这些不同的原因,可能源自源文件(比如该源文件包含的其他头文件、该源文件中定义的宏等),也可能源自编译选项。
C++20 中加入了 Module,我们先看 Module 的基本使用姿势,最后再总结 Module 比 头文件的优势。
Module(即模块)避免了传统头文件机制的诸多缺点,一个 Module 是一个独立的翻译单元,包含一个到多个 module interface file(即模块接口文件),包含 0 个到多个 module implementation file(即模块实现文件),使用 Import 关键字即可导入一个模块、使用这个模块暴露的方法。
实现一个最简单的 Module
module_hello.cppm:定义一个完整的hello模块,并导出一个 say_hello_to 方法给外部使用。当前各编译器并未规定模块接口文件的后缀,本文统一使用 ".cppm" 后缀名。".cppm" 文件有一个专用名称"模块接口文件",值得注意的是,该文件不光可以声明实体,也可定义实体。
main 函数中可以直接使用 hello 模块:
编译脚本如下,需要先编译 module_hello.cppm 生成一个 pcm 文件(Module 缓存文件),该文件包含了 hello 模块导出的符号。
以上代码有以下细节需要注意:
module hello:声明了一个模块,前面加一个 export,则意味着当前文件是一个模块接口文件(module interface file),只有在模块接口文件中可以导出实体(变量、函数、类、namespace等)。一个模块至少有一个模块接口文件、模块接口文件可以只放实体声明,也可以放实体定义。
import hello:不需加尖括号,且不同于 include,import 后跟的不是文件名,而是模块名(文件名为 module_hello.cpp),编译器并未强制模块名必须与文件名一致。
想要导出一个函数,在函数定义/声明前加一个 export 关键字即可。
Import 的模块不具有传递性。hello 模块包含了 string_view,但是 main 函数在使用 hello 模块前,依然需要再 import ; 。
模块中的 Import 声明需要放在模块声明之后、模块内部其他实体声明之前,即:import <iostream>; 必须放在 export module hello; 之后,void internal_helper() 之前。
编译时需要先编译基础的模块,再编译上层模块,buildfile.sh 中先将 module_hello 编译生成 pcm,再编译 main。
接口与实现分离
上个示例中,接口的声明与实现都在同一个文件中(.cppm中,准确地说,该文件中只有函数的实现,声明是由编译器自动生成、放到缓存文件pcm中),当模块的规模变大、接口变多之后,将所有的实体定义都放在模块接口文件中会非常不利于代码的维护,C++20 的模块机制还支持接口与实现分离。下面我们将接口的声明与实现分别放到 .cppm 和 .cpp 文件中。
module_hello.cppm:我们假设 say_hello_to、func_a、func_b 等接口十分复杂,.cppm 文件中只包含接口的声明(square 方法是个例外,它是函数模板,只能定义在 .cppm 中,不能分离式编译)。
module_hello.cpp:给出 hello 模块的各个接口声明对应的实现。
代码有几个细节需要注意:
整个 hello 模块分成了 module_hello.cppm 和 module_hello.cpp 两个文件,前者是模块接口文件(module 声明前有 export 关键字),后者是模块实现文件(module implementation file)。当前各大编译器并未规定模块接口文件的后缀必须是 cppm。
模块实现文件中不能 export 任何实体。
函数模板,比如代码中的 square 函数,定义必须放在模块接口文件中,使用 auto 返回值的函数,定义也必须放在模块接口文件。
可见性控制
在模块最开始的例子中,我们就提到了模块的 Import 不具有传递性:main 函数使用 hello 模块的时候必须 import <string_view>,如果想让 hello 模块中的 string_view 模块暴露给使用者,需使用 export import 显式声明:
hello 模块显式导出 string_view 后,main 文件中便无需再包含 string_view 了。
子模块(Submodule)
当模块变得再大一些,仅仅是将模块的接口与实现拆分到两个文件也有点力不从心,模块实现文件会变得非常大,不便于代码的维护。C++20 的模块机制支持子模块。
这次 module_hello.cppm 文件不再定义、声明任何函数,而是仅仅显式导出 hello.sub_a、hello.sub_b 两个子模块,外部需要的方法都由上述两个子模块定义,module_hello.cppm 充当一个“汇总”的角色。
子模块 module hello.sub_a 采用了接口与实现分离的定义方式:“.cppm” 中给出定义,“.cpp” 中给出实现。
module hello.sub_b 同上,不再赘述。
这样,hello 模块的接口和实现文件被拆分到了两个子模块中,每个子模块又有自己的接口文件、实现文件。
值得注意的是,C++20 的子模块是一种“模拟机制”,模块 hello.sub_b 是一个完整的模块,中间的点并不代表语法上的从属关系,不同于函数名、变量名等标识符的命名规则,模块的命名规则中允许点存在于模块名字当中,点只是从逻辑语义上帮助程序员理解模块间的逻辑关系。
Module Partition
除了子模块之外,处理复杂模块的机制还有 Module Partition。Module Partition 一直没想到一个贴切的中文翻译,或者可以翻译为模块分区,下文直接使用 Module Partition。Module Partition 分为两种:
module implementation partition
module interface partition
module implementation partition 可以通俗的理解为:将模块的实现文件拆分成多个。module_hello.cppm 文件:给出模块的声明、导出函数的声明。
模块的一部分实现代码拆分到
module_hello_partition_internal.cpp 文件,该文件实现了一个内部方法 internal_helper。
模块的另一部分实现拆分到 module_hello.cpp 文件,该文件实现了 func_a、func_b,同时引用了内部方法 internal_helper(func_a、func_b 当然也可以拆分到两个 cpp 文件中)。
值得注意的是, 模块内部 Import 一个 module partition 时,不能 import hello:internal;而是直接import :internal; 。
module interface partition 可以理解为模块声明拆分到多个文件中。module implementation partition 的例子中,函数声明只集中在一个文件中,module interface partition 可以将这些声明拆分到多个接口文件。
首先定义一个内部 helper:internal_helper:
hello 模块的 a 部分采用声明+定义合一的方式,定义在
module_hello_partition_a.cppm 中:
hello 模块的 b 部分采用声明+定义分离的方式,
module_hello_partition_b.cppm 只做声明:
module_hello_partition_b.cpp 给出 hello 模块的 b 部分对应的实现:
module_hello.cppm 再次充当了”汇总“的角色,将模块的 a 部分+ b 部分导出给外部使用:
module implementation partition 的使用方式较为直观,相当于我们平时编程中“一个头文件声明多个 cpp 实现”这种情况。module interface partition 有点类似于 submodule 机制,但语法上有较多差异:
module_hello_partition_b.cpp 第一行不能使用 import hello:partition_b;虽然这样看上去更符合直觉,但是不允许。
每个 module partition interface 最终必须被 primary module interface file 导出,不能遗漏。
primary module interface file 不能导出 module implementation file,只能导出 module interface file,故在 module_hello.cppm 中 export :internal; 是错误的。
同样作为处理大模块的机制,Module Partition 与子模块最本质的区别在于:子模块可以独立的被外部使用者 Import,而 Module Partition 只在模块内部可见。
全局模块片段
(Global module fragments)
C++20 之前有大量的不支持模块的代码、头文件,这些代码实际被隐式的当作全局模块片段处理,模块代码与这些片段交互方式如下:
事实上,由于标准库的大多数头文件尚未模块化(VS 模块化了部分头文件),整个第二章的代码在当前编译器环境下(Clang12)是不能直接编译通过的——当前尚不能直接 import < iostream > 等模块,通全局模块段则可以进行方便的过渡(在全局模块片段直接 #include <iostream>),另一个过渡方案便是下一节所介绍的 Module Map——该机制可以使我们能够将旧的 iostream编译成一个 Module。
Module Map
Module Map 机制可以将普通的头文件映射成 Module,进而可以使旧的代码吃到 Module 机制的红利。下面便以 Clang13 中的 Module Map 机制为例:
假设有一个 a.h 头文件,该头文件历史较久,不支持 Module:
通过给 Clang 编译器定义一个 module.modulemap 文件,在该文件中可以将头文件映射成模块:
编译脚本需要依次编译 A、ctype、iostream 三个模块,然后再编译 main 文件:
首先使用 -fmodule-map-file 参数,指定一个 module map file,然后通过 -fmodule 指定 map file 中定义的 module,就可以将头文件编译成 pcm。main 文件使用 A、iostream 等模块时,同样需要使用 fmodule-map-file 参数指定 mdule map 文件,同时使用 -fmodule 指定依赖的模块名称。
注:关于 Module Map 机制能够查到的资料较少,有些细节笔者也未能一一查明,例如:
通过 Module Map 将一个头文件模块化之后,头文件中暴露的宏会如何处理?
假如头文件声明的实体的实现分散在多个 cpp 中,该如何组织编译?
Module 与 Namespace
Module 与 Namespace 是两个维度的概念,在 Module 中同样可以导出 Namespace:
总结
最后,对比最开始提到的头文件的缺点,模块机制有以下几点优势:
无需重复编译:一个模块的所有接口文件、实现文件,作为一个翻译单元,一次编译后生成 pcm,之后遇到 Import 该模块的代码,编译器会从 pcm 中寻找函数声明等信息,该特性会极大加快 C++ 代码的编译速度。
隔离性更好:模块内 Import 的内容,不会泄漏到模块外部,除非显式使用 export Import 声明。
顺序无关:Import 多个模块,无需关心这些模块间的顺序。
减少冗余与不一致:小的模块可以直接在单个 cppm 文件中完成实体的导出、定义,但大的模块依然会把声明、实现拆分到不同文件。
子模块、Module Partition 等机制让大模块、超大模块的组织方式更加灵活。
全局模块段、Module Map 制使得 Module 与老旧的头文件交互成为可能。
缺点也有:
编译器支持不稳定:尚未有编译器完全支持 Module 的所有特性、Clang13 支持的 Module Map 特性不一定保留到主干版本。
编译时需要分析依赖关系、先编译最基础的模块。
现有的 C++ 工程需要重新组织 pipline,且尚未出现自动化的构建系统,需要人工根据依赖关系组构建脚本,实施难度巨大。
Module 不能做什么?
Module 不能实现代码的二进制分发,依然需要通过源码分发 Module。
pcm 文件不能通用,不同编译器的 pcm 文件不能通用,同一编译器不同参数的 pcm 不能通用。
无法自动构建,现阶段需要人工组织构建脚本。
编译器如何实现对外隐藏 Module 内部符号的?
在 Module 机制出现之前,符号的链接性分为外部连接性(external linkage,符号可在文件之间共享)、内部链接性(internal linkage,符号只能在文件内部使用),可以通过 extern、static 等关键字控制一个符号的链接性。
Module 机制引入了模块链接性(module linkage),符号可在整个模块内部共享(一个模块可能存在多个 partition 文件)。
对于模块 export 的符号,编译器根据现有规则(外部连接性)对符号进行名称修饰(name mangling)。
对于 Module 内部的符号,统一在符号名称前面添加 “_Zw” 名称修饰,这样链接器链接时便不会链接到内部符号。
截至2020.7,三大编译器对 Module 机制的支持情况:
以上就是本文的全部内容,关于 C++20 的四大特性我们介绍了其一
写在最后:其实每个人都有自己的选择,学编程,每一种编程语言的存在都有其应用的方向,选择你想从事的方向,去进行合适的选择就对了!对于准备学习编程的小伙伴,如果你想更好的提升你的编程核心能力(内功)不妨从现在开始!
编程学习书籍分享:
编程学习视频分享:
整理分享(多年学习的源码、项目实战视频、项目笔记,基础入门教程)
欢迎转行和学习编程的伙伴,利用更多的资料学习成长比自己琢磨更快哦!
对于C/C++感兴趣可以关注小编在后台私信我:【编程交流】一起来学习哦!可以领取一些C/C++的项目学习视频资料哦!已经设置好了关键词自动回复,自动领取就好了!
相关推荐
- win10自带文件恢复工具(win10文件恢复工具推荐)
-
步骤:第一步:打开系统的管理员命令提示符窗口。Windows10系统打开管理员命令提示符窗口有如下几种方法:方法一:在系统桌面左下角的搜索栏输入:CMD,点击:命令提示符,可以打开管理员命令提示符窗口...
- 电脑本地磁盘c盘满了怎么办(电脑本地磁盘c盘满了如何删除)
-
当您的电脑本地磁盘C满了时,可能会出现一些问题,例如无法安装新程序、无法保存文件等。以下是一些解决方法:1.删除不需要的文件:可以通过手动删除不需要的文件或使用磁盘清理工具来清理本地磁盘C。在清理磁...
- 傲游浏览器(傲游浏览器app下载)
-
1、开始——程序——找到遨游——打开,如果能打开说明快捷方式有问题2、362急救箱系统修复、网络修复傲游浏览器曾经是一个备受推荐的浏览器,由于其强大的功能和用户友好的界面,在中国的浏览器市场占有一...
- 电脑怎么定时关机软件(电脑怎样定时开关机软件)
-
给电脑设置定时开关机的方法如下:1、点击桌面左下角的开始按钮,打开“控制面板”。2、然后我们点击“系统和安全3、点击下方的“管理工具”。4、再点击“任务计划程序”。5、点击“计划任务程序库”,选择“创...
- 网易邮箱企业邮箱登录入口(网易邮箱企业免费邮箱登录)
-
网易企业邮箱官网(qiye.163.com),除此之外所看到的都是经销商网站。现阶段在该官网是可以填写信息直接开通网易企业邮箱体验试用的。如果有不明白的地方需要专人服务也是可以在官网点击在线咨询按钮或...
-
- qq电子邮箱怎么写(电子邮件信箱怎么注册)
-
1.每个人在注册QQ时都会有关联的一个邮箱,它的格式就是“QQ号码@qq.com”。2.用户可以免费开通自己的手机号码邮箱帐号。3.QQ邮箱还可以注册“……@foxmail.com”这样的商务型帐号。4.@qq.com邮箱可以有...
-
2026-01-12 22:05 liuian
- 台式机装机步骤(台式机 装机)
-
原因:1、更新的驱动不正确或未更新完成(使用USB键鼠经常发生);2、电脑更新驱动时假死,导致进程反应过慢。解决方法:1、如更新时驱动不正确,USB键盘、鼠标无作用时;可等待1~2分钟,看键鼠是否恢复...
- win8手机下载安装(win8安卓)
-
在电脑上面就可以下载,打开浏览器搜索windous8系统会出现一些下拉选择,选择第一条或者选择有官网字样的,就直接有下载按钮,然后点击下载就可以了关闭应用自动更新第一步、在系统中找到应用商店。第二...
- 台式电脑显卡怎么升级(台式电脑显卡升级方案)
-
一般情况下,建议到产品(您的显卡)品牌官网上去下载相应最新的驱动,这虽然并不能保证一定就是显卡最新的驱动,但相对于稳定性来说是首选。如果是高级玩家,追求更新、更好的性能发挥,可以利用驱动精灵一类的驱动...
- u盘数据丢失的原因(u盘数据丢失的原因有哪些)
-
U盘出现了损坏造成的磁道出现了损坏。这个U盘的磁道是最容易损坏的,有的时候你不知道怎么碰到它,它就有数据丢失了就无法显示这样的情况,你可以在电脑上进行修复,首先你点击U盘右键找到属性选择修复,这样把...
- window7下载哪个版本的ie(windows7用哪个版本的ie浏览器)
-
WIN7系统自带的IE浏览器是8.0版本的。IE全称InternetExplorer,是美国微软公司推出的一款网页浏览器。IE8扩展的新功能有:1、Activities(活动内容服务)。用户可以从网页...
- 服务器回收(上海服务器回收)
-
回收服务器内存后,首先应该彻底清除内存存储的所有数据和敏感信息,然后进行分类处理。如果内存仍然有效,可以进行检测、测试和修复后再重新使用。如果内存已损坏或过期,应该妥善处理,比如通过专业的硬件回收公司...
- 戴尔官网入口学生通道(戴尔学生渠道)
-
戴尔官网地址如下,在浏览器输入就可以加入了。DELL官方网站http://www.dell.com.cn/DELL官方旗舰店(天猫)http://dell.tmall.com/DELL官方旗舰店(京东...
- win7旗舰版激活码病毒(win7旗舰版激活密钥 永久激活码)
-
激活和破解工具会修改一些系统文件或数据,一般都会被杀毒软件识别为木马。而且现在网上的windows和office激活工具有的确实是带有木马的,最好去值得信任的网站或者论坛下载。
- 一周热门
-
-
飞牛OS入门安装遇到问题,如何解决?
-
如何在 iPhone 和 Android 上恢复已删除的抖音消息
-
Boost高性能并发无锁队列指南:boost::lockfree::queue
-
大模型手册: 保姆级用CherryStudio知识库
-
用什么工具在Win中查看8G大的log文件?
-
如何在 Windows 10 或 11 上通过命令行安装 Node.js 和 NPM
-
威联通NAS安装阿里云盘WebDAV服务并添加到Infuse
-
Trae IDE 如何与 GitHub 无缝对接?
-
idea插件之maven search(工欲善其事,必先利其器)
-
如何修改图片拍摄日期?快速修改图片拍摄日期的6种方法
-
- 最近发表
- 标签列表
-
- python判断字典是否为空 (50)
- crontab每周一执行 (48)
- aes和des区别 (43)
- bash脚本和shell脚本的区别 (35)
- canvas库 (33)
- dataframe筛选满足条件的行 (35)
- gitlab日志 (33)
- lua xpcall (36)
- blob转json (33)
- python判断是否在列表中 (34)
- python html转pdf (36)
- 安装指定版本npm (37)
- idea搜索jar包内容 (33)
- css鼠标悬停出现隐藏的文字 (34)
- linux nacos启动命令 (33)
- gitlab 日志 (36)
- adb pull (37)
- python判断元素在不在列表里 (34)
- python 字典删除元素 (34)
- vscode切换git分支 (35)
- python bytes转16进制 (35)
- grep前后几行 (34)
- hashmap转list (35)
- c++ 字符串查找 (35)
- mysql刷新权限 (34)
