C++20高级编程 特性补充 模块(Module)
特性补充 模块(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