编译处理
为了理解编译的流程,需要先了解模块的整体划分,它主要由模块的接口单元,模块实现单元(模块实现单元),模块分块和BMI编译生成的二进制接口文件组成.
也即,模块分为模块的定义和实现两部分,有点类似头文件和cpp文件.其同样也存在着模块的声明和定义一体的情况(在一个文件中).
特别是在小规模应用场景下,不愿意组织成不同文件形式处理(即主分块接口和实现单元),所以又提供了私有模块分块这一情况.
下面以主程序依赖B模块,而B又依赖A的情况说明:
首先,编译器将编译模块的接口单元(B.ixx,包括主模块接口单元,先编译分块接口,然后再依次聚集成主模块接口),生成BMI文件(B.gcm).
即解析接口单元代码,处理导出的声明有效性来生成对应的二进制接口文件(如前的std.gcm).在该文件中,包含着模块定义中对外声明的所有接口信息,可理解为头文件的声明(没有实现内容);
其次,编译器开始处理实现单元,首先验证gcm文件接口声明与实现单元中是否保持一致(比如函数的签名等),然后编译实现单元(如B.cpp)生成目标文件(.o文件),但此过程不会生成新的gcm文件.
再次,编译模块的依赖模块,比如B依赖A模块,则开始检查是否生成A的BMI,如未生成则开始编译A模块.
然后解析B模块中import A中的接口信息与A.gcm中的是否保持一致.编译生成目标文件(A.o)且不生成新的BMI.
即其方式与主编译单元方式一致.
最后,编译主程序并链接B模块,先从B.gcm导入接口信息并验证,如果,则编译主程序生成目标文件(main.o),然后在链接过程中,将三个目标文件即A.o,B.o,main.o链接成可执行文件.
编译器在处理模块间的依赖时,是显式依赖构建依赖图的,从而保证编译的依赖顺序(大家是不是想到了传统的动态库中的手动,控制库的依赖顺序的问题及头文件顺序的问题),避免类似库依赖时顺序逆转就会导致链接错误的情况.
另,模块的接口变化仅处理A模块及其依赖私有模块的编译,而内部变化则只影响模块自身的编译.
因为模块提供了增量编译的功能,所以其避免了重复编译和解析.
与头文件混合处理
引入模块与使用头文件并不排斥,而是可混合存在.
前面例程中已提供了对头文件和模块共同使用的方式.则如何才能更好的整合新老代码呢?
首先,在新工程中按模块接口单元封装引入模块;其次,对强使用,相对简单的旧代码优先引入模块机制;利用命名模块来处理与传统宏定义的冲突;
最后,对一些很少应用,特别复杂的不引入模块.
模块的结构组织
如前,很容易在实际的工程中模块的组织架构处理:
1,抽象对外导出接口及其相关功能描述说明
2,以模块分块和分片段为基础划分出最底层的实现单元
3,以层级(树结构)来处理功能实现中模块(子模块间)的依赖(避免循环依赖和重复导出)
4,划分命名和匿名模块来处理一些特殊场景.
混合编程
新技术的引进往往是渐进性的,模块机制亦是如此.混合编程即可兼顾模块机制的各种优势(如解决依赖顺序,提高编译效率等)又可兼容老的头文件库和框架.
下面给出示例:
cpp
//`legacy.h`
#pragma once
#include <string>
#include <iostream>
class LegacyDemo {
public:
void display(const std::string& msg);
};
int add(int a, int b);
//`legacy.cpp`
#include "legacy.h"
void LegacyDemo::display(const std::string& msg) {
std::cout << "old " << msg << std::endl;
}
int add(int a, int b) {
return a + b;
}
//`new_module.cppm`
export module new_module;
//`全局模块``片段`:包含传统`头文件`
module;
//开始`全局模块``片段`
#include "legacy.h"
export module new_module;
#include <iostream>
#include <string>
export namespace ModuleDemo {
export int add(int a, int b);
export class DemoClass {
private:
int ret_ = 0;
public:
DemoClass();
int sub(int a, int b);
};
}
//`new_module.cpp`
module new_module;
//`#include<cmath>`
namespace ModuleDemo {
int add(int a, int b) {
return a + b;
}
DemoClass::DemoClass(){}
int DemoClass::sub(int a, int b) {
ret_ = a - b;
return ret_;
}
}
//`main.cpp`
#include <iostream>
#include "legacy.h"
import new_module;
int main() {
//使用`头文件`机制
std::cout << "legacy add : " << add(10, 20) << std::endl;
LegacyDemo ld;
ld.display("Hello include!");
//使用`模块机制`
std::cout << "moudle add : " << ModuleDemo::add(30, 40) << std::endl;
ModuleDemo::DemoClass calc;
std::cout << "计算器结果: " << calc.sub(5, 6) << std::endl;
return 0;
}
说明:如果在模块的实现文件中增加了未使用的头文件,在直接用命令编译时没有问题但使用CMake``编译时则会报编译错误.