背景

先看一段使用了module的小程序:

//gemfield.cpp
export module gemfield;
import <iostream>;
export void whoami(){
    std::cout<<"I am Gemfield."<<std::endl;
}

//main.cpp
import gemfield;
int main(){
    whoami();
}

使用g++编译(注意module是C++20加的,所以编译要加上-std=c++20;又因为import只有在开启-fmodules-ts下才能使用,所以还要加上-fmodules-ts):

g++ -fmodules-ts -std=c++20 gemfield.cpp main.cpp -o gemfield

但是编译报错:

/usr/include/c++/11/iostream: error: failed to read compiled module: No such file or directory
/usr/include/c++/11/iostream: note: compiled module file is ‘gcm.cache/./usr/include/c++/11/iostream.gcm’
/usr/include/c++/11/iostream: note: imports must be built before being imported
/usr/include/c++/11/iostream: fatal error: returning to the gate for a mechanical issue

这是因为标准库还没有被转换成module,使用如下命令进行转换:

g++ -c -fmodules-ts -x c++-system-header  -std=c++20 iostream string vector

然后再编译,程序编译成功。上面就是使用C++ modules的一个最简化的例子。

Modules的好处

从上面的程序示例中就可以看出,module替代的是之前的头文件。因此modules带来的好处自然就是头文件机制带来的坏处,主要有这么几点:

  1. 避免了include头文件重复预处理的过程,加快了编译速度;
  2. 避免了头文件中同名宏定义;
  3. 结构化及方便工具分析,得益于import和module关键字,工具能更快更有效的理解项目代码的依赖图;
  4. 源代码不再需要提供接口(头文件)+实现的形式。注意1:module当然也可以提供接口+实现的方式;注意2:这不是说头文件就完全不需要了,有些header-only的库还是有存在价值的,毕竟同一份源码就一个版本;而以二进制形式提供的库,同一份源码需要有多份不同的编译版本。

入门Modules语法

