【问题标题】:The orthogonality of module interface/implementation units and partitions模块接口/实现单元和分区的正交性
【发布时间】:2022-10-21 22:33:42
【问题描述】:

C++20 标准似乎定义了模块单元的两种分类:接口/实现单元,以及模块单元是否为分区。这两个分类似乎是正交的:您可以有一个作为分区的实现单元,一个不是分区的接口单元,等等。

分类的接口/实现轴似乎是关于你可以import 和你不能。但如果这是真的,那么作为命名分区的实现单元又有什么意义呢?你不能让那个执行单元不是一个分区吗?

这两个概念是真正正交的,还是相互依存的?如果是后者,他们在多大程度上相互依赖?

【问题讨论】:

    标签: c++ c++20 c++-modules


    【解决方案1】:

    模块单元分类的这两个轴是正交的,从某种意义上说,模块可以独立地成为这些分类的任何组合的一部分。但是,该标准为 4 种分类中的每一种定义了许多专门的规则,这使得每种分类的使用不仅仅是分类定义所表明的。

    在查看这些组合之前,我们首先需要定义要分类的内容。

    接口单元与实现单元

    接口单元不是您可以导入的东西。嗯,你能够,但那不是定义“接口单元”。模块单元是模块M 的接口单元,因为它是界面模块M。这意味着,如果有人导入模块M,构建系统将需要构建模块M 的所有接口单元。在有人可以导入M 之前,不需要构建模块M 的实现单元。

    这是全部接口/实现分类意味着(虽然它不是全部,但我们会解决的)。实现单元在概念上是模块M 的一部分,但它们不是其接口的一部分。

    重要的是要注意成为“模块M 的一部分”意味着什么。如果在M 的权限内声明了一个实体,那么它就是M 的一部分。所以如果你想再次声明它(因为你正在定义它,比如说),第二个声明必须M ([basic.link]/10) 的范围内。

    这是所有类型的实施单位的重点:在M 的范围内而不对其做出贡献界面.

    分区与纯

    对于不是分区的模块单元,标准中没有术语,因此我将此类模块单元称为“纯”。

    作为模块M 的分区X 的模块单元可以通过分区导入语法导入:import :X。这只能由作为M 一部分的模块单元完成。纯模块单元不能以这种方式导入。

    所以分区与纯分类是关于模块内的模块单元是否可以通过特殊语法导入同一模块内的某些模块单元。

    同样重要的是要注意导入某些东西的含义。导入事物是在翻译单元的基础上完成的。导入非分区模块意味着导入该模块的所有接口模块单元TU。导入一个模块分区就是只导入那个分区单元。

    但是,export只重要对于通过代码导入的声明外部声明它们的模块。因此,如果M 的某个模块单元导入M 的分区单元,它将看到该分区单元权限内的所有声明,无论它们是否为exported ([basic.scope.namespace]/2)。

    现在,让我们检查 C++ 为四种组合中的每一种定义的所有特殊情况规则。到白衣:

    纯接口单元

    这种组合附加了许多特殊规则,以至于标准给它起了一个名字:主接口单元对于模块M

    如果我们只看上面的规则,M 的一个主要接口单元是M 接口的一个组件。并且由于它是纯的,M 的主接口单元不能通过分区语法导入。

    但随后标准在此之上设置了更多规则:

    1. 对于任何模块M,应有准确且只有一个M ([module.unit]/2) 的主要接口单元。

    2. 全部分割M的接口单元必须M ([module.unit]/3) 的主接口单元export imported(直接或间接)。

    3. 如果没有M 的其他实现或接口单元,则该文件可能有一个私有模块片段,用于将M 的未导出内容放在单个文件中([module.private.frag])。

      简而言之:如果构建系统需要构建模块M,那真正意味着它需要构建这个文件(以及它进口的任何东西)。该文件是定义import M; 将公开的内容的导入根。

      接口分区单元

      这样的模块单元是模块M的接口的一个组成部分,因此必须编译生成模块M。但这被处理了,因为主接口单元必须包括所有这些。它们也可以包含在内……我们知道,因为主要接口单元必须包含它们。

      因此,对于这个没有任何其他地方未涵盖的特殊规则。

      接口分区单元的意义只是将大模块接口分成多个文件的工具。

      纯执行单元

      作为实现单元,它们对模块的接口没有贡献。并且作为纯模块单元,它们不能作为分区导入。这意味着发生在他们身上的一切停留在其中(就导入任何东西而言)。

      但他们也有一些特殊的规则:

      1. M的所有纯实现单元含蓄地import M; ([module.unit]/8)。

      2. 他们不能明确的import M; ([module.import]/9)。

        如果一个实现单元的目的是能够定义一个模块的接口特性,那么这些规则是有意义的。如前所述,只有M 的模块单元可以定义作为M 接口一部分的声明。所以这些是大多数定义所在的文件。

        因此,他们也可以隐式包含M 的接口以方便使用。

        分区执行单元

        这是一个模块单元,它不是其模块接口的一部分。但是由于是分区,所以可以被M的其他模块单元导入。

        这个声音矛盾的,直到你得到这个特殊情况规则:

        1. 模块单元不能export import 分区实现单元 ([module.import]/8)。

          因此,即使接口单元导入了实现分区,它也无法导出该分区。实施单元也无法导出其中定义的任何内容(您不能将未导出的内容重新声明为exported 稍后)。

          但请记住export只重要用于导入非分区(即:其他模块)。由于分区只能由它们自己的模块的成员导入,并且导入的分区中的所有声明都将可供导入代码使用,所以我们所拥有的是模块单元,其中包含模块实现私有的声明,但需要可被多个模块实现单元访问。

          这一点特别重要,因为模块名称是全球的, 而分区名称是模块的本地名称。通过将内部共享代码放入实现分区,您不会用模块的实现细节污染模块名称的全局空间。

    【讨论】:

    • The main point of these module units being members of the module is to allow them to access the following: Partition implementation unit你能澄清一下你的意思吗?
    • @Serikov:我的意思是要导入模块的分区单元,您必须是部分该模块的。但是如果没有人需要导入你定义的东西,你就不需要成为一个分区。你只需要成为一个纯粹的执行单元。
    • 可以理解为“'纯'实现单元成为模块成员的要点是能够轻松地导入其他实现分区”,这是不正确的。如果不仅仅是我误读了那段,也许应该改变它。
    • :) 就标准而言,不仅分区不能导出所有实现单元(“不得导出模块实现单元”)。是的,我知道根本无法导入“纯”实施单元。
    • 是的。 C++20 中引入了一些新概念:“模块链接”,模块的声明附件。见this part for example
    【解决方案2】:

    C++20 标准似乎定义了模块单元的两种分类:接口/实现单元,以及模块单元是否为分区。

    还有另一类重要的模块单元(也是最重要的一个)——主模块接口。

    命名模块必须只包含一个主模块接口,并且可选地可以包含多个实现单元、多个接口分区和多个实现分区。

    分类的接口/实现轴似乎是关于你可以导入什么,你不能导入什么。

    不,这是关于什么可以为命名模块接口做出贡献,什么不能。模块界面unit 可以导出一些东西,因此可以为模块做出贡献界面.执行单位不能导出任何东西(所以不能自己导出),因此只能为执行的模块。

    命名模块的接口由主模块接口单元定义。如果命名模块包含其他接口单元(接口分区),那么它们应该从主模块接口直接或间接(传递)导出。

    但如果这是真的,那么作为命名分区的实现单元又有什么意义呢?你不能让那个执行单元不是一个分区吗?

    首先让我们考虑模块分区与“普通”模块实现单元有何不同。

    不是分区的模块实现单元自动(隐式)导入相应的模块接口。当我们写普通的“.cpp/.hpp”文件大多数时候我们将源文件中相应的头文件作为它的第一行。就是这样,模块实现单元类似于普通的源文件。

    为什么我们想要分区?

    由于不可能从另一个模块转发声明一个类,因此有时需要将原本可能是独立但相关的模块合并到一个复合模块中。这样做时,将复合模块的所有接口都写在一个文件中会很麻烦。在 C++20 中,可以使用模块接口分区将模块接口分成多个文件。类似地,可以使用“实现模块分区”在文件之间划分实现。

    可以使用import :partition-name; 语法将一个模块分区导入另一个模块分区,因此可以

    • 在分区 A 中声明实体,
    • 将分区 A 导入分区 B 以使用此实体
    • 在分区 C 中定义该实体。

    它就像头文件和源文件,但在单个模块中。

    考虑到私有模块片段只有在命名模块由q个单个模块单元(主模块接口单元)组成时才会出现,我们可以说有三种方式来构造命名模块:

    1. 单文件模块(主模块接口,内部带有可选的私有片段)。

    2. 主要接口单元+“未命名”实现单元。

      这是“头文件+源文件”的替代方案。“未命名”实现单元隐式导入模块接口,这很好。

      一个用例是,如果与依赖文件时间戳的构建系统一起使用,当更改仅限于实现文件时,实现和接口的分离可能会限制依赖模块的重新编译。 另一个用例是一个通用主模块接口的多个实现,可以由构建系统脚本在构建时选择。例如,特定操作系统的不同模块实现。

      1. 作为模块的库:主接口单元 + 多个接口分区 + 多个实现分区。

      它类似于具有多个公共头文件和多个私有源文件的库。

      主接口分区定义 API 表面并作为库的单一入口点(如一个“include-all.hpp”)。所有其他接口分区应直接或间接从中导出。

      分区不会自动导入模块接口。分区可以单独显式导入单个兄弟分区或作为一个整体导入模块。这类似于从库内部包含头文件。

      此模块结构可用于具有相互依赖类型且无法拆分为子模块的大型模块。

      当使用这种模块结构的变体时,实际上可以额外使用“未命名”模块实现单元,但 IMO 在这种情况下它没有带来任何新的东西。

    【讨论】:

    • "还有另一类重要的模块单元(也是最重要的一个)——主模块接口。" 这只是一个模块接口单元,不是分区。所以这不是“另一个”类;它是两个类的组合。
    • 主模块接口单元的规则是不同的,以至于不要试图用接口分区单元来挤压它。分区(接口和实现)也出现在提案的后期,具有不同的目标和自己的一套规则。因此,IMO 将主模块接口单元视为另一个类是合理的。
    • 差异示例:主单元接口必须存在于命名模块中,命名模块中只能有一个,它可以有私有模块片段,它必须导出接口分区,它不是分区但可以从其他分区导入.
    • 我知道有什么区别;我确实写了这个问题的另一个答案。我的观点是,就标准而言,“主接口单元”是不是分区单元的接口单元。它是两个类别的特定交集。所有类别交集都有特殊规则(例如,纯实现会自动导入模块,但分区实现不会)。
    • @NicolBolas 让我尝试从不同的角度解释我的立场。分区的共同属性是什么?它们不会隐式导入模块接口,因此可以回避循环依赖的问题,它们可以使用“import :partition”语法显式导入。它是一种不同类型的模块单元。接口单元的共同属性是什么?他们可以将某些内容导出到模块接口。执行单元的共同属性是什么?他们根本不能有“出口”,也不能“进口出口”。但是“纯”模块单元的属性是什么?
    猜你喜欢
    • 2013-12-04
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-01-29
    • 1970-01-01
    • 2021-11-25
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多