究竟什么是C ++模块?

时间:2014-03-27 16:42:44

标签: c++

我一直在跟进C ++标准化,并且遇到了C ++模块的想法。我找不到一篇好文章。究竟是什么?

4 个答案:

答案 0 :(得分:28)

动机

一个简单的答案是C ++模块就像一个 header ,它也是一个翻译单元。就像标题一样,您可以使用它(与import,这是一个新的上下文关键字)来访问库中的声明。由于它是一个翻译单元(对于一个复杂的模块,它是多个翻译单元),因此它被单独编译,并且只能编译一次。 (回想一下#include从字面上将文件的内容复制到包含指令的翻译单元中。)这种组合产生了许多优点:

  1. 隔离:由于模块单元是一个单独的翻译单元,因此它具有自己的宏集和using声明/指令,它们既不会影响导入的翻译,也不会受到导入翻译的影响单元或任何其他模块。这样可以防止一个标头中的标识符#define d与另一个标头中使用的标识符之间发生冲突。尽管using的使用仍然应该是明智的,但即使在模块接口的名称空间范围内甚至写入using namespace也不会对本质上有害。
  2. 接口控制:因为模块单元可以声明具有内部链接的实体(使用staticnamespace {}),使用export(用于目的而保留的关键字) (例如C ++ 98以来的版本),或两者都不用,它可以限制客户端可以使用多少内容。这取代了namespace detail惯用语,该惯用语可能在标头(在相同的包含名称空间中使用标头)之间发生冲突。
  3. 重复数据删除:因为在许多情况下,不再需要在头文件中提供声明,而在单独的源文件中提供定义,因此减少了冗余和相关的差异机会。
  4. 避免违反一个定义规则:之所以存在ODR,完全是因为需要在每个翻译单元中定义某些实体(类型,内联函数/变量和模板)使用它们。一个模块只能定义一个实体,但是仍然向客户端提供定义。此外,当已经通过内部链接声明违反ODR的现有标头转换为模块时,它们也将不再格式错误,无需进行诊断。
  5. 非局部变量初始化顺序:由于import在包含(唯一)变量 definitions 的翻译单元之间建立了依赖顺序,因此有一个明显的顺序其中initialize non-local variables with static storage duration。 C ++ 17为inline变量提供了可控制的初始化顺序;模块将其扩展为普通变量(根本不需要inline变量)。
  6. 模块私有声明:模块中声明的既不导出也不具有内部链接的实体可由模块中的任何翻译单元使用(按名称),从而为预先存在的实体提供了有用的中间立场是否选择static。尽管有待确切地了解这些实现将如何实现,但它们与动态对象中“隐藏”(或“未导出”)符号的概念紧密对应,为这种实际的动态链接优化提供了潜在的语言识别。 / li>
  7. ABI稳定性:已经调整了inline(其ODR兼容性目的与模块无关)的规则,以支持(但不要求!)实施策略。 -inline函数可以用作共享库升级的ABI边界。
  8. 编译速度:由于模块的内容不需要作为使用它们的每个翻译单元的一部分进行重新解析,因此在许多情况下,编译的速度要快得多。
  9. 工具:涉及importmodule的“结构声明”在使用上受到限制,以使它们易于被需要了解依赖关系图的工具检测到一个项目。这些限制还允许大多数(如果不是全部)将这些常用词用作标识符。

方法

由于必须在客户端中找到模块中声明的名称,因此需要一种重要的新型名称查找,该名称可跨翻译单元使用;为参数依赖的查找和模板实例化获取正确的规则,是使该提议花十年时间进行标准化的重要组成部分。简单的规则是(除了出于明显的原因与内部链接不兼容之外)export影响仅 的名称查找;通过(例如decltype或模板参数可用的任何实体,无论是否导出,都具有完全相同的行为。

因为模块必须能够以允许其 contents 使用的方式为其客户端提供类型,内联函数和模板,所以通常,编译器在处理模块时会生成工件(有时称为编译模块接口),其中包含客户端所需的详细信息。 CMI与预编译头文件类似,但没有限制,每个相关翻译单元必须以相同顺序包含相同的头文件。尽管与从模块仅导入特定名称的功能没有类似之处,但这也与Fortran模块的行为类似。

由于编译器必须能够基于import foo;查找CMI(并基于import :partition;查找源文件),因此它必须知道从“ foo”到(CMI)文件名的某些映射。 Clang为此概念建立了术语“模块图”。通常,如何处理模块(或分区)名称与源文件名称和隐式目录结构不匹配的情况还有待观察。

非功能

与其他“二进制标头”技术一样,模块不应被视为“ 分发机制” (就像一个秘密弯腰的人可能希望避免提供标头和所有包含的所有定义)模板)。尽管编译器可以使用模块为每个项目重新生成CMI,但它们在传统意义上也不是“仅标头”。

虽然在许多其他语言(例如, ,Python)中,模块既是编译单元又是命名单元,而C ++模块是非命名空间。 C ++已经有了名称空间,并且模块的用法和行为都没有改变(部分是为了向后兼容)。但是,可以预期的是,模块名称通常会与名称空间名称对齐,尤其是对于具有众所周知的名称空间名称的库,这些名称库可能会与其他模块的名称混淆。 (nested::name可以表示为模块名称nested.name,因为那里不允许.而不是::.在C ++中没有意义20,除非另有约定。)

模块也不会使pImpl idiom过时或阻止fragile base class problem。如果某个客户端的类已完成,则更改该类仍然需要重新编译客户端。

最后,模块不提供提供的机制,而宏是某些库的界面的重要组成部分;可以提供一个看起来像

的包装标头
// wants_macros.hpp
import wants.macros;
#define INTERFACE_MACRO(x) (wants::f(x),wants::g(x))

(除非您可能不需要#include防护,除非同一宏可能有其他定义。)

多文件模块

一个模块具有一个包含export module A;的单个主接口单元:这是编译器处理的转换单元,用于生成客户端所需的数据。它可能会招募其他包含export module A:sub1;接口分区;这些是单独的翻译单元,但包含在该模块的一个CMI中。也可以具有实现分区module A:impl1;),该接口可以通过接口导入,而无需将其内容提供给整个模块的客户端。 (出于某些技术原因,某些实现可能会将这些内容泄漏给客户端,但这绝不会影响名称查找。)

最后,(非分区)模块实现单元(仅带有module A;)根本不向客户端提供任何内容,但是可以定义在模块接口中声明的实体(它们隐式导入) )。一个模块的所有翻译单元都可以使用在其导入的同一模块的另一部分中声明的任何内容,只要它没有内部链接(换句话说,它们忽略export)即可。

