C++学习笔记----10、模块、头文件及各种主题(一)---- 模块(1)
在使用模块之前,头文件用于提供代码重用的接口。头文件确实有许多问题,比如避免同一头文件的多重包含以及确保头文件的包含顺序正确。还有,简单的#include,例如,<iostream>就添加了几千行代码,编译器不得不编译。如果几个源文件#include <iostream>,所有这些翻译单元变得更大了。这还只是包含了一个简单的头文件。想像一下,如果需要<iostream>,<vector>,<format>等等。
模块解决了所有这些问题,甚至更多。模块被import的顺序并不重要。模块一旦被编译为进制格式,每当该模块被import进行另一个源文件时,编译器就可以使用。这与头文件是一个明显的对比,头文件是每当编译器碰到#include那个头文件时都要进行一而再再而三的编译。因此,模块可以极大节省编译时间。当在模块中修改了内容时,也节省了增加的编译时间,例如,修改一个模块接口文件中的导出函数实现,不需要触发使用那个模块的用户的重新编译。模块不受外部定义宏的影响,任何在模块中定义的宏对模块之外的任何代码不可见,也就是说,模块是自我隔离的。因此,推荐如下:
注意:有了以上讨论的好处,如果编译器支持模块,新写的代码应该使用模块来结构化代码成为逻辑上分隔的构造块。
如果可能的话,历史遗留代码也可以慢慢转换为模块。然而,遗留代码太多了,许多第三方库还不支持模块,现在也不是所有的编译器完全支持模块。基于这些原因了解传统的头文件是如何工作的依然重要。这就是本章仍然要包含头文件的讨论的原因。
注意:如何编译模块与编译器相关。咨询编译器的文档来学习特定编译器的模块工作方式。
注意:如果你的编译器不支持模块,那就只能将我们的例子转化为非模块的代码了,但还是建议你与时俱进,跟上时代的脚步吧,找一个支持模块的编译器,使你的工作事半功倍;而不是因循守旧,事倍功半。
1、将代码非模块化
如果想要使用还不完全支持模块的编译器来编译本博客中的代码,可以按如下步骤将代码非模块化:
- 重命名.cppm模块接口文件为.h头文件。
- 在每个.h头文件的顶部添加#pragma once。
- 去除export module xyz声明
- 替换module xyz声明为#include来包含相应的头文件。
- 替换import与export import声明为合适的#include指令。如果代码使用了import std;那就需要用#include指令来包含所有需要的单个头文件了。
- 去除任何export关键字。
- 去除所有的module;,它指出了全局模块部分的开始。
- 如果函数定义或变量定义出现在一个.h头文件中,在前面添加inline关键字。
2、标准命名的模块(c++23)
通过导入标准命名模块std可以访问c++标准库的任何东东。该命名模块使得整个标准库可用,包括所有的定义在<cstddef>中的C功能。然而,只要通过std命名空间就可以使用所有的C功能。对于遗留代码,可以考虑导入std.compat命名模块,它导入了所有的std import的,同时使得std命名空间与全局空间的C功能可用。在新的代码中不推荐使用std.compat。
3、模块接口文件
模块接口文件定义了由模块提供的功能的接口,通常以.cppm作为文件扩展名。模块接口文件以声明标示文件是由一个特定的名字定义的模块开头。这叫做模块声明。模块名字可以是任何合法的c++标识符。名字可以包含.但是不能以.开始或结束,并且不能一行中包含多个.。合法的名字的例子是datamodel,mycompany.datamodel,mycompany.datamodel.core,datamodel_core,等等。
注意:目前,还没有模块接口文件的标准扩展名。然而,大多数编译器支持.cppm(C++模块)扩展名,所以我们也这么用。可以检查一下你的编译器使用的是什么样的扩展名。
模块需要显式声明要把什么导出去,也就是说,当客户端代码导入模块时什么是可见的。模块可以导出任何声明,例如变量声明,函数声明,类型声明,using指令,以及using声明。还有,导入声明也可以被导出。从模块中导出实体通过export关键字来完成。从模块中没有导出的东东只在模块自身内可见。所有导出的实体的集合叫做模块接口。
下面是一个模块接口文件的例子,名字叫做Person.cppm,定义了一个person模块,导出了一个Personal类。注意,它导入了由std提供的功能。
export module person; // Named module declarationimport std; // Import declarationexport class Person // Export declaration
{
public:explicit Person(std::string firstName, std::string lastName): m_firstName{ std::move(firstName) }, m_lastName{ std::move(lastName) } { }const std::string& getFirstName() const { return m_firstName; }const std::string& getLastName() const { return m_lastName; }private:std::string m_firstName;std::string m_lastName;
};
在标准术语中,所有东东以命名模块声明开始(上面代码段中的第一行),直到文件结尾,叫做模块范围。
Person类可以通过导入person模块来使用,如下(test.cpp):
import person;
import std;using namespace std;int main()
{Person person{ "Kole", "Webb" };println("{}, {}", person.getLastName(), person.getFirstName());
}
任何东东都可以从模块导出,只要它有名字。例子为类定义,函数原型,类枚举类型,using声明与指令,命名空间,等等。如果命名空间使用export关键字显式导出,该命名空间中的所有东东也自动导出。例如,下面的代码段导出了整个DataModel命名空间;因此,没有必要再显式导出单个类与类型别名:
export module datamodel;import std;export namespace DataModel
{class Person { /* ... */ };class Address { /* ... */ };using Persons = std::vector<Person>;
}
也可以使用export block导出一个声明的整块,下面是一个例子:
export
{namespace DataModel{class Person { /* ... */ };class Address { /* ... */ };using Persons = std::vector<Person>;}
}