Namespaces
Variants

Modules (since C++20)

From cppreference.net
C++ language
General topics
Flow control
Conditional execution statements
Iteration statements (loops)
Jump statements
Functions
Function declaration
Lambda function expression
inline specifier
Dynamic exception specifications ( until C++17* )
noexcept specifier (C++11)
Exceptions
Namespaces
Types
Specifiers
constexpr (C++11)
consteval (C++20)
constinit (C++20)
Storage duration specifiers
Initialization
Expressions
Alternative representations
Literals
Boolean - Integer - Floating-point
Character - String - nullptr (C++11)
User-defined (C++11)
Utilities
Attributes (C++11)
Types
typedef declaration
Type alias declaration (C++11)
Casts
Memory allocation
Classes
Class-specific function properties
Special member functions
Templates
Miscellaneous

大多数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) 导出声明。导出 declaration declaration-seq 中的所有命名空间作用域声明。
4,5,6) 导入声明。导入一个模块单元/模块分区/头文件单元。
7) 开始一个 全局模块片段
8) 开始一个 私有模块片段

模块声明

一个翻译单元可以包含模块声明,此时它被视为 模块单元 。 若存在 模块声明 ,则它必须是翻译单元的首个声明(除后续将介绍的 全局模块片段 外)。每个模块单元通过模块声明关联到一个 模块名称 (以及可选的模块分区)。

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

模块名称由一个或多个以点分隔的标识符组成(例如: mymodule mymodule.mysubmodule mymodule2 ...)。点号本身不具有内在含义,但在非正式用法中常被用来表示层级关系。

如果模块名称或模块分区中的任何标识符被定义为 类对象宏 ,则程序非良构。

一个 命名模块 是具有相同模块名称的模块单元集合。

声明中包含关键字 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 模块名称 属性  (可选) ;

在给定命名模块的模块接口单元中导出的所有声明和定义,将通过导入声明在使用该模块的翻译单元中可用。

导入声明可以在模块接口单元中被导出。也就是说,如果模块 B 导出式导入 A ,那么导入 B 时也会使 A 的所有导出内容可见。

在模块单元中,所有导入声明(包括导出导入)必须分组放置在模块声明之后、其他所有声明之前。

/////// 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 header-name attr  (可选) ;

头文件单元是从头文件合成的独立翻译单元。导入头文件单元将使其所有定义和声明可被访问。预处理器宏同样可被访问(因为导入声明会被预处理器识别)。

然而,与 #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_C_SOURCE 会根据 POSIX 标准向标准头文件添加函数
#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;    // 导入 'A' 时 Hello() 可见
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() 对任何导入 ':C' 的 'A' 模块单元可见
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; } // 正确:lib_A中的f与lib_B中的f指向不同实体
export int y = f(); // y等于1

如果 同一实体的两个声明 分别属于不同模块,则程序非良构;若两个声明彼此不可达,则无需提供诊断信息。

/////// decls.h
int f(); // #1, 隶属于全局模块
int g(); // #2, 隶属于全局模块
/////// 模块 M 的接口
module;
#include "decls.h"
export module M;
export using ::f; // 正确:不声明实体,导出 #1
int g();          // 错误:匹配 #2,但已附加到 M
export int h();   // #3
export int k();   // #4
/////// 其他翻译单元
import M;
static int h();   // 错误:匹配第3条规则
int k();          // 错误:匹配第4条规则

以下声明未附加到任何命名模块(因此所声明的实体可以在模块外部定义):

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) 模块 — 核心语言支持
__cpp_lib_modules 202207L (C++23) 标准库模块 std std. compat

关键词

private module import export

缺陷报告

下列行为变更缺陷报告被追溯应用于先前发布的 C++ 标准。

缺陷报告 适用范围 发布时的行为 正确行为
CWG 2732 C++20 未明确可导入头文件是否能够
响应导入点的预处理器状态
不响应
P3034R1 C++20 模块名称和模块分区可能
包含被定义为对象式宏的标识符
禁止