C++20高级编程 特性补充 模块(Module)

发布时间 2023-12-01 01:26:25作者: Mesonoxian

特性补充 模块(Module)


模块

模块的优点

C++20 引入了用于组件化C++程序的一种新式方法:模块

模块由编译为二进制文件的源代码文件组成.每次导入模块时,编译器都会重复使用二进制文件,从而节省时间.

模块没有头文件存在的脆弱问题.

导入模块不会更改模块的语义,也不会更改任何其他导入的模块的语义.

在模块中声明的宏、预处理器指令和非导出名称对导入它的源文件是不可见的.

可以按任意顺序导入模块,并且不会更改模块的含义.

在某些情况下,可以将头文件作为标头单元,而不是 #include 文件导入.

标头文件

标头单元 是预编译头文件(PCH)的推荐替代方法.与共享PCH文件相比,它们更易于设置和使用,但它们提供类似的性能优势.

若要将文件编译为没有默认标头单元文件扩展名的标头单元(例如.cpp),请在“配置属性”>“C/C++”>“高级”>“编译为”中设置“编译为 C++ 标头单元(/exportHeader)”.

为了导入标头文件,可以采取的方式有:

  • 在“配置属性”>“C/C++”>“常规”中修改“扫描源以查找模块依赖项”属性
  • 在“配置属性”>“C/C++”>“常规”中修改“将包含转换为导入”属性

比较标头单元、模块和预编译标头

  • 头文件:头文件很脆弱,因为 #include 的顺序可能会修改行为或破坏代码,并且会受到宏定义的影响.
    头文件编译速度缓慢.特别是当多个文件包含同一个文件时,因为头文件会被多次重新处理.
  • 预编译标头:预编译标头(PCH) 通过创建一组头文件的编译器内存快照来缩短编译时间.
    PCH 文件存在一些限制,导致它们难以维护.
  • 标头单元:标头单元 是一个"中间"步骤,旨在帮助转换为命名模块,以防依赖头文件中定义的宏.
  • 模块:这是导入功能最快、最可靠的方式.

启用模块

由于 模块(Module) 是C++20标准引入的新的特性,为了启用模块,需要做的步骤有:

  • 使用 /experimental:module 或 在“配置属性”>“C/C++”>“语言”属性页中修改“启用 C++ 模块(实验性)”属性
  • 使用 /std:c++latest 或 在“配置属性”>“C/C++”>“语言”属性页中修改“C++ 语言标准”属性

在C++20中,通过include引入头文件的方式被通过import导入模块的方式取代.

下面是一个例子:

#include <iostream>
#include <vector>
//传统include
import <iostream>;
import <vector>;
//对旧式include兼容的import
import std;
//实际上更使用的方式
//或者import std.core

模块相关语法

模块声明及模块导出:

export module MyModule;//模块导出声明

export void func()
{
    //do something
    return;
}
export struct myStruct{
    int num;
};

export namespace mySpace{
    int someFunc(){
        //won't be exported
    }
}

需要注意的,在using语句的export时,需要注意到一个问题:无法通过:

export using SomeSymbol;//这是不合法的
//error C2873: ‘SomeSymbol’: symbol cannot be used in a using-declaration

取而代之的,你应该通过显式表明全局所有权来解决

export using ::SomeSymbol;//合法的

这样就很好解决了上面的问题
模块导入:

import std.core;//模块导入
import <string>;//标头文件
import <windows.h>;//标头文件
//...
//旧式头文件引入#include

全局模块片段:

module;
//...一些预处理指令
module myModule;

模块分区:

//myModule-part.ixx
export myModule:part;
//myModule-part.cpp
module;
//...
module myModule:part;
//myModule.ixx
export module myModule;

export module :part;
//some other things

私有模块片段:

module:private;

传统预处理器指令控制导入的模块:

#define _SOME_H

#ifdef _SOME_H
    import myModule;
#endif

使用模块

在Visual Studio中,使用 .ixx 作为后缀来标明这是一个模块接口文件

接口文件同时包含函数定义和声明.但是还可以将定义放置在一个或多个单独的模块实现文件中.

//BasicPlane.Figures-Rectangle.ixx
export module BasicPlane.Figures:Rectangle;
export struct Rectangle
{
    Point ul, lr;
};

export int area(const Rectangle& r);
export int height(const Rectangle& r);
export int width(const Rectangle& r);
//BasicPlane.Figures-Rectangle.cpp
module;
#define ANSWER 12
module BasicPlane.Figures:Rectangle;