在特殊情况下,单文件模块可以包含module :private;声明,该声明可以有效地将实现单元与接口打包在一起;这称为私有模块片段。特别是,它可以用于定义一个类,同时在客户端中将其保留为不完整(提供二进制兼容性,但不会阻止使用典型的构建工具进行重新编译)。

升级

将基于标头的库转换为模块既不是琐事也不是艰巨的任务。所需的样板非常小(在许多情况下为两行),并且可以将export {}放在文件的相对较大的部分周围(尽管有不幸的限制:不能使用static_assert声明或推导指南)附上)。通常,namespace detail {}可以转换为namespace {},也可以不输出。在后一种情况下,其内容通常可以移动到包含名称空间。如果希望甚至ABI保守的实现也可以内联来自其他翻译单元的调用,则必须将类成员显式标记为inline

当然,并不是所有的库都可以即时升级。向后兼容性一直是C ++的重点之一,并且有两种独立的机制可以使基于模块的库依赖(基于最初的实验实现提供的库)。 (从另一个方向来说,标头可以像其他任何东西一样简单地使用import,即使模块以任何一种方式使用了它。)

如“模块技术规范”中所述,全局模块片段可能会出现在仅包含预处理程序指令的模块单元的开头(由裸module;引入):特别是, #include个模块所依赖的标头。在大多数情况下,可以实例化一个模块中定义的模板,该模板使用包含在其标头中的声明,因为这些声明已合并到CMI中。

还可以选择导入“模块化”(或 importable )标头(import "foo.hpp";):导入的是合成的标头单元就像模块一样,除了它导出其声明的所有内容—甚至包括具有内部链接(可能无法在标头外部使用)和宏的东西。 (使用由不同的导入的标头单元赋予不同值的宏是错误的;不考虑使用命令行宏(-D)。)非正式地,标头是模块化的,如果只包含一次,则不包含定义的特殊宏足以使用它(而不是像是带有标记粘贴的模板的C实现)。如果实现知道标题是可导入的,则可以将其#include自动替换为import

在C ++ 20中,标准库仍以标头形式出现;所有C ++头文件(但不是C头文件或<cmeow>包装器)均指定为可导入。 C ++ 23可能还会提供命名模块(尽管每个标头可能都不提供)。

示例

一个非常简单的模块可能是

export module simple;
import <string>;
import <memory>;
using std::unique_ptr;  // not exported
int *parse(const std::string&) {/*…*/}  // cannot collide with other modules
export namespace simple {
  auto get_ints(const char *text)
  {return unique_ptr<int[]>(parse(text));}
}

可以用作

import simple;
int main() {
  return simple::get_ints("1 1 2 3 5 8")[0]-1;
}

结论

模块有望通过多种方式改善C ++编程,但是这些改进是渐进的,(实际上)是渐进的。委员会强烈反对将模块设置为“new language” eg ,它更改有符号和无符号整数之间的比较规则)的想法,因为这将使转换现有代码和转换代码变得更加困难。在模块文件和非模块文件之间移动代码会很危险。

MSVC已经有一段时间(在TS之后)实施了模块。几年来,Clang的实现也非常依赖于可导入的标头。在撰写本文时,GCC的实施方式还很有限,但是它基于最终被接受的提案。

最后,请注意,C ++ 20至少还要再审查几个月,并且可能会在最后一刻进行一些更改。 (实际上,此处描述的某些行为实际上并未出现在任何草稿中,而只会出现在C ++ 20的最终版本中。)

答案 1 :(得分:4)

答案 2 :(得分:2)

C ++模块是允许编译器使用&#34;语义导入&#34;而不是旧的文本包含模型。找到#include预处理程序指令时,它们不会执行复制和粘贴,而是读取包含表示代码的抽象语法树序列化的二进制文件。

这些语义导入避免了多次重新编译头文件中包含的代码,从而加快了编译速度。例如。如果你的项目包含100 #include<iostream>,在不同的.cpp文件中,每个语言配置只会解析一次标题,而不是每个使用该模块的翻译单元一次。

Microsoft的提案超出了此范围,并引入了internal关键字。具有internal可见性的类的成员将不会在模块外部看到,从而允许类实现者隐藏类中的实现细节。 http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2015/n4465.pdf

我在博客中使用<iostream>编写了一个使用LLVM模块缓存的小例子: https://cppisland.wordpress.com/2015/09/13/6/

答案 3 :(得分:1)

请看一下我喜欢的这个简单示例。那里的模块真的很好解释。本文使用简单的术语和出色的示例来研究问题的各个方面。

https://www.modernescpp.com/index.php/c-20-modules