【C++20】模块

模块

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高级编程》

  • 12
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 15
    评论
评论 15
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值