コンテンツにスキップ

最終更新:2026-10-12

C++20 モジュール Modules

自作のヘッダファイルやサードパーティライブラリを使うとき、通常は#includeディレクティブで**ライブラリ**を取り込みます。これは皆さんがよく使う方法です。この方法は実際には、あるソースファイル(ヘッダファイル)のすべてのコードを別のファイルにコピーすることです。

すると、次のような問題に直面します:

  • ソースファイルが同じ翻訳単位の中で複数回インクルードされる可能性があるため、通常は#pragma onceや#ifndefを使って、ソースファイルが同じ翻訳単位の中で複数回インクルードされるのを防ぎます。
  • コードのコピーによりコンパイル時間が長くなります。ヘッダファイルを1つ修正すると、そのヘッダファイルを間接的にインクルードしているファイルが再コンパイルされます。
  • #includeの順序の問題。ときどき不可解なコンパイル問題に遭遇します。

C++20 は、#include ディレクティブに代わる新しい方法として、モジュールを導入しました。

以下、モジュールを深く学んでいきましょう。

1.モジュールユニット

C++ モジュールは1つ以上の翻訳単位(tu)で構成され、その中にはモジュール宣言のための特定のキーワードが含まれます。このような翻訳単位をモジュールユニットと呼びます。

モジュールユニットでない翻訳単位は、グローバルモジュールの一部とみなされます。グローバルモジュールは匿名で、インターフェースを持たず、通常の非モジュールコードを含みます。

1.1 Global Module Fragment

モジュールユニットは、グローバルモジュールフラグメントを**プレフィックス**として持てます。ヘッダファイルをインポートできない場合(特にヘッダファイルがプリプロセスマクロを設定として使っている場合)、このグローバルモジュールフラグメントで元のコードを直接使えます。

例えば:次のコードで、module;からexport module Foo;までの間が global module fragment です。

module;
#include <iostream>
#ifdef Say
void hello();
#endif
export module Foo;
// purivew
void world();

注意すべき点:このモジュールにグローバルモジュールフラグメントがある場合、最初の宣言はmodule;でなければなりません。つまり、この宣言を他の位置に置くとエラーになります。

1.2 purview

purview はモジュールの全体の範囲と理解できます。モジュール宣言から始まり、翻訳単位の末尾まで広がります。

例えば:hello は範囲外、world と GetData は purview 内にあります。

module;
void hello();
// <- 这里不在Foo模块的purview内
export module Foo;
// <- 在Foo模块的purview内
void world();
export void GetData();

1.3 Private module fragment

主モジュールインターフェースユニットは、プライベートモジュールフラグメントを**サフィックス**として持てます。この部分は主モジュールインターフェースユニットにしか現れず、存在する場合、それが現れるモジュールユニットはそのモジュールの唯一のユニットでなければなりません。その目的は、モジュールのインターフェースと実装を1つの翻訳単位にカプセル化し、実装の詳細を公開しないことです。

例えば:Shape を作ってその面積を計算したいとします。

外部には具体的な Shape を作るインターフェースだけを公開し、共通の面積計算インターフェースを呼び出します。そこで次のようなモジュールが書けます。

export module Shape;

export class Shape {
  public:
   virtual double CalculateArea() = 0;
};

export { std::shared_ptr<Shape> CreateRectangle(double length, double width); }


module :private; // here

class Rectangle : public Shape {
 private:
  double length;
  double width;

 public:
  Rectangle(double l, double w) : length(l), width(w) {}

  double CalculateArea() override { return length * width; }
};

std::shared_ptr<Shape> CreateRectangle(double length, double width) {
  return std::make_shared<Rectangle>(length, width);
}

内部の詳細をすべて private の中に入れます。私の g++ はバージョン 13 で、現時点ではサポートされておらず、次のエラーが出ます:

gcc の現在のサポート状況はここをご覧ください。

https://gcc.gnu.org/projects/cxx-status.html

shape.cppm:14:1: sorry, unimplemented: private module fragment
   14 | module :private;

ローカルの clang はバージョン 16 で、テストしたところ正常に動作しました!

➜  clang++ -std=c++20 shape.cppm --precompile -o shape.pcm  
➜  clang++ -std=c++20 shape.cc -fprebuilt-module-path=. shape.pcm -o shape
➜  ./shape 
area is 2

上の3つの部分のうち、グローバルとプライベートのモジュールフラグメントはモジュールの存在に必須ではなく、purview がモジュールに必須の部分です。

