【一人 C++20 Advent Calendar 2019】C++ にモジュールがやってくる!【23日目】

一人 C++20 Advent Calendar 2019 23日目の記事になります。

C++ にモジュールがやってくる!

ついに C++ にモジュールがやってきます! モジュールとはヘッダーファイル・インクルードに置き換わる『外部ファイル・実装を取り込む新しい仕組み』になります。
よくわからんので実際に簡単なコードを書いてみます。

今までのヘッダーファイル・インクルード

これまではヘッダーファイルで宣言や定義を行い、それを include することで外部ファイルを取り込んでいました。

// math.h
#ifndef MATH_H
#define MATH_H

template<typename T, typename U>
auto
plus(T a, U b){
    return a + b;
}

#endif /* MATH */
// main.cpp
#include "./math.h"
#include <iostream>

int
main(){
    std::cout << plus(1, 2) << std::endl;
    std::cout << plus(3.14, 3.14) << std::endl;

    return 0;
}
/*
output:
3
6.28
*/

モジュールを使った実装

モジュールを使用すると以下のように置き換えることができます。

// math.cppm
// モジュールの宣言
export module math;

// エクスポートの宣言
export template<typename T, typename U>
auto
plus(T a, U b){
    return a + b;
}
// main.cpp
// インポートの宣言

// エクスポート宣言した関数が使用できるようになる
import math;
#include <iostream>

int
main(){
    std::cout << plus(1, 2) << std::endl;
    std::cout << plus(3.14, 3.14) << std::endl;

    return 0;
}
/*
output:
3
6.28
*/

基本的には

  • エクスポートで実装
  • インポートで取り込み

っていう使い方ですかね。 見てわかるように今までの書き方とは全然違います。
わかりやすいところだとインクルードガードも必要なくなり、 include のようなファイルパス指定もなくなります。

ビルド方法

インクルードする場合はファイルを指定して展開するだけなので簡単な構成の場合のビルドはそこまで難しくなかったのですが、モジュールを使った場合は少し複雑になります。
clang 8 で試してみたんですが以下のようにビルドします。

# clang 8 での動作確認
$ clang++- --version
clang version 8.0.1-svn369350-1~exp1~20190820121219.79 (branches/release_80)
Target: x86_64-pc-linux-gnu
Thread model: posix
InstalledDir: /usr/bin
# モジュールファイルを指定してプリコンパイル済みモジュールを生成する
$ clang++ -std=c++2a -fmodules-ts --precompile math.cppm -o math.pcm
# プリコンパイル済みモジュールからバイナリファイル(.o) を生成
$ clang++ -std=c++2a -fmodules-ts -c math.pcm -o math
# 実行ファイルを生成
$ clang++ -std=c++2a -fmodules-ts -fprebuilt-module-path=. math.o main.cpp -o math

モジュールを取り込んで実行バイナリを生成するまでに3つのステップあります。

1. モジュールファイルからプリコンパイル済みモジュールを生成する
2. プリコンパイルモジュールからバイナリファイル(.o)を生成
3. 2. のバイナリファイルを取り込んで実行バイナリを生成

このような流れで最終的な実行バイナリを生成します。
また clang でビルドする場合は -fmodules-ts オプションが必要になります。
このビルドの仕方はここに書いてあるやり方を参考にしています。
他にも手段はあるんですかね? あとこの仕組みだとコード補完や lint で使用する静的コード解析がどうなるのかが気になりますね。
プリコンパイル済みモジュールを読み込んで静的コード解析する感じになるんですかね?

その他の書き方

他にも以下のように namespace を区切った書き方もできます。

C++17

// lib.h
#ifndef LIB_H
#define LIB_H

#include <string>
#include <iostream>

namespace lib{

struct user{
    std::string name;
    int age;
};

inline std::ostream&
operator <<(std::ostream& os, user u) {
    os << "name: " << u.name << ", age: " << u.age;
    return os;
}

inline int
fact(int n){
    return n > 1 ? n * fact(n - 1) : n;
}

}  // namespace lib

#endif /* LIB */
// main.cpp
#include "./lib.h"

int
main(){
    auto homu = lib::user{ "homu", 14 };
    auto mami = lib::user{ "mami", 14 };

    std::cout << homu << std::endl;
    std::cout << mami << std::endl;
    std::cout << lib::fact(5) << std::endl;

    return 0;
}

C++20 + モジュール

// lib.cppm
#include <string>
#include <iostream>

export module lib;

// namespace 内の定義がエクスポート宣言される
export namespace lib{

struct user{
    std::string name;
    int age;
};

inline std::ostream&
operator <<(std::ostream& os, user u) {
    os << "name: " << u.name << ", age: " << u.age;
    return os;
}

inline int
fact(int n){
    return n > 1 ? n * fact(n - 1) : n;
}

}  // namespace lib
// main.cpp
#include <iostream>

import lib;

int
main(){
    auto homu = lib::user{ "homu", 14 };
    auto mami = lib::user{ "mami", 14 };

    std::cout << homu << std::endl;
    std::cout << mami << std::endl;
    std::cout << lib::fact(5) << std::endl;

    return 0;
}
/*
output:
name: homu, age: 14
name: mami, age: 14
120
*/

慣れるまでは結構大変そうですがモジュールを使うと C++ の書き方がすごく変わりそうですね。
今までの概念ががらっと変わりそう…。
ここで書いたような例はモジュールのほんの一部でしかないので詳しくはこのスライドを読んでみるといいと思います。
まだ標準ライブラリとかには対応してないんですが、数年後の C++ は全然違うものになってそうですねー色んな意味で楽しみです。

参照