模块
C++语言从一开始便继承了C语言的include头文件机制,通过包含头文件的方式来引用其他组件的代码,这些头文件通常包含了该组件相关的接口声明。但使用头文件通常伴有如下问题:
- 不够清晰
- 不够清晰
- 同名符号覆盖问题
C++20提供了模块特性,一个将库与软件组件化的现代解决方案,它能够像头文件一样在源代码间共享符号,与头文件不同的地方在于,模块并不会泄露宏的定义以及一些私有的实现细节。模块容易被组合,它们能够实现精确的控制按需将接口暴露给导入它们的源文件,并且不会因为导入顺序、宏定义等改变一个模块的语义。
- 模块提供了头文件无法做到的额外的安全保证,编译器与链接器一起协作来避免一些潜在的名称冲突问题,并且能够保证一处定义原则。
- 模块由一系列源文件所组成,它们能够被独立导入该模块的源文件编译。模块只需要编译一次,它编译的结果就会被存储到一个二进制文件中,该文件里记录了所有导出的符号,例如函数与模板。当某个源文件导入该模块时,这个二进制文件会被读取,读取二进制文件要比解析头文件快,并且能够在项目的每一个导入该模块的源文件中复用。
Hello World模块
微软的编译器却对标准库进行了模块化,通过使用import关键字导入一个模块
import std.core;//导入标准库模块
int main()
{
std::cout <<"Hello World\n";
}
上述代码通过importstd.core导入标准库的一些输入输出流的模块,MSVC提出了如下几种标准模块:
• std.regex提供了正则表达式相关的支持。
• std.filesystem提供了文件系统相关的支持。
• std.memory提供了智能指针等模块支持。
• std.threading提供了并行相关的支持。
• std.core提供剩余标准库的支持。
定义一个模块
使用import语句可以导入一个已有的模块,并使用那个模块中导出的接口。定义一个模块也相当简单,通常一个模块由一个接口文件和零到数个源文件组成,如果程序员在一个接口文件中声明并实现所有接口,那么也可以不再提供源文件。
创建一个数学模块,它通过math.mpp提供接口文件,内容如下
export module math;//使用export module表达对外模块名,同时表明这是一个接口文件
export template<typename T>//导出的一个接口
T square(T x){return x * x;}
过在接口文件中使用“export module模块名;来声明一个模块,这个模块名就是后续可以被用户导入的名字
如果需要对square的模板参数进行约束,例如使用标准库concept中的integral概念来约束时只能接受整数类型的参数,此时需要如何做?在模块的接口文件或源文件中提供了全局模块片段(global module fragment),它是专门在这个片段中处理头文件的预处理包含指令,这部分内容并不归模块所有,也不会导出,因此头文件仅仅是实现细节所需。
module; //全局模块片段
#include <concept>
export module math; //对外模块名
export template<std::integral T>//导出的一个接口
T square(T x){return x * x;}
在模块的接口或实现文件中,仍然可以导入其他模块,以便实现所需要的行为。例如,使用import导入标准库头文件
export module math;
import <concept>
若将实现从接口文件中拆分到源文件中,代码如下
//接口文件math.mpp
export module math;
export int square(int x);
//实现文件math.cpp
module math;
int square(int x){return x * x;}
在实现文件中,首先用module声明它所属的模块。接着square函数无须使用export进行修饰,export只能位于接口文件中
模块分区
如果一个模块的接口过大,可以进一步考虑将它们分解成一个个小的模块分区,这些模块分区拥有自己的接口文件,然后在主接口模块单元进行组合并导出它们。模块分区名在模块名后通过冒号(:)指明
考虑一个shape模块,矩形由两个点组成,并且提供接口求矩形的长、宽以及面积。为了展示模块分区,将点和长方形作为两个独立的模块分区,分别被命名为shape:point以及shape:rectangle
点模块分区的接口文件point.mpp
//export module表明是个接口文件
//:表明是个模块分区
export module shape:point;
export struct Point{
int x , y;
};
模块分区的接口文件就和主接口文件一样,通过使用export module表明,唯一的区别在于模块名中的“:”,它分割了模块名与分区名。模块分区只能被同一个模块下的其他文件所导入,例如后续需要使用该模块分区的矩阵模块分区
矩阵模块分区的接口文件rectangle.mpp
export module shape:rectangle;
import:point;
export struct Rectangle{
Point topLeft , bottomRight;
int width();
int hehght();
int area();
};
主接口模块文件shape.mpp的职责是直接或间接地导出它所有的分区
export module shape;
export import :point;
export import :rectangle
私有片段
模块分区名以冒号(:)分割,而“:private”拥有额外的语义,它表达了模块的私有片段。顾名思义,当程序员不想提供额外的实现文件时,这些实现部分可以放到接口文件的私有片段中。当使用私有片段时,则无法再对模块进行分区,换句话说私有片段只能在模块主接口文件中使用,这个模块仅仅由这一个文件组成
module :private;//私有片段
template<std::integral T>//导出的一个接口
T square(T x){return x * x;}
测试代码
import <iostream>
import shape
int main()
{
Rectangle r{{1,2},{3,4}};
std::cout << r.area() << std::endl;
std::cout << r.width() << std::endl;
std::cout << r.height() << std::endl;
}
模块样板文件
模块的主接口文件,样板如下
模块的分区接口文件,样板如下
模块的分区接口文件,样板如下
注意事项
目前,GCC的模块在导入标准库头文件时,可能会存在编译问题,但它对分区支持比较好
参考资料 :《C++20高级编程》