C++ Module(Base on MSVC)

发布于 2023-05-02  129 次阅读


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();
}
最后更新于 2023-05-02