2.モジュールの使用

2.1 モジュールの作成

モジュールの作成はヘッダファイルを定義するのに似ています。こちらもファイルを持ち、一般的に拡張子は .cppm です。このファイルの中でexportとmoduleキーワードを使い、後にモジュール名を続けると、エクスポート可能なモジュールが作成されます。例えば:

// foo.cppm
export module foo;

// main.cc
import foo;

exportキーワードはクラスや変数などにも使えます。通常、次の2つの書き方があります:

export void func();

export {
  void func();
  constexpr double PI{3.14};
};

1つは export キーワードを通常の宣言の前に置く書き方、もう1つはエクスポートブロックで、namespace の書き方に似ており、複数の内容をエクスポートできます。

2.2 エクスポート

ここで重要な問題が関わってきます。何をエクスポートできるのか?

  • variables、classes、structs、functions、namespaces、template functions/classes、concepts はエクスポート可能
  • 内部リンケージのものはエクスポート不可。static 変数や関数、匿名 namespace など。
export static constexpr double PI = 3.14; // 不可导出
  • エクスポート宣言は名前空間レベルで行われなければならない
namespace {
  export void print_no_export() { // 匿名命名空间,不可导出
    std::cout << "print no export" << std::endl;
  }
};
namespace light {
  export void print_export() { // ok
    std::cout << "print export" << std::endl;
  }
};

struct Foo {
  export int a; // 不能导出成员变量
};
  • 名前空間をエクスポートすると、その中のすべての内容が暗黙にエクスポートされる
export class Rectangle {
 private:
  double length;
  double width;
 public:
  Rectangle(double l, double w) : length(l), width(w) {} 

  double CalculateArea() { return length * width; } // 隐式导出
};

export namespace {
  void print_export() { // 隐式导出
    std::cout << "print export" << std::endl;
  }
};

export {
  void func();  // 隐式导出
  constexpr double PI{3.14}; // 隐式导出
};
  • エクスポート対象の最初の宣言はエクスポート宣言でなければならない。以降の宣言と定義に export キーワードは不要。
export class Foo; // ok
export class Foo; // ok,只是会冗余

class Foo { // ok,隐式export
 public:
  void print() { std::cout << "this is foo" << std::endl; }
};

class Bar;  // not export
export class Bar; // 无效,第一个声明已经是不可导出,后续的不可导出


Foo f; // ok
f.print(); // ok
Bar b; // not ok
  • 非エクスポートモジュールではエクスポート不可
module foo;

export void print { }  // error: 'export' may only occur after a module interface declaration

2.3 インポート

これに対応するのがインポートです。インポートにもいくつかのルールがあります。例えば:

  • 自分自身をインポートできない

  • モジュールユニットの中では、すべてのインポートはそのモジュールユニット内のいかなる宣言よりも前に来なければならない。モジュールユニット内の任意の位置でインポートすることはできない。

void func() {}
import shape; // error: post-module-declaration imports must be contiguous

---------------
import shape; // ok
void func() {}
  • グローバルスコープでのインポートのみ許可される
namespace light {
  import shape; // not ok
};
  • 循環インポートは許可されない
// shape.cppm
export module shape;
import circle; // 循环导入

// circle.cppm
import shape;

2.4 モジュール内での include

#include <iostream>はモジュールの中でどう使うのでしょうか?

2.4.1 purview 領域での#include

#includeを import に置き換えます

export module foo;
import <iostream>;

例えば:

g++-13 で次のようにコンパイルします。c++-system-headerの後に iostream を続けることでgcmをコンパイルできます。

g++-13 -std=c++20 -fmodules-ts -x c++-system-header iostream 
g++-13 -fmodules-ts -std=c++20 -x c++ shape.cppm circle.cppm shape.cc

自分のヘッダファイル、例えばconsts.hの場合、モジュールの中に直接#includeできることがわかります。例えば:

export module foo;
#include "consts.h"

2.4.2 Global Module Fragment 領域での#inlcude

#includeやマクロはこの領域で直接使えます。例えば:

module;
#ifdef
#include <iostream>
#endif
export module foo;

3.モジュールの分割

大きなモジュールをより小さなモジュールに分解したいとき、次の2つの方法が使えます:

  • モジュールパーティション。
  • モジュールを複数のファイルに分解することを許可します。ただし、これは使用者からは実際には見えず、使用時は通常どおりモジュールをインポートするだけです。
  • サブモジュール。
  • 大きなモジュールを任意の数のサブモジュールの階層に分解することを許可します。使用者はモジュール全体をインポートすることも、特定のサブモジュールだけをインポートすることも選べます。

