模块 (C++20 起)

来自cppreference.com
< cpp‎ | language

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

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

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

// 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';
}

模块分区

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

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() 不可见
}

关键词

exportimportmodule