究竟什么是C++模块?

Ama*_*ani 10 c++ c++20

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

Dav*_*ing 14

动机

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

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

方法

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

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

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

非功能

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

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

模块也不会淘汰pImpl习惯用法或防止脆弱的基类问题。如果某个客户端的类已完成,则更改该类仍需要重新编译该客户端。

最后,当模块是库接口的重要组成部分时,模块不提供提供的机制。可以提供一个看起来像

// wants_macros.hpp
import wants.macros;
#define INTERFACE_MACRO(x) (wants::f(x),wants::g(x))
Run Code Online (Sandbox Code Playgroud)

#include除非同一个宏可能有其他定义,否则您甚至不需要警卫。)

多文件模块

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

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

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

升级中

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

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

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

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

在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));}
}
Run Code Online (Sandbox Code Playgroud)

可以用作

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

结论

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

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

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

  • `#include &lt;cmeow&gt;` (12认同)
  • `meow` 通常在 C++ 示例中用作通配符或占位符,类似于 `foo`。(我不确定是否是 STL 发起的,但那是我第一次看到它的地方。)所以 `&lt;cmeow&gt;` 指的是以 `c` 开头的 C++ 头文件集,特别是那些旨在成为类似名称的 C 头文件的包装器。(尽管根据我的经验,可能是错误的,大多数 C++ 代码只是直接包含 C 标头并完全忽略 `&lt;cmeow&gt;` 标头。) (2认同)

Stf*_*lev 6

请看一下我喜欢的这个简单的例子。那里的模块确实有很好的解释。作者使用简单的术语和精彩的例子来研究文章中所述问题的各个方面。

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


Ger*_*dez 5

建议使用C ++模块,使编译器可以使用“语义导入”代替旧的文本包含模型。当找到#include预处理程序指令时,它们将不执行复制和粘贴操作,而是读取一个二进制文件,该文件包含表示该代码的抽象语法树的序列化。

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

微软的建议超出了此范围,并引入了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/