3.1 モジュールパーティション

構文:

export module <module-name>:<part-name>;
import :<part-name>; // 可以在import前面添加export导出该分区接口

例えば:shape があり、外部から使うときはimport shapeだけで、対応するインターフェースを呼び出せます。ここでは circle パーティションと rectangle パーティションのインターフェースをそれぞれ使っています。

import shape;

int main() {
    DrawCircle();
    DrawRectangle();
    return 0;
}

shape には2つのパーティション、circle と rectangle があります。そこでモジュール shape.cppm の内容は次のようになります:

export module shape;

export import :circle;
export import :rectangle;

2つのサブパーティションの内容は次の通りです:

// circle.cppm
module;
#include <iostream>
export module shape:circle;
export void DrawCircle() { std::cout << "draw circle" << std::endl; }

--------------
// rectangle.cppm

module;
#include <iostream>
export module shape:rectangle;
export void DrawRectangle() { std::cout << "draw rectangle" << std::endl; }

clang と g++ でコンパイルしてみたところ、clang-16 はコンパイルエラーになり、サポートされていません。

 error: sorry, module partitions are not yet supported

g++-13 はサポートしています。コンパイル時にサブパーティション → 主パーティションの順でコンパイルしないとエラーになることに注意してください。

➜ g++-13 -fmodules-ts -std=c++20 -x c++ shape.cppm circle.cppm rectangle.cppm shape.cc
shape:circle: error: failed to read compiled module: No such file or directory
shape:circle: note: compiled module file is 'gcm.cache/shape-circle.gcm'
shape:circle: note: imports must be built before being imported
shape:circle: fatal error: returning to the gate for a mechanical issue

次のようにすべきです:

g++ -fmodules-ts -std=c++20 -x c++ circle.cppm rectangle.cppm shape.cppm shape.cc

3.2 サブモジュール

構文:

export module <module-name>.<sub_module-name>;
import <module-name>.<sub_module-name>; // 可以在import前面添加export导出该分区接口

上の例はサブモジュールでも実現できます。shape を主モジュールのまま、2つのサブモジュールをそれぞれ circle と rectangle にします。

// shape.cppm
export module shape;

export import shape.circle;
export import shape.rectangle;

---------------------
// rectangle.cppm
module;
#include <iostream>
export module shape.rectangle;
export void DrawRectangle() { std::cout << "draw rectangle" << std::endl; }

---------------------

// circle.cppm
module;
#include <iostream>
export module shape.circle;
export void DrawCircle() { std::cout << "draw circle" << std::endl; }

使用面では、モジュールパーティションが:を使うのに対し、サブモジュールは.を使うことがわかります。

呼び出し側のコードはモジュールパーティションと同じです。

ただし、使用時にはいくつか違いがあります。例えば:サブパーティションが取り込まれ、そのインターフェースを使うと internal compiler error: Segmentation fault: 11 が発生しますが、サブモジュールは正常に取り込んで使用できます。

// 引入子分区
import shape:circle;

int main() {
    DrawCircle(); // error
    return 0;
}


// 引入子模块
import shape.circle;

int main() {
    DrawCircle(); // ok
    return 0;
}

サブモジュールの場合、import 時に主モジュール名を省略できません。上の主パーティションでパーティションモジュールを取り込む際には:circleが使えますが、ここでは.circleは使えません。エラーは次の通りです:

shape.cppm:3:8: error: 'import' does not name a type
    3 | export import .circle;

4.インターフェースと実装

通常コードを書くとき、「ヘッダファイル」と「実装ファイル」にコードを分割しますが、モジュールではこの操作は不要になりました。ただし、従来の書き方に従うこともできます。

例えば:shape を描画します。

  • モジュール shape.cppm を定義
export module shape;

export class Shape {
 public:
  Shape();
  void Draw();
};
  • 実装モジュール shape.cc
import shape;
import <iostream>;

Shape::Shape() {}
void Shape::Draw() { std::cout << "draw a shape" << std::endl; }

普段使っている.hと.cppの分離モードとほぼ同様です。

https://clang.llvm.org/docs/StandardCPlusPlusModules.html

https://timsong-cpp.github.io/cppwp/n4861/module.import#7.sentence-2

https://gcc.gnu.org/projects/cxx-status.html

関連トピック

コメント