int area(const Rectangle& r) { return width(r) * height(r); }
int height(const Rectangle& r) { return r.ul.y - r.lr.y; }
int width(const Rectangle& r) { return r.lr.x - r.ul.x; }

此文件以module;开头,它引入了称为 "全局模块片段" 的模块特殊区域.它位于命名模块的代码的前面,你可以在其中使用预处理器指令.

模块的最简单形式可以包含一个结合了模块接口和实现的文件.但是还可以将实现放入一个或多个单独的模块实现文件中, 类似于.h和.cpp文件的使用方式.

//Example.ixx
export module Example;

namespace Example_NS
{
   export int f();
}
//Example.cpp
module;
module Example;
#define ANSWER 42

namespace Example_NS
{
   int func() {
        return ANSWER;
    }
   export int f() {
      return func();
   }
}

模块由一个或多个模块单元组成. 模块单元 是一个包含模块声明的 转换单元(源文件).有多种类型的模块单元:

  • 模块接口单元 是导出模块名称或模块分区名称的模块单元.
  • 模块实现单元 是不导出模块名称或模块分区名称的模块单元.
  • 主模块接口单元 是导出模块名称的模块接口单元.
  • 模块分区接口单元 是导出模块分区名称的模块接口单元.
  • 模块分区实现单元 是一个模块实现单元.

模块分区

对于较大的模块,可以将模块的各个部分拆分为称为 "分区"子模块 .

每个分区由导出模块分区名称的模块接口文件组成.分区可能还有一个或多个分区实现文件.

整个模块有一个主模块接口,它是模块的公共接口,也可以导入和导出分区接口.

//example_1.cpp
module;
module Example:part1;
//...
//example_1.ixx
export module Example:part1;
//...

//example_2.cpp
module;
module Example:part2;
//...
//example_2.ixx
export module Example:part2;
//...

//Example.ixx
export import :part1;
export import :part2;
//...

导入的名称不包括完整的模块名称.例如: part2 分区被声明为 export module Example:part2 然而,在此处导入的是 :part2.

由于我们在模块 Example 的主模块接口文件中,模块名称是隐含的,并且只指定了分区名称.

模块定义的模板

模块接口定义文件:

module; // optional. Defines the beginning of the global module fragment

// #include directives go here but only apply to this file and
// aren't shared with other module implementation files.
// Macro definitions aren't visible outside this file, or to importers.
// import statements aren't allowed here. They go in the module preamble, below.

export module [module-name]; // Required. Marks the beginning of the module preamble

// import statements go here. They're available to all files that belong to the named module
// Put #includes in in the global module fragment, above

// After any import statements, the module purview begins here
// Put exported functions, types, and templates here

module :private; // optional. The start of the private module partition.

// Everything after this point is visible only within this file, and isn't 
// visible to any of the other files that belong to the named module.

模块实现单元:

// optional #include or import statements. These only apply to this file
// imports in the associated module's interface are automatically available to this file

module [module-name]; // required. Identifies which named module this implementation unit belongs to

// implementation

模块分区文件:

module; // optional. Defines the beginning of the global module fragment

// This is where #include directives go. They only apply to this file and aren't shared
// with other module implementation files.
// Macro definitions aren't visible outside of this file or to importers
// import statements aren't allowed here. They go in the module preamble, below

export module [Module-name]:[Partition name]; // Required. Marks the beginning of the module preamble

// import statements go here. 
// To access declarations in another partition, import the partition. Only use the partition name, not the module name.
// For example, import :Point;
// #include directives don't go here. The recommended place is in the global module fragment, above

// export imports statements go here

// after import, export import statements, the module purview begins
// put exported functions, types, and templates for the partition here

module :private; // optional. Everything after this point is visible only within this file, and isn't 
                         // visible to any of the other files that belong to the named module.
//...

模块命名的规范

  • 可以在模块名称中使用句点('.') ,但它们对编译器没有特殊意义.使用它们向模块的用户传达意义.
    例如,以库或项目的顶级命名空间开始.以描述模块功能的名称结束.

  • 包含模块主接口的文件的名称通常是模块的名称.
    例如,如果模块名称为 BasicPlane.Figures,则包含主接口的文件的名称将为BasicPlane.Figures.ixx.

  • 模块分区文件的名称格式通常是 <主模块名称>-<模块分区名称>,其中,模块的名称后跟一个连字符('-'),然后是分区的名称.例如:BasicPlane.Figures-Rectangle.ixx