C++ 20 Modules 尝鲜

Modules 是 C++ 20 标准里面非常重要的一个新特性,引进了在其它编程语言中常用的 import 关键词,让 C++ 在几十年前就有的 #include 基础上支持更现代的模块设计,减轻传统的头文件带来的一些问题,提升编译的速度。于是我尝试了一下用 Modules 写一个简单的 Hello World。

安装 Clang 10

由于 Modules 是 C++ 20 的标准,因而较老的编译器并不一定能够支持这一特性,推荐最新的 Clang 10.0.0。首先安装必要的运行时与编译库:

1
$ sudo apt install build-essential libncurses5

可以到 LLVM 的 下载页面 选择自己的平台下载,解压后即可在 bin 目录中找到 Clang 及 LLVM 的相关可执行文件。建议使用 Linux 平台以避免一些使用问题。也可以直接用 LLVM 提供的安装脚本

1
$ bash -c "$(wget -O - https://apt.llvm.org/llvm.sh)"

Hello World Module

接着我们创建一个简单的 helloworld.cpp 文件,实现一个最简单的 helloworld Module,通过 export 关键词将函数变量提供给外部程序使用:

1
2
3
/// helloworld.cpp
export module helloworld;
export auto hello() { return "Hello C++ 20!"; }

接着创建 main.cpp,使用 import 关键词导入 helloworld Module:

1
2
3
4
5
6
7
8
/// main.cpp
#include <iostream>
import helloworld;

int main() {
    std::cout << hello() << std::endl;
    return 0;
}

编译分为两步,首先我们需要将用到的模块编译成 BMI(Binary Module Interface),即先把 helloworld.cpp 模块编译成 .pcm(precompiled module)文件,再编译 main.cpp。注意这里需要 -std=c++2a,用 -std=c++20 就有问题,迷…… 用 g++10 编译命令似乎更简单,估计等 Clang 对 C++ 20 的标准支持更完善了之后,编译会更方便。

用 Clang 10 编译和运行:

1
2
3
4
5
$ clang++ -std=c++2a -c helloworld.cpp -Xclang -emit-module-interface -o helloworld.pcm
$ clang++ -std=c++2a -stdlib=libc++ -fprebuilt-module-path=. main.cpp helloworld.cpp

$ ./a.out
Hello C++ 20!

用 import 代替 #include

进一步,我们可以将上面的 #include 也用 import 来代替。注意最后需要添加分号 ;

1
2
3
// main.cpp
import <iostream>;
import helloworld;

这时候我们在编译的时候需要额外两个 flag:-fimplicit-modules-fimplicit-module-maps,具体可以参考 Clang 的文档。现在 Clang 还不支持 Semantic import,也就是无法使用 import std.io 语法(MSVC 支持)。这里我们直接用 import 替换 #include,去除 #include 使语法暂时保持一致。

由于之前的 helloworld 模块并没有任何变化,因此只需要重新编译一下第二步即可:

1
$ clang++ -std=c++2a -stdlib=libc++ -fimplicit-modules -fimplicit-module-maps -fprebuilt-module-path=. main.cpp helloworld.cpp

Module 中使用 namespace

在一个模块里,我们也能 export 命名空间,下面的例子演示了如何导出和使用 helloworldnamespace,避免带来全局函数的干扰。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
/// helloworld.cpp
module;
import <cstdio>;
export module helloworld;

export namespace helloworld {
    int global_data;

    void say_hello() {
        std::printf("Hello Module! Data is %d\n", global_data);
    }
}
1
2
3
4
5
6
7
/// main.cpp
import helloworld;

int main() {
    helloworld::global_data = 123;
    helloworld::say_hello();
}

编译命令和之前基本相同,只不过因为模块里面用到了 import <cstdio>,因此在编译模块的时候需要指定 -stdlib=libc++ 以及 -fimplicit-modules-fimplicit-module-maps

1
2
3
4
5
$ clang++ -std=c++2a -stdlib=libc++ -fimplicit-modules -fimplicit-module-maps -c helloworld.cpp -Xclang -emit-module-interface -o helloworld.pcm
$ clang++ -std=c++2a -stdlib=libc++ -fimplicit-modules -fimplicit-module-maps -fprebuilt-module-path=. main.cpp helloworld.cpp

$ ./a.out
Hello Module! Data is 123

Module 的规范

一个推荐的 Module 规范结构如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
module;
#include <standard library header>
#include "library not ready for modularization"
...
export module top.middle.bottom;

import modularized.standard.library.component;
import other.modularized.library;
...

#include "module internal header" // beware!

non-exported declarations;
...
export namespace top {
    namespace middle {
        namespace bottom {
        exported declarations;
        ...
}}}

写在后面

Module 还有一系列的特性,比如将接口(Interface)和实现(Implementation)分离,模块分割(Module Partitions)等等,更多高级的用法可以参考视频:Modules the beginner's guide - Daniela Engert - Meeting C++ 2019

但是目前主流的编译器对于 C++ 20 的标准还没有支持完备,因此很多功能也只能先“尝鲜”,等到一段时间后或许这些新的标准会像 C++ 11 一样让 C++ 再次焕发新的活力。

参考:

加载评论