博客 阅读时间 4 分钟

为什么 C++ 模块特性对 C++ 的未来如此重要?

分享本文
Why is the C++ modules feature so important for the future of C++?

C++ 曾经停滞了许多年,许多开发者确信这门语言会步 COBOL、Fortran 和 VB6 的后尘:不再用它开发新项目,C++ 开发者只是维护既有项目。但出乎意料的是,C++ 浴火重生,新标准极大地改变了这门语言的使用方式。

然而,遗留的#include机制依然存在。在语言完成现代化之后,它成了下一个需要解决的薄弱环节。事实上,它有许多缺点;以下是摘自这份有趣文档

  • 编译期可扩展性:每次包含一个头文件,编译器都必须预处理并解析该头文件中的文本,以及它传递性包含的所有头文件。这个过程必须为应用程序中的每个翻译单元重复一遍,涉及大量冗余工作。在一个拥有N个翻译单元、每个翻译单元包含M个头文件的项目中,编译器要执行M x N的工作,尽管M个头文件中的大多数是在多个翻译单元之间共享的。C++ 受到的影响尤其严重,因为模板的编译模型迫使大量代码进入头文件。
  • 脆弱性#include指令被预处理器视为文本包含,因此会受到包含时任何活跃宏定义的影响。如果某个活跃的宏定义恰好与库中的某个名称冲突,就可能破坏库的 API,或导致库头文件本身编译失败。举个极端的例子:先#define std "The C++ Standard",然后再包含一个标准库头文件:结果是 C++ 标准库实现中一连串可怕的失败。更微妙的现实问题是,当两个不同库的头文件因宏冲突而相互影响时,用户被迫重新排列#include指令的顺序,或者引入#undef指令来打破(非预期的)依赖。
  • 约定俗成的变通方法:C 程序员采用了许多约定来规避 C 预处理器模型的脆弱性。例如,绝大多数头文件都需要包含保护(include guard),以确保多次包含不会破坏编译。宏名称以LONG_PREFIXED_UPPERCASE_IDENTIFIERS书写以避免冲突,一些库/框架开发者甚至在头文件中使用__underscored名称,以避免与"正常"名称冲突——而这些"正常"名称(按照约定)根本不该是宏。这些约定对来自非 C 语言的开发者构成了入门门槛,对经验丰富的开发者来说是样板代码,还让我们的头文件远比应有的样子丑陋。
  • 工具的困惑:在基于 C 的语言中,很难构建与软件库良好配合的工具,因为库的边界并不清晰。哪些头文件属于某个特定的库?这些头文件应该按什么顺序包含才能保证正确编译?这些头文件是 C、C++、Objective-C++,还是这些语言的某种变体?头文件中的哪些声明是真正属于 API 的,哪些又仅仅是因为不得不写在头文件里才存在的?

这些问题的解决方案早在最初的C++0x 草案中就已提出:模块(Modules)特性。然而,它在每一版新标准规范中都被推迟了。

模块:更健壮的语义模型

模块以更健壮、更高效的语义模型改进了对库的访问。从用户的角度来看,代码只是略有不同,因为使用的是import声明,而不是#include预处理器指令。下面是 C++0x 草案中的一个例子:

import std; // Module import directive.
int main() {
    std::cout << "Hello World\n";
}

无需包含多个 STL 文件——只需一次 import 就足够了,这让代码更加整洁。模块导入会加载std模块的二进制表示,并使其 API 直接对应用程序可用。位于 import 声明之前的预处理器定义不会影响std提供的 API,因为该模块本身是作为一个独立的模块单独编译的。

这里有一篇有趣的文章,展示了如何用 VS2017 开发 C++ 模块:Visual Studio 2017 中的 C++ 模块

结论

C++ 模块将消除与 include 机制相关的各种问题,让使用其他库变得像 C# 或 Java 一样容易。

试着创建您的第一个模块,探索这个强大的功能吧。

分享本文