模块 (C++20 起)

来自cppreference.com
< cpp‎ | language

大多数 C++ 项目用到了多个翻译单元,因此它们需要在那些单元间分享声明定义。正是因为这样,使用标头非常重要,例如标准库的声明可以通过包含对应的标头提供。

模块是一个用于在翻译单元间分享声明和定义的语言特性。它们可以在某些地方替代使用标头。

模块和命名空间是正交的。

// helloworld.cpp
export module helloworld; // 模块声明
 
import <iostream>;        // 导入声明
 
export void hello()       // 导出声明
{
    std::cout << "Hello world!\n";
}
// main.cpp
import helloworld; // 导入声明
 
int main()
{
    hello();
}

语法

export(可选) module 模块名 模块分区 (可选) 属性 (可选) ; (1)
export 声明 (2)
export { 声明序列 (可选) } (3)
export(可选) import 模块名 属性 (可选) ; (4)
export(可选) import 模块分区 属性 (可选) ; (5)
export(可选) import 头名 属性 (可选) ; (6)
module; (7)
module : private; (8)
1) 模块声明。声明当前翻译单元是一个模块单元
2,3) 导出声明。导出 声明声明序列 中所有具有命名空间作用域的声明。
4,5,6) 导入声明。导入一个模块单元,模块分区,或标头单元。
7) 开始一个全局模块片段
8) 开始一个私有模块片段

模块声明

翻译单元可以有一个模块声明,这种情况下它们会被视为模块单元模块声明在有提供时必须是翻译单元的首个声明(后面提到的全局模块片段除外)。每个模块单元都对应一个模块名(可以带一个分区),它在模块声明中提供。

export(可选) module 模块名 模块分区 (可选) 属性 (可选) ;

模块名包含由点分隔的一个或多个标识符(例如:mymodulemymodule.mysubmodulemymodule2...)。点没有内在含义,不过它们会非正式地用于表示继承关系。

一个具名模块是一组模块名相同的模块单元。

声明中带有关键词 export 的模块单元是模块接口单元。其他模块单元被称为模块实现单元

对于每个具名模块,必须有恰好一个未指定模块分区的模块接口单元。这个模块单元被称为主模块接口单元。在导入对应的具名模块时可以使用它导出的内容。

// (每行表示一个单独的翻译单元)
 
export module A;   // 为具名模块 'A' 声明主模块接口单元
module A;          // 为具名模块 'A' 声明一个模块实现单元
module A;          // 为具名模块 'A' 声明另一个模块实现单元
export module A.B; // 为具名模块 'A.B' 声明主模块接口单元
module A.B;        // 为具名模块 'A.B' 声明一个模块实现单元

导出声明和定义

模块接口单元可以导出声明和定义,这些内容可以导入到其他翻译单元。它们可以有 export 关键词作为前缀,或者处于 export 块中。

export 声明
export { 声明序列 (可选) }
export module A; // 为具名模块 'A' 声明主模块接口单元
 
// hello() 会在所有导入 'A' 的翻译单元中可见
export char const* hello() { return "hello"; } 
 
// world() 不可见
char const* world() { return "world"; }
 
// one() 和 zero() 均可见
export
{
    int one()  { return 1; }
    int zero() { return 0; }
}
 
// 也可以导出命名空间:hi::english() 和 hi::french() 均可见
export namespace hi
{
    char const* english() { return "Hi!"; }
    char const* french()  { return "Salut!"; }
}

导入模块和头

模块可以通过导入声明导入:

export(可选) import 模块名 属性 (可选) ;

所有在给定具名模块的模块接口单元中导出的声明和定义都会在使用导入声明的翻译单元中可见。

导入的声明在模块接口单元里可以再导出。也就是说,如果模块 A 导入 B 后又导出,那么导入 A 也会使所有从 B 导出的内容可见。

在模块单元里,所有导入声明(包括带导出的导入)必须集中在模块声明后以及所有其他声明前。

/////// A.cpp ('A' 的主模块接口单元)
export module A;
 
export char const* hello() { return "hello"; }
 
/////// B.cpp ('B' 的主模块接口单元)
export module B;
 
export import A;
 
export char const* world() { return "world"; }
 
/////// main.cpp (非模块单元)
#include <iostream>
import B;
 
int main()
{
    std::cout << hello() << ' ' << world() << '\n';
}

在模块单元里(全局模块片段以外)不能使用 #include,因为所有被包含的声明和定义都会作为模块的一部分。标头可以改为通过导入声明导入:

export(可选) import 头名 属性 (可选) ;

导入一个标头会使它所有的声明和定义可见。预处理宏也会可见(因为预处理器会识别导入声明)。然而与 #include 相反的是,在(导入这个标头的)翻译单元中定义的预处理宏不会影响标头的处理。这在某些场合会不方便(某些标头用预处理宏作为配置方式),这种情况下需要使用全局模块片段

/////// A.cpp ('A' 的主模块接口单元)
export module A;
 
import <iostream>;
export import <string_view>;
 
export void print(std::string_view message)
{
    std::cout << message << std::endl;
}
 
/////// main.cpp (非模块单元)
import A;
 
int main()
{
    std::string_view message = "Hello, world!";
    print(message);
}

全局模块片段

模块单元可以有全局模块片段前缀,它可以在无法导入标头时(尤其是在标头用预处理宏进行配置时)包含它们。

module;

预处理指令序列 (可选)

模块声明

如果一个模块单元有一个全局模块片段,那么它的首个声明必须是 module;。然后在全局模块片段中只能出现预处理指令。然后用一个标准的模块声明结束这个全局模块片段,后面就是模块内容。

