Module
C++的Module是C++20引入的一个重大更新,它不仅仅改变了传统include的宏泛滥的问题而且大大加快了编译速度(不再是传统的递归拷贝到编译单元再编译)。
include
在C++引入Module之前,include是这么工作的。如果我们有一个test.h和test.cpp,它们的内容如下所示:
// test.h
#define GLOBAL_INIT 0
extern int global_variable;
void func();
// test.cpp
#include "test.h"
int global_variable = GLOBAL_INIT;
void func()
{
printf("%d", global_variable);
}
在预处理阶段,预处理器会扫描源代码,寻找以#开头的预处理指令。在以上代码中,预处理器会找到#define GLOBAL_INIT 0和#include "test.h"两个预处理指令。
对于#include "test.h"这个指令,预处理器会在源文件所在的目录中查找名为test.h的文件,并将其内容展开到该指令所在的位置。展开后的代码如下:
// test.cpp
#define GLOBAL_INIT 0
extern int global_variable;
void func();
int global_variable = GLOBAL_INIT;
void func()
{
printf("%d", global_variable);
}
对于#define GLOBAL_INIT 0这个指令,预处理器会将GLOBAL_INIT定义为宏,并将其值设置为0。
// test.cpp
extern int global_variable;
void func();
int global_variable = 0;
void func()
{
printf("%d", global_variable);
}
以上的代码会有三个主要问题:
- 编译时间长。这是因为对于每个cpp文件,都需要拷贝头文件并独立编译,事实上就像静态库一样,一些cpp可以单独作为一个“库”,编译器可以在项目中导入模块的每个位置重复使用该文件。
- 头文件污染。在头文件定义的宏、预处理器指令等等外部都是可见的。
- 不能直接控制哪些函数可见。
tutorial
为了解决上述问题,C++引入了模块。
对于标准库的module支持,需要你在visual studio installer的C++桌面开发工具中加入适用于v143生成工具的C++模块。
这样你就可以写出下面这样的代码了
import std.core;
int main() {
std::cout << "Hello, World!" << std::endl;
return 0;
}
std.core提供除了std.threading, std.filesystem, std.regex, std.memory之外的其他内容。
具体内容参见: MSVC Module
- 注意,对于现有的std.core可能会有"C++ IFC文件具有不受支持的版本 0.43"红色下划线,但是代码的编译不受影响。
- 如果你想去掉红线,可以使用std.compat代替std.core.
但是这显然不够coooool,我们要做自己的Module!
msvc module file
在VS内,后缀为ixx的文件被视为一个模块文件。ixx有着cpp和h共有的功能,十分强大。
只需要创建一个ixx文件就可以得到一个默认模块的默认实现代码。
现在你可以得到这样的代码
export module Module;
export void MyFunc();
export
我们尝试修改这个模块
首先加入一个简单的数学运算namespace
export module Module;
namespace math {
int add_impl(int a, int b) // 注意:add_impl需要在add上面,否则你需要先声明add_impl
{
return a + b;
}
export int add(int a, int b) {
return add_impl(a, b);
}
}
现在我们在main函数里调用这个add
import std.compat;
import Module;
int main()
{
using namespace std;
cout << math::add(1, 2);
// cout << math::add_impl(1, 2); // ERROR:调用未导出的函数
}
我们也可以导出模板函数,往Module.ixx内加入一个Print函数
// module.ixx
export module Module;
namespace math {
int add_impl(int a, int b);
export int add(int a, int b) {
return add_impl(a, b);
}
int add_impl(int a, int b) // 注意:add_impl需要在add上面
{
return a + b;
}
}
import std.compat;
export
template<class...Ts>
void print(Ts&&... variables)
{
(std::cout << ... << variables) << std::endl;
}
我们就可以打出一加一啦
import Module;
import Module; // ok, but error in include
import std.compat;
int main()
{
using namespace std;
print("1 + 2", " = ", math::add(1, 2));
}
export import
如果我们使用一个新的模块NewModule,这个模块import了模块Module,并且希望导出Module的内容。我们可以使用export import。
export module NewModule;
export import Module;
export const char* say()
{
return "newmodule"; // 放在常量区,所以不存在声明周期(和主进程一样长)
}
现在在main函数中就可以使用这两个模块的内容了
import NewModule;
int main()
{
print("hi, my name is ", say());
print("1 + 2", " = ", math::add(1, 2));
}
如果我们只想导出模块的一部分内容,我们可以使用这样的语法
export module NewModule;
import Module;
export
{
template<typename ...Ts>
void print(Ts&&...);
};
export const char* say()
{
return "newmodule";
}
现在我们在main函数中就只能访问到Module中的print了。
import NewModule;
int main()
{
print("hi, my name is ", say());
// print("1 + 2", " = ", math::add(1, 2)); // error
}
submodule
Module还提供一个额外功能,那就是子模块功能。C++的子模块用冒号表示。比如Module:Impl就表示Module的子模块Impl.这个不要和其他语言常用的点混淆。
以std.core为例,其实它只是一个名字,就叫std.core。而不是std的子模块core.
我们使用submodule来搞一个极简的pimpl设计模式。
// module.impl.ixx
export module Module:Impl;
export class MathImpl
{
int val = 0;
public:
void add(int item) {
++this->val;
}
int get()
{
return val;
}
};
export module Module;
import std.compat;
import :Impl; // equal to Module:Impl
export class Math
{
std::unique_ptr<MathImpl> ptr;
public:
Math() : ptr(std::make_unique<MathImpl>()) {}
void add(int i)
{
ptr->add(i);
}
int get()
{
return ptr->get();
}
};
private module
模块私有片段一般用于分离定义与实现,下面是一个例子:
export module Math;
export int add(int, int);
module: private;
int add(int a, int b)
{
return a + b + 0;
}
global module
对于那些使用预处理器的部分,我们需要与真正的模块代码隔离。处理#开头的预处理部分--- ---这也就是全局模块的作用。
一下是例子
// header.h
#pragma once
#define __M_FILE__ __FILE__
// hello.ixx
module;
#include "header.h"
export module Hello;
export const char* get_hello()
{
return __M_FILE__;
}
// math.ixx
module;
#include "header.h"
export module Math;
export const char* get_math()
{
return __M_FILE__;
}
// main.cpp
import std.compat;
import Math;
import Hello;
int main()
{
std::cout << get_math() << "\n"
<< get_hello();
}