C++模块是什么?

68

我一直在关注C++标准化,最近接触到了C++模块的概念。但是我找不到一篇好的文章来介绍它。它到底是什么?


你能贴出你找到它的链接吗?这样或许人们可以相互关联。 - Pranit Kothari
@pranitkothari 最近在这里提到过(http://meetingcpp.com/index.php/br/items/cpp-status.html) - Shafik Yaghmour
1
https://dev59.com/BHA65IYBdhLWcg3w2imp - Erbureth
@pranitkothari: http://meetingcpp.com/index.php/br/items/cpp-status.html - Amani
Clang有一个很好的文档这里,它是用于C++实验性质的。 - Shafik Yaghmour
请参考此答案:https://dev59.com/BHA65IYBdhLWcg3w2imp#25621565 - lanoxx
5个回答

123

动机

简单来说,C++模块就像是既是头文件又是翻译单元的东西。它像一个头文件,因为你可以使用它(通过import,这是一个新的上下文关键字)来访问库中的声明。因为它是一个翻译单元(或对于复杂模块而言是多个翻译单元),它会被单独地编译一次。(记住,#include会将文件的内容拷贝到包含该指令的翻译单元中。)这种组合带来了许多优点:

  1. 隔离性:因为模块单元是一个独立的翻译单元,它有自己的一组宏和using声明/指令,既不影响也不受导入翻译单元或任何其他模块中的内容所影响。这可以防止在一个头文件中定义的标识符# define与另一个头文件中使用的标识符发生冲突。虽然使用using仍应谨慎,但在模块接口的命名空间范围内编写甚至using namespace也不会本质上有害。
  2. 接口控制:因为模块单元可以声明具有内部链接(使用static或namespace{}),导出(保留用于此类目的关键字自C++98以来),或者两者都没有的实体,所以它可以限制其内容对客户端的可用性。这取代了可能会在使用相同包含命名空间的头文件之间发生冲突的namespace detail习惯用法。
  3. 消除重复:因为在许多情况下不再需要在头文件中提供声明和在单独的源文件中提供定义,所以减少了冗余和相关机会的分歧。
  4. 避免一次定义规则违规:ODR的存在仅是因为需要在使用它们的每个翻译单元中定义某些实体(类型、内联函数/变量和模板)。模块可以仅定义一个实体,但仍然向客户端提供该定义。此外,已经通过内部链接声明违反ODR的现有头文件在转换为模块时不再是非法的,无需诊断。
  5. 非局部变量初始化顺序:因为import在包含唯一变量定义的翻译单元之间建立依赖关系顺序,所以有明显的顺序初始化具有静态存储期的非局部变量。C++17提供了具有可控初始化顺序的inline变量;模块将其扩展到普通变量(并且根本不需要inline变量)。
  6. 模块专用声明:在模块中声明的实体既不导出也没有内部链接,任何模块中的翻译单元都可以使用(按名称),提供了一个有用的中间地带,介于static或非static的先前选择之间。虽然尚不清楚实现会对这些做什么,但它们与动态对象中“隐藏”(或“未导出”)符号的概念非常相似,提供了这种实用的动态链接优化的潜在语言识别。
  7. ABI稳定性:inline规则(其ODR兼容目的在模块中不相关)已经调整,以支持(但不要求!)非内联函数可以作为共享库升级的ABI边界的实现策略。
  8. 编译速度:因为模块的内容不需要作为使用它们的每个翻译单元的一部分进行重新解析,所以在许多情况下,编译速度会更快。值得注意的是,编译的关键路径(管理无限并行构建的延迟)实际上可能更长,因为必须按依赖关系顺序单独处理模块,但总CPU时间

    方法

    因为在模块中声明的名称必须在客户端中找到,所以需要一种新的重要的名称查找方法,可以跨越翻译单元工作;获得正确的参数相关查找和模板实例化规则是这个提案花费十多年时间进行标准化的重要部分。简单的规则是(除了与内部链接不兼容的明显原因之外),export仅影响名称查找;通过(例如)decltype或模板参数可用的任何实体都具有完全相同的行为,无论它是否被导出。

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

    因为编译器必须能够基于import foo;(并基于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 guards。)

    多文件模块

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

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

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

    升级

    将基于头文件的库转换为模块既不是微不足道的任务,也不是巨大的任务。所需的样板非常少(在许多情况下仅有两行),并且可以在文件的相对较大的部分周围放置{{export {}}}(尽管存在不幸的限制:无法包含static_assert声明或推导指南)。通常,namespace detail {}可以转换为namespace {},也可以简单地保持未导出状态;在后一种情况下,它的内容通常可以移动到包含命名空间中。如果需要从其他翻译单元内联调用类成员,则需要显式标记其inline

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

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

    还有一种选项是导入“模块化”(或可导入)头文件(import "foo.hpp";):导入的是一个合成的头单元,它的行为类似于模块,只是它导出了它声明的所有内容 - 即使是具有内部链接的内容(如果在头文件外部使用可能仍会产生ODR冲突),以及宏。 (如果不同的导入头单元给出了不同的值,则使用宏将产生错误; 命令行宏(-D)对此不考虑。)非正式地说,如果包含头文件一次而没有定义特殊宏就足以使用它,则该头文件是模块化的(而不是例如使用标记粘贴实现模板的C实现)。如果实现知道头文件是可导入的,则可以自动将其#include替换为import

    在C++20中,标准库仍然作为头文件呈现; 所有C ++头文件(但不包括C头文件或<cmeow>包装器)都被指定为可导入。 C++23可能还将提供命名模块(尽管可能不是每个头文件都有一个)。

    示例

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

    export module simple;
    import <string_view>;
    import <memory>;
    using std::unique_ptr;  // not exported
    int *parse(std::string_view s) {/*…*/}  // 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++编程,但这些改进是渐进式的(在实践中)逐步实现的。委员会强烈反对将模块化作为“新语言”(例如,改变有符号和无符号整数之间比较规则),因为这会使转换现有代码更加困难,并且在模块化和非模块化文件之间移动代码会变得危险。

    MSVC已经有了一个模块化实现(紧密遵循TS)。Clang也有可导入头文件的实现已经几年了。GCC有一个功能齐全但不完整的标准化版本实现。


17
抱歉,#include <cmeow>不是有效的C++代码行,因此它不能被翻译成中文。它看起来像是一个玩笑或者调侃,因为"C meow"(“C 喵”)听起来像是“kitty”,也就是一只小猫的名字。如果您有其他需要翻译的内容,请告诉我。 - Lightness Races in Orbit
@B_Dex_Float:不是这样的——这可能暗示着A:B:CA:X:Y之间存在某种隔离,而该模型并未包括在内。 - Davis Herring
很遗憾,这是否意味着在C++20模块中没有真正建立层次结构的方法,就像在Python中一样,对吗?(另外,<cmeow>是什么?) - B_Dex_Float
2
“meow”在C++示例中通常用作通配符或占位符,类似于“foo”。(我不确定是STL-the-person开始使用的,但那是我第一次看到它的地方。)因此,“<cmeow>”指的是以“c”开头的一组C++头文件,特别是那些旨在成为同名C头文件的包装器。(尽管根据我的经验,可能是错误的,大多数C++代码直接包含C头文件并完全忽略<cmeow>头文件。) - Miral
@DavisHerring 在写作时(2021年9月),g++-11已经发布并全面支持模块化,因此建议稍微重写结论部分。我有一个git仓库,使用GCC以多种方式测试模块。我已经体验到了对于模块划分、标准头文件的编译以及从模块构建/链接的共享库的全面支持。 如果可能的话,我也很想讨论模块如何与头文件包含(模块依赖关系)的翻译单位并行竞争。 - alexpanter
显示剩余2条评论

11

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

这些语义导入避免了头文件中包含的代码的多次重新编译,加快了编译速度。例如,如果您的项目包含100个不同的.cpp文件中的100个#include <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/


6
请看我喜欢的这个简单例子。其中的模块解释得非常好。作者使用简单的术语和巧妙的例子来检查问题的各个方面,此文所述内容与IT技术有关。
请访问以下链接:https://www.modernescpp.com/index.php/c-20-modules

2

1
我正要发布你上面提到的clang链接。那篇文章真的以一种易于理解的方式向大众介绍了模块的概念。对此给予赞赏! - David Peterson
12
感谢您发布答案!请注意,您应该在此网站上发布答案的关键部分,否则您的帖子可能会被删除[请参阅FAQ,其中提到“几乎只是链接”的答案]。如果您愿意,仍然可以包含链接,但仅作为“参考”。答案应该能够独立存在,不需要链接来支持。 - Taryn
1
@Taryn,您需要的链接是https://stackoverflow.com/help/deleted-answers。 - patatahooligan

1
模块是一组源文件,它们被编译为一个二进制组件。模块可以被其他模块(或支持模块导入的翻译单元)导入。组成模块的源文件集包括一个接口文件(例如 .ixx 文件)和 0 个或多个源文件,如 .cpp 文件。
(请参见下文,了解模块相对于头文件的优势示例。)

https://learn.microsoft.com/en-us/cpp/cpp/modules-cpp?view=msvc-170

每个模块的接口文件是模块向消费者公开功能的唯一文件。例如,要公开在模块的.cpp文件中的类和函数等功能,它们必须从模块的接口文件中“导出”。接口文件还可以包含功能及其导出。
关于模块的关键概念:
  • 不需要预编译的概念,因为接口文件和其他模块源文件总是被编译成二进制形式。

  • 模块通过接口文件公开(导出)功能。

  • 接口文件(例如 .ixx)和 .cpp 文件使用 import 语句按模块名称导入其他模块。例如,import ModuleA; 将在幕后使用接口文件 ModuleA.ixx 的二进制形式,例如 ModuleA.ixx.obj。我们不需要关心这些细节,只需简单地使用 import ModuleA;

  • 模块比头文件更加自包含,因为:

    • 只有模块明确导出的内容对外可见。
    • 一个模块通过 import 暴露给导入它的文件使用的内容,对其他导入的模块不可见。

    例如,即使模块 A 包含传统的头文件,其中的宏等对使用 import module A; 的模块 XYZ 不可见。模块 XYZ 只能看到模块 A 明确导出的内容。如果模块 XYZ 在导入模块 A 之后再导入模块 B,那么模块 A 的任何内容都不会暴露给模块 B(反之亦然)。请注意,这与头文件不同,头文件会将所有内容暴露给随后包含的其他头文件以及包含它的文件。

与之相比,"include ModuleA.h"(例如)将暴露ModuleA.h中的所有内容以及ModuleA.h包含的所有内容!

  • 通常,头文件(包括模板库头文件)可以通过import模块的方式包含在模块中,以便使用这些头文件。例如,STL的大部分甚至全部可以在一个单独的.ixx文件中#include并进行编译。生成的模块是原始头文件的单一二进制形式,可以被其他模块import。通过在SomeSetOfHeaders.ixx(例如)中包含头文件所生成的接口二进制形式,在import时更加隔离且编译效率更高,而不是直接包含原始头文件。

以下示例用于说明模块的隔离特性:

ModuleA.ixx

export module ModuleA;
    
import ThingThatModuleA_Uses;
    
export void ExampleExportedFunction
{
    std::cout << "\nModule Test\n";
}

ModuleB.ixx

export module ModuleB;

import ModuleA;

// ThingThatModuleA_Uses IS NOT visible here.

...
...

ExampleA.h

#include ThingThatExampleA_Uses.h

...
...

ExampleB.cpp

#include ExampleA.h 

// Contents of ThingThatExampleA_Uses.h IS visible here.

...
...

在上面的例子中:
- 导入到ModuleA的模块ThingThatModuleA_Uses,在ModuleB中不可见。 - 然而,被ExampleA.h包含的ThingThatExampleA_Uses.h,在ExampleB.cpp中可见

https://learn.microsoft.com/en-us/cpp/cpp/tutorial-named-modules-cpp?view=msvc-170


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接