/////// A.cpp ('A' 的主模块接口单元)
module;
 
// 按照 POSIX 标准,定义 _POSIX_C_SOURCE 会向标准标头中添加函数。
#define _POSIX_C_SOURCE 200809L
#include <stdlib.h>
 
export module A;
 
import <ctime>;
 
// 仅用于演示(差的随机源)。应改为使用 C++ <random>。
export double weak_random()
{
    std::timespec ts;
    std::timespec_get(&ts, TIME_UTC); // 来自 <ctime>
 
    // 按照 POSIX 标准从 <stdlib.h> 提供。
    srand48(ts.tv_nsec);
 
    // drand48() 返回 0 与 1 之间的一个随机数
    return drand48();
}
 
/////// main.cpp (非模块单元)
import <iostream>;
import A;
 
int main()
{
    std::cout << "0 与 1 之间的随机值:" << weak_random() << '\n';
}

私有模块片段

主模块接口单元可以后随一个私有模块片段,这样就可以在不会把模块的所有内容暴露给导入方的情况下将模块表示为单个编译单元。

module : private;

声明序列 (可选)

私有模块片段会终止模块接口单元中可以影响其他编译单元的行为的部分。如果模块单元包含了私有模块片段,那么它就是它的模块中唯一的模块单元。

export module foo;
 
export int f();
 
module : private; // 终止模块接口单元中可以影响其他编译单元的行为的部分
                  // 开始私有模块片段
 
int f()           // 定义对 foo 的导入方不可及
{
    return 42;
}

模块分区

一个模块可以有模块分区单元。它们是模块声明中包含了一个模块分区的模块单元,模块分区在模块名之后,以一个冒号 : 开头。

export module A:B; // 为模块 'A' 分区 'B' 声明一个模块接口单元。

一个模块分区表示恰好一个模块单元(两个模块单元不能指定同一个模块分区)。它们只在自己所在的具名模块内部可见(在该具名模块外的翻译单元不能直接导入这些模块分区)。

模块分区只能被相同具名模块的模块单元导入。

export(可选) import 模块分区 属性 (可选) ;
/////// A-B.cpp   
export module A:B;
...
 
/////// A-C.cpp
module A:C;
...
 
/////// A.cpp
export module A;
 
import :C;
export import :B;
 
...

模块分区内的所有声明和定义在将它导入的模块单元中均可见,无论它们是否被导出。

模块分区可以是模块接口单元(如果模块声明中有 export)。它们必须被主模块接口单元在导入同时导出,并且它们导出的语句在模块被导入时均可见。

export(可选) import 模块分区 属性 (可选) ;
///////  A.cpp   
export module A;     // 主模块接口单元
 
export import :B;    // Hello() 在导入 'A' 时可见
import :C;           // 现在 WorldImpl() 只对 'A.cpp' 可见
// export import :C; // 错误:无法导出模块实现单元
 
// World() 对所有导入 'A' 的翻译单元均可见
export char const* World()
{
    return WorldImpl();
}
/////// A-B.cpp 
export module A:B; // 模块分区接口单元
 
// Hello() 对所有导入 'A' 的翻译单元均可见
export char const* Hello() { return "Hello"; }
/////// A-C.cpp 
module A:C; // 模块分区实现单元
 
// WorldImpl() 对 'A' 中所有导入 ':C' 的翻译单元均可见
char const* WorldImpl() { return "World"; }
/////// main.cpp 
import A;
import <iostream>;
 
int main()
{
    std::cout << Hello() << ' ' << World() << '\n';
    // WorldImpl(); // 错误:WorldImpl() 不可见
}

模块所有权

通常来说,在模块单元中的模块声明后出现的声明都 附着于 该模块。

如果一个实体的声明附着于一个具名模块,该实体只能在该模块中定义。每个这种实体的所有声明都必须附着于同一模块。

如果一个声明附着于一个具名模块,并且该声明没有被导出,那么声明的名字具有模块链接

export module lib_A;
 
int f() { return 0; } // f 具有模块链接
export int x = f();   // x 等于 0
export module lib_B;
 
int f() { return 1; } // OK,lib_A 中的 f 和 lib_B 中的 f 指代不同的实体
export int y = f(); // y 等于 1

如果两个匹配的声明附着于不同的模块,并且它们都声明了具有外部链接的名字,那么程序非良构;在两个声明互相不可及时不要求诊断。在实践中有两种模型:

  • 弱模块所有权 模型中,这些声明被视为声明了相同的实体。
  • 强模块所有权 模型中,这些声明被视为声明了不同的实体。
export module lib_A;
 
export constexpr int f() { return 0; } // f 具有外部链接
export module lib_B;
 
export constexpr int f() { return 1; }
// 在弱模块所有权模型中:存在多个 f 的定义;链接器会选择其中任意一个
// 在强模块所有权模型中:OK,lib_A 中的 f 和 lib_B 中的 f 是不同的实体

以下声明不附着于任何具名模块(因此声明的这些实体可以在模块外定义):

export module lib_A;
 
namespace ns // ns 不附着于 lib_A
{
    export extern "C++" int f(); // f 不附着于 lib_A
           extern "C++" int g(); // g 不附着于 lib_A
    export              int h(); // h 附着于 lib_A
}
// ns::h 必须在 lib_A 中定义,但 ns::f 和 ns::g 可以在其他地方定义
// (例如在传统源文件中)

注解

功能特性测试宏 标准 备注
__cpp_modules 201907L (C++20) 模块

关键词

exportimportmodule