C++ モジュールを使おう! 

モジュールとは?

モジュールという概念はC++に限られたものではありません。 

C++20より追加された「モジュール」は従来のソースファイル、ヘッダファイルに代わる新たなファイル分割の仕組みです。 

「モジュール」を利用することには多くのメリットがあります。 

モジュールの長所

コンパイル時間の短縮

ソースファイルとヘッダファイルによるファイル分割の仕組みでは、それらを読み込むためにincludeディレクティブを使用する必要があります。 

includeディレクティブはプリプロセッサにより、後に指定されたファイルの内容をその位置に展開します。そのため、大規模なプロジェクトでは工夫されていないとincludeディレクティブが何度も登場し、コンパイルに時間がかかってしまう問題がありました。 

モジュールではincludeディレクティブのように展開されることは無く、ファイル(正確には翻訳単位)一つ一つを独立して扱い、それらの関係を整理することでファイル分割をサポートするため、早いコンパイルが可能です。 

安全性・保守性の向上

モジュールでは外部に公開することを明示しない限り、外部から参照されることはありません。

「using namespace std;」と記述されたソースファイルまたはヘッダーファイルをincludeすると、includeしたファイル内でも「using namespace std;」が機能してしまいます。 

もしstd名前空間内の関数やオブジェクトと同じ名前のものを宣言していた場合、衝突が発生してしまうでしょう。 

エラーが出力されれば修正を適用するだけですが、困ったことに出力されない場合があります。 

このように、includeディレクティブではやっかいな問題が発生してしまうのです。 

一方、モジュールでは外部への影響をコントロールできます。

これにより安全性を高められます。 

また、モジュールにより翻訳単位の階層構造を表現できるため、保守性が向上します。 

モジュールの短所

コンパイル時間の肥大 

モジュールは従来のファイル分割の仕組みより早いと説明しましたが、場合によります。 

これは2つの場合に分けられます。 

  • ジョブ数が多い 
  • ジョブ数が少ない 

・ジョブ数が少ない場合 

コンパイル時のジョブ数が少ない場合、モジュールは有効です。 

モジュールは仕組みが複雑なために並列で処理されると、こんがらがってしまいます。 

一方で、includeディレクティブは展開するだけのため、並列処理だと高速に処理できるのです。 

用語

以下の図はhttps://cpprefjp.github.io/lang/cpp20/modules.html「仕様」より作成しました。

モジュールユニット

モジュール機能の対象となる翻訳単位

  • モジュール宣言を含む翻訳単位だけがモジュールユニットと呼ばれる。

モジュールインターフェースユニット

ヘッダーファイルに相当するモジュールユニット

  • モジュールインターフェースユニットには宣言を記述する。
  • ヘッダーファイルと異なり、宣言の公開を明示しない限り外部から参照できない。
  • ここでインポートされたモジュールはこのモジュールユニットの依存先となる。

モジュール実装ユニット

ソースファイルに相当するモジュールユニット

  • ここにはモジュールインターフェースユニットで宣言したクラスやメンバ関数などを定義する。
  • インポートしたモジュールはモジュールユニットの依存先として扱われない。

予約語

モジュール機能を構成する予約語は3つあります。

  • export
  • import
  • module

export

続く宣言を外部に公開することを明示します。

ちなみに英語でexportとは輸出のような外部に送り出すことを意味する言葉です。

import

モジュールを取り込むことを明示します。

注意点としては、取り込み先またはそれ以降のモジュールが自分と依存関係を持っていてはいけません。(モジュールの依存関係は循環してはならない。)

module

モジュールユニットであることを明示します。

構文(シンタックス)

構文は大きく分けると3つあります。

各項目で示す例はリファレンスから利用しています。

  • モジュール宣言
  • エクスポート宣言
  • インポート宣言

モジュール宣言

export(opt) module モジュール名 :パーティション名(opt) 属性(opt);

属性はあまり使わないため、ここでは説明を省略します。

すると、構文のパターンは4通りあることが分かります。

どのパターンでも翻訳単位の先頭に書かなければいけません。

プライマリーモジュールインターフェース

export module モジュール名;

翻訳単位がプライマリーモジュールインターフェースであることを宣言します。

プライマリーモジュールインターフェースでは、エクスポート宣言により外部への公開・非公開を制御できます。

つまり、ヘッダーファイルに相当する役割を持ちます。

モジュールインターフェースパーティション

export module モジュール名 :パーティション名;

翻訳単位がモジュールインターフェースパーティションであることを宣言します。

コードの量が多い場合、プライマリーモジュールインターフェースだけに宣言を記述してしまうと可読性が低下するため、大まかな役割でパーティションに分けることができるようになっています。

分割されたパーティションは別ファイルのように振舞います。

外部から参照できるようにするためには、モジュールインターフェースユニットで再エクスポートを行わなければいけません。

モジュール本体の実装ユニット

module モジュール名;

翻訳単位がモジュール本体の実装ユニットであることを宣言します。

プライマリーモジュールインターフェースで宣言されたクラスや関数などを定義します。

モジュール実装パーティション

module モジュール名 :パーティション名;

翻訳単位がモジュール実装パーティションであることを宣言します。

モジュール本体の実装ユニットのパーティション版です。

エクスポート宣言

export int f(int x);
export class T { /*...*/ };

通常の宣言の前に予約語exportを記述することでエクスポート宣言を行えます。

エクスポート宣言されたクラスや関数などは、インポート時に使うことが可能です。

インポート宣言

import モジュール名;

指定したモジュールを取り込みます。

export宣言されていないものがインポートされることはありません。

また、「using namespace」もインポートされることは無いため、環境をクリーンに保てます。

export import モジュール名;

ちなみに、先頭に予約語exportを付加することで再エクスポートが行えます。

再エクスポートされたモジュールは一緒にインポートされます。

ライブラリとして複数の異なる機能をまとめて提供したい場合などに使用します。

まとめ

モジュールを習得すると、さまざまな恩恵を得ることができます。

モジュールを構成する予約語は以下の3つだけであるため、ポイントを押さえれば習得は容易です。

ぜひ、使ってみてください!

exportモジュールの公開
importモジュールの取り込み
moduleモジュールユニットの宣言
参考文献

コメント