c++20
中引入模块之前头文件是用来可重用代码提供接口的。但是头文件有很多问题,比如避免多次包含同一头文件,以及确保头文件以正确的顺序包含。此外,只需要简的 #include
,就会新增数万行编译器必须处理的代码。如果多个源文件都包含如 #include
,那么所有的这些编译单元都会变的更大,这还仅仅只包含了一个头文件,要是如果包含更多的,如 string、vector
等。那么将变得异常庞大。
模块解决了所有这些问题,甚至更多,模块导入的顺序并不重要,模块只需要编译一次,而不是像头文件那样需要反复编译;因此,可以大大的缩短编译时间。模块中的某些修改,(比如在模块接口文件中修改一个导出函数的实现),不会触发该模块的用户重新编译!模块不受任何个外部定义的宏的影响。并且在模块内部定义的任何宏对模块外部的任何代码都不可见。
如果要使用模块需要编译器的支持,本文使用的是gcc11
。
模块接口文件,是为模块提供的功能定义的接口。模块接口文件通常是以.cppm
作为扩展名。模块以声明开头,声明该文件正在定义一个具有特定名称的模块。这成为模块的声明。模块名称可以是任何有效的c++标识符,名称可以包含.
,但是不能以.
开头或者结尾,也不能在一行中包含多个.
。
模块需要显示的声明要导出的内容,当用户代码导入模块时,哪些内容是可见的。使用export
关键字从模块中导出实体(如,类、函数、常量、其他模块等)。任何没有从模块导出的内容只在该模块可见。所有导出实体的集合称为模块接口。如果你想要使用某个模块中的功能,则需要导入这个模块。这是通过一条import
声明做到的。
示例:一个名为person.cppm
的模块接口文件,它定义了person
模块,并导出了person
类
// person.cppm
export module person; // 注意要写在所有import前面
import ;
import ;
using namespace std;
export class Person
{
public:
Person(string fName, string sName) : m_firstName(move(fName)), m_secondName(move(sName)) {}
const string &getFirstName() const { return m_firstName; }
const string &getSecondName() const { return m_secondName; }
private:
string m_firstName;
string m_secondName;
};
通过导入person
模块来使用Person
类
// test.cpp
import person;
import ;
using namespace std;
int main()
{
Person person("zhang", "san");
cout << person.getFirstName() << ", " << person.getSecondName() << endl;
return 0;
}
所有的c++
头文件,都是所谓的可导入头文件,可以通过import
声明导入。如果在导入某个头文件编译报错为没有这个文件时候,需要加上编译选项 -xc++-system-header [需要添加的头文件名字]
, 需要注意的是对于c语言的某些头文件兼容还不够好,并不是所有的头文件都可以导入,这时候,需要使用传统的#include的方式加入。而且它必须出现在全局模块片段中,在任何命名模块之前!
在标准术语中,从已命名的模块声明到文件末尾的所有内容称为模块主体(module purview
)。
几乎所有内容都可以从模块导出,只要它有一个名称。比如类定义、函数原型、枚举类型、using
声明和指令、名称空间等。如果用export
关键字导出名称空间,则该名称空间中所有内容也将自动导出。
一个模块可以被拆分为一个模块接口文件和一个或者多个模块实现文件。模块实现文件通常以.cpp
作为扩展名。我们可以将前面的person
模块拆分为接口和实现文件。
// person.cppm
module;
#include
export module person;
import ;
import ;
using namespace std;
export class Person
{
public:
Person(string fName, string sName);
const string &getFirstName() const;
const string &getSecondName() const;
private:
string m_firstName;
string m_secondName;
};
// person.cpp
module person;
using namespace std;
Person::Person(string fName, string sName) : m_firstName(move(fName)), m_secondName(move(sName)) {}
const string &Person::getFirstName() const { return m_firstName; }
const string &Person::getSecondName() const { return m_secondName; }
// main.cpp
import person;
import ;
using namespace std;
int main()
{
Person person("zhang", "san");
cout << person.getFirstName() << ", " << person.getSecondName() << endl;
return 0;
}
注意实现文件没有person
的模块的导入声明,module person
的声明隐式包含import person
的声明。还要注意的是,实现文件中没有
的导入声明,即使它在方法的实现中使用了string
,正是由于隐式的import person
,并且因为这个实现文件是同一个person
模块的一部分,所以它隐式的从模块接口文件中继承了
的导入声明。与之相反,想main.cpp
文件添加import person
的声明并不会隐式的继承
的导入声明,因为main.cpp
不是person
模块的一部分。
警告:模块实现文件不能导出任何内容,只有模块接口文件可以。
当使用头文件而不是模块时,强烈建议只把声明放在头文件.h中,并将所有实现移到源文件.CPP中。原因之一是可以缩短编译时间。如果将实现文件放在头文件中,任何的代码变更,哪怕只是修改注释,都需要重新编译包含该头文件的所有其他源文件。对于某些头文件,可能会影响整个代码库,导致整个程序完全重新编译。将实现放到源文件中,在不涉及头文件的情况下,对这些实现进行更改,这样只需要重新编译单个源文件。
模块的工作方式是不同的。 模块接口仅由类定义、函数原型等组成,虽然这些实现在模块接口文件中,但模块接口中不包含任何函数或方法的实现。因此,只要变更不涉及模块接口的部分,变更模块接口文件中的函数或方法的实现是不需要重新编译该模块的,比如函数头(函数名、参数列表和返回类型)。但有两个例外,使用inline关键字标记的函数或方法和模块的定义。对于这两者,编译器需要在编译使用他们的用户代码时知道其完整实现。因此,内联函数/方法或模块定义的任何变更都可能引发用户代码的重新编译。
注意:当头文件中的类定义中包含方法的实现,那么这些方法是隐式的内联,尽管没有使用inline关键字标识他们。对于在模块接口文件中的类定义里的方法实现来说,却不太一样。如果要变成内联方法,就需要使用inline关键字显示标识他们。
将接口从实现中分离可以通过两种方式实现。第一种方法是上面所述的,将模块拆分为接口文件和实现文件。第二种方法是在单个模块的接口文件中拆分接口和实现。
module;
#include
export module person;
import ;
import ;
using namespace std;
export class Person
{
public:
Person(string fName, string sName);
const string &getFirstName() const;
const string &getSecondName() const;
private:
string m_firstName;
string m_secondName;
};
// 实现
Person::Person(string fName, string sName) : m_firstName(move(fName)), m_secondName(move(sName)) {}
const string &Person::getFirstName() const { return m_firstName; }
const string &Person::getSecondName() const { return m_secondName; }
在上面的main.cpp
中,没有导入
模块,却可以访问person
中的string
类型。但是使用string
创建string
对象却无法编译。例如:
import person;
using namespace std;
int main()
{
string str; // 错误,无法编译
Person person("zhang", "san");
auto length{person.getFirstName().length()}; // 可以编译
return 0;
}
注意:由于模块是c++20新引入的内容,所以编译的时候需要加一些编译选项当编译报错的时候。本文使用的编译命令如下:
g++ -std=c++20 -fmodules-ts -x c++ person.cpp person.cppm main.cpp
当然有时候还需要添加其他编译选项,如果你使用的gnu,那么请参考[C++ Modules (Using the GNU Compiler Collection (GCC))](https://gcc.gnu.org/onlinedocs/gcc/C_002b_002b-Modules.html#:~:text=Compiling a module interface unit produces an additional,(DAG). You must build imports before the importer.) 适当增添编译选项。
原创不易,未经允许请勿转载!