C++学习笔记----10、模块、头文件及各种主题(一)---- 模块(3)
6、可见性与可触达性
如前所述,当在另外一个不是person模块的一部分的源文件中导入person模块,例如在test.cpp文件的盒子中,就没有隐式地继承person模块接口文件中的std导入声明。没有显式导入std的test.cpp,std::string名字就不可见,意味着下面的代码编译不会成功:
import person;
int main()
{
std::string str;
}
还有,即使没有添加显式的std的导入到test.cpp中,下面的代码就没有问题:
Person person { "Kole", "Webb" };
const auto& lastName { person.getLastName() };
auto length { lastName.length() };
这是怎么回事呢?在c++中可见性与可触达性之间是有区别的。通过导入person模块,std的功能变为可触达但不可见。可触达的类的成员函数走动可见。所有这些都意味着可以使用std的特定功能,例如通过使用auto类型推演在变量中保存getLastName()的结果,以及在它上面调用像length()这样的成员函数。
为了使std::string在test.cpp中可见,要求显示的std或<string>的导入。
7、子模块
c++标准中是不会叫做子模块的;然而,在模块名字中是允许使用.的,这就使得可以用想要的层次结构来结构化模块。例如,前面的例子可以用下面的DataModel命名空间的例子给出:
export module datamodel;
import std;
export namespace DataModel
{
class Person { /* ... */ };
class Address { /* ... */ };
using Persons = std::vector<Person>;
}
Person与Address类都在DataModel命名空间与datamodel模块中。这可以重新结构化为定义两个子模块:datamodel.person与datamodel.address。datamodel.person子模块的模块接口文件如下:
export module datamodel.person; // datamodel.person submodule
export namespace DataModel { class Person { /* ... */ }; }
下面是datamodel.address的模块接口文件:
export module datamodel.address; // datamodel.address submodule
export namespace DataModel { class Address { /* ... */ }; }
最后,datamodel模块定义如下,它导入并且立刻导出了两个子模块。
export module datamodel; // datamodel module
export import datamodel.person; // Import and export person submodule
export import datamodel.address; // Import and export address submodule
import std;
export namespace DataModel { using Persons = std::vector<Person>; }
当然了,在子模块中的成员函数实现也可以进入到模块实现文件中。例如,假定Address类有一个缺省构造函数,只是打印一句话到标准输出。该实现可以在一个叫做datamodel.address.cpp的文件中:
module datamodel.address; // datamodel.address submodule
import std;
using namespace std;
DataModel::Address::Address() { println("Address::Address()"); }
用子模块来结构化代码的好处是客户可以立刻导入所有或只导入想要使用的部分。例如,如果客户代码需要访问datamodel模块中的所有,下面的导入声明是最容易的:
import datamodel;
另一方面,如果客户代码只对使用Address类有兴趣,那么下面的导入声明就够了:
import datamodel.address;
立刻导入所有比单独导入需要的更方便,特别是对于极少更改的稳定模块而言。然而,通过使用不太稳定的模块的单独的导入,可能会在对模块进行修改时提升编译时效。例如,如果对datamodel.address子模块的接口文件进行了修改,那么 只有那些导入了该子模块的文件需要重新编译。
8、模块分区
另一种结构化模块的选项就是把它们分割成单独的区。子模块与分区的的区别是子模块结构对于模块的用户是可见的,允许用户单独只导入那些想要用的子模块。而分区是用于在内部结构化模块。分区并不暴露给模块的用户。所有在模块接口分区中声明的分区必须最终要被模块接口主文件导出,直接或间接。一个模块总是只有一个这样的主模块接口文件,就是包含了export module name声明的那个接口文件。
模块分区通过用:来分割模块名与分区名的方式来生成。分区名可以是任何合法的标识符。例如,前面章节中的DataModel模块可以使用分区而不是子模块来重新结构化。下面是在datamodel.person.cppm模块接口分区文件中的person分区:
export module datamodel:person; // datamodel:person partition
export namespace DataModel { class Person { /* ... */ }; }
下面是address分区,包含了一个缺省的构造函数:
export module datamodel:address; // datamodel:address partition
export namespace DataModel
{
class Address
{
public:
Address();
/* ... */
};
}
不幸的是,当与分区结合使用实现文件时有个警告:只能有一个特定分区名字的文件。这样的话,以下面的声明开头的实现文件的格式是不对的:
module datamodel:address;
倒过来,可以只放置address分区实现在datamodel模块实现文件中,如下:
module datamodel; // Not datamodel:address!
import std;
using namespace std;
DataModel::Address::Address() { println("Address::Address()"); }
警告:多个文件不能有同一个分区名。拥有多个文件接口分区文件的同一分区名是不合法的,在模块接口分区文件中的声明的实现不能放到拥有同一个分区名的实现文件中。反过来,只是把那些实现放到一个模块实现文件中。
要记住的关键一点是:当在分区中进行模块结构化时,每个模块接口分区必须最终要被模块接口主文件导出,直接或间接。要导入一个分区,只是指定以:开头的分区名字,例如:import :person。像import datamodel:person这样是非法的。记住,分区并不暴露给模块的用户;分区只在内部结构化模块。因此,用户不能导入特定的分区;只能导入整个模块。分区只能在模块自身内部被导入,所以在:之前指定模块名字是冗余的(也是非法的)。下面是datamodel模块的主模块接口文件:
export module datamodel; // datamodel module (primary module interface file)
export import :person; // Import and export person partition
export import :address; // Import and export address partition
import std;
export namespace DataModel { using Persons = std::vector<Person>; }
该结构化的分区datamodel模块可以使用如下:
import datamodel;
int main() { DataModel::Address a; }
注意:分区用于内部结构化模块,模块外部不可见。因此,模块的用户无法导入特定分区;必须导入整个模块。如果想要允许用户有选择地导入模块的部分,可以使用子模块而不是分区。
前面解释过module name声明隐式包含了一个import name声明。对分区来说不是这样。
例如,datamodel:person分区没有隐式import datamodel声明。在这个例子中,甚至不允许添加显式的import datamodel到datamodel:person接口分区文件中。这样做会产生循环依赖:datamodel接口文件包含import :person声明,而datamodel:person接口分区文件包含了import datamodel声明。
为了打破这种循环依赖,可以将在datamodel接口文件中需要的datamodel:person分区的功能移到另外一个分区,该分区按顺序被datamodel:person接口分区文件与datamodel接口文件导入。