仅仅通过背景章节中的小例子,我们也能够看出module的一些基本语法:

  • export module gemfield就是module的声明,声明了当前的翻译单元是个module unit(这是啥意思?后面讲。放在whoami函数的前面,于是whoami就可以被export并使用;
  • import gemfield 导入gemfield模块,并且使得whoami函数在main.cpp中可见。

下面的例子中:

export module gemfield; // 为命名module gemfield声明了 primary module interface unit
 
// 其它翻译单元如果使用import gemfield, 则whoami() 将可见
export char const* whoami() { return "I am Gemfield"; } 
 
// whoAreYou()则不可见
char const* whoAreYou() { return "CivilNet"; }
 
// one() 和 zero() 都可见
export{
    int one()  { return 1; }
    int zero() { return 0; }
}
 
// Exporting namespaces: gem::english() and gem::chinese() 都可见
export namespace gem{
    char const* english() { return "Hi!"; }
    char const* chinese()  { return "有朋自远方来"; }
}
  • 在module中,import关键字必须出现在export之后,其它所有声明之前
  • 假如module A 中 import了 module B,那么import A也就间接的包含了import B;
/////// A.cpp (primary module interface unit of 'A')
export module A;
export char const* hello() { return "hello"; }
/////// B.cpp (primary module interface unit of 'B')
export module B;
export import A;
export char const* world() { return "world"; }
 
/////// main.cpp (not a module unit)
#include <iostream>
import B;
int main(){
    std::cout << hello() << ' ' << world() << '\n';
}

Module Unit

module unit就是声明了module的翻译单元(translation unit)。分为如下几种:

  • module interface unit:在module unit中声明module的时候使用了export关键字;
  • module implementation unit:所有不是module interface unit 的module init,也就是没有使用export关键字;
  • module partition:声明module时包含了module-partition(参考下面的module partitions章节);
  • module interface partition:首先是一个module interface unit,其次是一个module partition(也就是既有export关键字,又有module-partition);
  • module implementation partition:包含module-partition,但没有export关键字;
  • primary module interface unit:没有partition的module interface unit。一个module只有一个primary module interface unit。

为了预防大型源文件变得难以处理, C++ module可以将单个module分为不同的翻译单元(多份源文件),这些翻译单元合并在一起形成整个module。这种分割方式称之为module partitions。

Module partitions

假设我们不想在同一个模块中包含两个庞大、繁琐且笨拙的函数:

export module gemfield;
//假设下面的函数是庞大的
export const char* getWeibo(){
    return "@在通州";
}
export const char* getZhihu(){
    return "libGemfield";
}

现在我们将上面的代码重构成使用module partitions:

//gemfield.cpp
export module gemfield;

export import :weibo;
export import :zhihu;

//gemfield_weibo.cpp
export module gemfield:weibo;
export const char* getWeibo(){
    return "@在通州";
}

//gemfield_zhihu.cpp
export module gemfield:zhihu;
export const char* getZhihu(){
    return "libGemfield";
}

//main.cpp
import gemfield;
import <iostream>;

int main(){
    std::cout<<getWeibo()<<std::endl;
    std::cout<<getZhihu()<<std::endl;
}

上面的例子中,我们定义了:

export module :import :export import :

其中,getWeibo、getZhihu函数都属于gemfield module。且:

  • gemfield.cpp是primary module interface unit。注意:primary interface unit必须直接或间接的export出当前module的所有的interface partitions,否则程序错误;
  • gemfield_weibo.cpp、gemfield_zhihu.cpp是module interface partitions;
  • main.cpp是常规的翻译单元。

module implementation unit

module关键字前没有export关键字的module unit,就是module implementation unit。在implementation unit中声明的实体,只在该module中可见。举个例子:

//gemfield.cpp
export module gemfield;
import :weibo
import :zhihu

export const char* getWeibo();
export const char* getZhihu();

// gemfield_weibo.cpp
module gemfield:weibo;
const char* getWeibo(){
    return "@在通州";
}

// gemfield_zhihu.cpp
module gemfield:zhihu;
const char* getZhihu(){
    return "libGemfield";
}

相比于再往前一个的例子,这个例子做了如下改动:

  • 在primary interface unit(也就是gemfield.cpp)中,没有使用export关键字来import partition;
  • 在partition中(gemfield_weibo.cpp、gemfield_zhihu.cpp),也没有使用export关键字,这就使得这俩翻译单元从之前的module interface partitions变成了implementation partitions;
  • implementation partitions中的函数也没有export关键字了,注意:在implementation unit中是否使用export关键字没关系;
  • 在primary interface unit中,这俩函数的声明使用了export关键字,这就使得这俩函数也是接口的一部分(即使它们还没有被定义)。

实际上,在implementation units中不允许export关键字的存在。那么module implementation unit有什么好处呢?那就是,implementation units中的改动不会导致使用该module的其它源文件重新编译,从而大大加快整个工程的编译速度。

Header Units

在背景章节的小例子中,我们import 了<iostream>,iostream已经是module了吗?非也,它是Header Units。Header Units说的是,可以把include header直接改成import header:

#include <vector>      => import <vector>;
#include "gemfield.h"  => import "gemfield.h"; 

import查找头文件的方式遵循了include,但是,import又不像是include(只是文本替换),而是会生成一些中间物,并试图将其看成是真正的module——这种方式将导致编译速度超过precompiled headers。由于precompiled headers不是C++标准,各个编译器实现也不一样,因此header units就更吃香了。

但是美中不足的是,不是所有的header都可以import(C++标准保证了标准库头文件都可以import)。

Global module fragment

在module中include头文件会带来一个问题,举个例子:

export module gemfield;
#include <standard_header.h>

这样做会将standard_header.h中的每个符号绑定到module gemfield上,很显然,这并不符合预期。那怎么办呢?一种解决方案就是使用上面说的header units:

export module gemfield;
import <standard_header.h>

但这也有两个问题:

  • header unit 不会考虑当前翻译单元的preprocessor的状态,也就是当前翻译单元中的#define 宏对header units不起作用——这就产生了宏的单向隔离;
  • 不是所有的header都可以import。

是时候有请global module fragment了,语法为:

module;
//#include <...>
//...
module gemfield;

第一行的module; 告诉编译器,虽然这是一个module,但是后面的内容先按照非module来处理,直到遇到module gemfield。于是,在module; module gemfield之间的代码(也就是global module fragment),将像从前那样进行预处理,且将被绑定到global module上(一个默认的隐式module,也是main函数所在的module)。

但请注意,只有preprocessor directive可以出现在global module fragment中,比如:

module;
extern void gem(); //错误,这不是preprocessor directive。

export module gemfield;

你可以改为:

//gem.h
extern void gem();

//gemfield.cpp
module;
#include "gem.h"   //正确,#include是 preprocessor directive。

export module gemfield;

Private module fragment

module : private;

举个例子:

export module gemfield;
export int civilnet();

module : private; 

int civilnet() {          // civilnet函数的定义对别的翻译单元不可见
    return 7030;
}

Primary module interface unit后面可以跟个private,表明接口部分的内容到此结束。后面的内容将不再影响别的翻译单元。

Module ownership

通常情况下,如果一个名称声明出现在module的声明之后,那么这个名称就会绑定到这个module上。并且,如果名称的声明被绑定到这个module上,那么它的定义也只能绑定到这个module上。

如果绑定到named module上的声明没有被export,那么该名称具有module linkage,这一点在Gemfield:C++的符号链接一文中已经说过:

export module gemfield;
int f() { return 0; } // f 具有module linkage

但是,前面说的是“通常情况下”名称会绑定到前面的module上,那什么情况下不会绑定呢?有两个例外:

  • 当名称是namespace时;
  • 名称声明时使用了language linkage specification。
export module gemfield;
namespace ns // ns没有绑定到gemfield上
{
    export extern "C++" int f(); // 函数f没有绑定到gemfield上
           extern "C++" int g(); // 函数g没有绑定到gemfield上
    export              int h(); // 函数h绑定到gemfield上
}

因此ns::h必须定义在module gemfield中,而ns::f、ns::g则可以定义到module之外。不过,namespace可以显式的进行export:

export namespace CivilNet {
   int gemfield = 7030; // 正确,export了CivilNet::gemfield
   static int gemfield2 = 7030; // 错误,internal linkage的符号不能export(参考下面)
}

还有什么符号不能被export?

  • 具有internal linkage的符号,比如static变量/函数,比如匿名命名空间中的符号:
namespace {
   export void libGemfield() {}  //错误,匿名namespace
   export int gemfield = 7030; //错误,匿名namespace
   export class Gemfield {}; //错误,匿名namespace 
}

export static void gemfield() {} //错误,static函数
export static int gemfield = 7030; //错误,static变量
  • 要export的符号必须在首次声明时就export:
export class Gemfield;  // 正确
export class Gemfield;  // 正确,只不过重复了
class Gemfield;  // 隐式带有export关键字

class Gemfield { // 隐式带有export关键字
    int g;
    int e;
};

class CivilNet; // 正确,没有export
export class CivilNet; // 错误! 要export的符号必须在首次声明时就export!
  • 要export的符号必须在namespace层级,换言之,不能是在class内、函数内等。