乐趣区

关于c++:C20-四大特性之一Module-特性详解

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 <string_view>;。
  • 模块中的 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 的四大个性咱们介绍了其一,在后续的文章中,咱们也会陆续安顿另外三大(concept、range、coroutine)的解读,也欢送持续关注咱们。文中内容难免会有疏漏与有余,欢送留言与咱们交换。

退出移动版