C++ のモジュールの概要

C++20 では "モジュール" が導入されています。 "モジュール" は、一連のソース コード ファイルであり、ソース ファイル (正確には、それらをインポートする翻訳単位) とは別にコンパイルされます。

ヘッダー ファイルの使用に関連する問題の多くは、モジュールによって排除または軽減されます。 これにより多くの場合、コンパイル時間が (時には大幅に) 短縮されます。 モジュール内で宣言されたマクロ、プリプロセッサ ディレクティブ、およびエクスポートされていない名前は、モジュールの外部からは見えません。 それらが、モジュールをインポートする翻訳単位のコンパイルに影響を与えることはありません。 マクロの再定義を気にすることなく、モジュールを任意の順序でインポートできます。 インポートする変換単位の宣言は、インポートされたモジュールのオーバーロードの解決や名前の検索には含まれません。 モジュールが 1 回コンパイルされると、結果は、エクスポートされた型、関数、およびテンプレートを記述するバイナリ ファイルに格納されます。 コンパイラではそのファイルを、ヘッダー ファイルよりも高速に処理できます。 また、モジュールがプロジェクトにインポートされる場所ごとにコンパイラによって再利用できます。

モジュールは、ヘッダー ファイルとサイド バイ サイドで使用できます。 C++ ソース ファイルは、import モジュールをインポートでき、#include ヘッダー ファイルもインポートできます。 場合によっては、ヘッダー ファイルをモジュールとしてインポートできます。これは、#include を使ってプリプロセッサで処理する場合よりも高速です。 新しいプロジェクトでは、可能な限りヘッダー ファイルではなくモジュールを使用することをお勧めします。 積極的な開発において既存のプロジェクトの規模が大きくなる場合は、レガシ ヘッダーをモジュールに変換する方法を試してください。 コンパイル時間を大幅に短縮できるかどうかに基づいて採用します。

モジュールを、標準ライブラリをインポートする他の方法と比較するには、「ヘッダー ユニット、モジュール、プリコンパイル済みヘッダーを比較する」を参照してください。

Microsoft C++ コンパイラでモジュールを有効にする

Visual Studio 2022 バージョン 17.1 の時点では、C++20 標準モジュールは Microsoft C++ コンパイラに完全には実装されていません。

C++20 標準で指定される前は、Microsoft はモジュールを実験的にサポートしていました。 以下で説明するように、コンパイラでは、事前構築済みの標準ライブラリ モジュールのインポートもサポートしました。

Visual Studio 2022 バージョン 17.5 以降、Microsoft C++ コンパイラでは、標準ライブラリをモジュールとしてインポートすることが標準化され、完全に実装されています。 このセクションでは、まだサポートされている、以前の実験的な方法について説明します。 モジュールを使用して標準ライブラリをインポートする新しい標準化された方法については、モジュールを使用した C++ 標準ライブラリのインポートに関する記事を参照してください。

モジュール機能を使用すると、単一パーティション モジュールを作成し、Microsoft が提供する標準ライブラリ モジュールをインポートできます。 標準ライブラリ モジュールのサポートを有効にするには、/experimental:module/std:c++latest を使用してコンパイルします。 Visual Studio プロジェクトで、ソリューション エクスプローラーでプロジェクト ノードを右クリックし、[プロパティ] を選択します。 [構成] ドロップダウンを [すべての構成] に設定し、[構成プロパティ]>[C/C++]>[言語]>[C++ モジュールを使用できる (試験段階)] を選択します。

モジュールとそれを使用するコードは、同じコンパイラ オプションを使用してコンパイルする必要があります。

C++ 標準ライブラリをモジュールとして使用する (試験段階)

このセクションでは、まだサポートされている、試験的な実装について説明します。 C++ 標準ライブラリをモジュールとして使用する新しい標準化された方法については、モジュールを使用した C++ 標準ライブラリのインポートに関する記事を参照してください。

ヘッダー ファイルを通じて含めるのではなく、C++ 標準ライブラリをモジュールとしてインポートすることで、プロジェクトのサイズに応じて、コンパイル時間を短縮できる可能性があります。 試験的ライブラリは、次の名前付きモジュールに分割されます。

  • std.regex はヘッダー <regex> のコンテンツを提供する
  • std.filesystem はヘッダー <filesystem> のコンテンツを提供する
  • std.memory はヘッダー <memory> のコンテンツを提供する
  • std.threading はヘッダー <atomic><condition_variable><future><mutex><shared_mutex><thread> のコンテンツを提供する
  • std.core は C++ 標準ライブラリの他のすべてを提供する

これらのモジュールを使用するには、ソース コード ファイルの一番上にインポート宣言を追加します。 次に例を示します。

import std.core;
import std.regex;

Microsoft 標準ライブラリ モジュールを使用するには、/EHsc および /MD オプションを使ってプログラムをコンパイルします。

次の例は、Example.ixx というソース ファイル内の単純なモジュール定義を示しています。 .ixx 拡張子は、Visual Studio のモジュール インターフェイス ファイルに必要です。 この例では、インターフェイス ファイルに関数定義と宣言が含まれています。 ただし、定義は 1 つ以上の個別のモジュール実装ファイルに配置することもできます (後の例を参照)。

export module Example; ステートメントは、このファイルが Example と呼ばれるモジュールのプライマリ インターフェイスであることを示します。 f() の修飾子 export は、Example が別のプログラムまたはモジュールによってインポートされた場合に、この関数が表示されることを示します。

// Example.ixx
export module Example;

#define ANSWER 42

namespace Example_NS
{
   int f_internal() {
        return ANSWER;
      }

   export int f() {
      return f_internal();
   }
}

ファイル MyProgram.cpp は、import を使用して、Example によってエクスポートされた名前にアクセスします。 名前空間名 Example_NS はここに表示されますが、そのすべてのメンバーがエクスポートされるわけではないため、表示されないメンバーもあります。 また、マクロはエクスポートされないため、マクロ ANSWER は表示されません。

// MyProgram.cpp
import Example;
import std.core;

using namespace std;

int main()
{
   cout << "The result of f() is " << Example_NS::f() << endl; // 42
   // int i = Example_NS::f_internal(); // C2039
   // int j = ANSWER; //C2065
}

import 宣言は、グローバル スコープでのみ使用できます。

モジュール文法

module-name:
module-name-qualifier-seqoptidentifier

module-name-qualifier-seq:
identifier .
module-name-qualifier-seq identifier .

module-partition:
: module-name

module-declaration:
exportoptmodulemodule-namemodule-partitionoptattribute-specifier-seqopt;

module-import-declaration:
exportoptimportmodule-nameattribute-specifier-seqopt;
exportoptimportmodule-partitionattribute-specifier-seqopt;
exportoptimportheader-nameattribute-specifier-seqopt;

モジュールの実装

"モジュール インターフェイス" は、モジュール名と、モジュールのパブリック インターフェイスを構成するすべての名前空間、型、関数などをエクスポートします。
"モジュール実装" は、モジュール によってエクスポートされるものを定義します。
最も単純な形式として、モジュールは、モジュール インターフェイスと実装を組み合わせた 1 つのファイルにすることができます。 また、.h ファイルと .cpp ファイルで行う方法と同様に、1 つ以上の個別のモジュール実装ファイルに実装を入れることもできます。

大規模なモジュールの場合は、"パーティション" と呼ばれるサブモジュールにモジュールを分割できます。 各パーティションは、モジュールのパーティション名をエクスポートする 1 つのモジュール インターフェイス ファイルで構成されます。 1 つのパーティションには、1 つ以上のパーティション実装ファイルを含めることもできます。 モジュール全体には、モジュールのパブリック インターフェイスである 1 つの "プライマリ モジュール インターフェイス" があります。 これは、必要に応じて、パーティション インターフェイスをエクスポートできます。

モジュールは、1 つ以上の "モジュール ユニット" で構成されます。 モジュール ユニットは、モジュール宣言を含む翻訳単位 (ソース ファイル) です。 モジュール ユニットにはいくつかの種類があります。

  • "モジュール インターフェイス ユニット" は、モジュール名またはモジュール パーティション名をエクスポートします。 モジュール インターフェイス ユニットには、そのモジュール宣言に export module が含まれています。
  • "モジュール実装ユニット" は、モジュール名またはモジュール パーティション名をエクスポートしません。 名前が示すように、モジュールを実装します。
  • "プライマリ モジュール インターフェイス ユニット" は、モジュール名をエクスポートします。 1 つのモジュールで使用できるプライマリ モジュール インターフェイス ユニットは 1 つのみです。
  • "モジュール パーティション インターフェイス ユニット" は、モジュール パーティション名をエクスポートします。
  • "モジュール パーティション実装ユニット" のモジュール宣言にはモジュール パーティション名が含まれますが、export キーワードはありません。

export キーワードは、インターフェイス ファイルでのみ使用されます。 実装ファイルでは別のモジュールを import できますが、名前を export することはできません。 実装ファイルには、拡張子を付けることができます。

モジュール、名前空間、引数に依存する検索

モジュール内の名前空間の規則は、他のコードと同じです。 名前空間内の宣言がエクスポートされた場合、外側の名前空間 (その名前空間に明示的にエクスポートされていないメンバーを除く) も暗黙的にエクスポートされます。 名前空間が明示的にエクスポートされた場合、その名前空間定義内のすべての宣言がエクスポートされます。

コンパイラがインポート翻訳単位でオーバーロード解決に対して引数に依存する検索を行う場合、コンパイラは、関数の引数の型が定義されているのと同じ翻訳単位 (モジュール インターフェイスを含む) で宣言されている関数を考慮します。

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

モジュール パーティションは、次の点を除き、モジュールに似ています。

  • モジュール全体のすべての宣言の所有権を共有する。
  • パーティション インターフェイス ファイルによってエクスポートされた名前はすべて、プライマリ インターフェイス ファイルによってインポートおよびエクスポートされる。
  • パーティションの名前は、モジュール名の後にコロンで始まる必要がある (:)。
  • どのパーティション内での宣言でも、モジュール全体から表示できる。\
  • 1 つの定義規則 (ODR) エラーを回避するために特別な予防措置は必要ない。 あるパーティションで名前 (関数やクラスなど) を宣言し、別のパーティションでそれを定義することができます。

パーティション実装ファイルは次のように始まり、これは C++ 標準の観点から見た内部パーティションになります。

module Example:part1;

パーティション インターフェイス ファイルは、次のように始まります。

export module Example:part1;

別のパーティション内の宣言にアクセスするには、パーティションがそれをインポートする必要があります。 ただし、モジュール名ではなく、パーティション名のみを使用できます。

module Example:part2;
import :part1;

プライマリ インターフェイス ユニットでは、次のように、モジュールのすべてのインターフェイス パーティション ファイルをインポートして再エクスポートする必要があります。

export import :part1;
export import :part2;

プライマリ インターフェイス ユニットでは、パーティション実装ファイルをインポートできますが、エクスポートすることはできません。 これらのファイルで、名前をエクスポートすることは許可されていません。 この制限により、モジュールは実装の詳細をモジュールの内部に保持できます。

モジュールとヘッダー ファイル

モジュールの宣言の前に #include ディレクティブを置くことで、モジュール ソース ファイルにヘッダー ファイルを含めることができます。 これらのファイルは、グローバル モジュール フラグメント内にあると見なされます。 モジュールは、明示的に含まれるヘッダーに含まれるグローバル モジュール フラグメント内の名前のみを表示できます。 グローバル モジュール フラグメントには、使用されるシンボルだけが含まれています。

// MyModuleA.cpp

#include "customlib.h"
#include "anotherlib.h"

import std.core;
import MyModuleB;

//... rest of file

従来のヘッダー ファイルを使用して、インポートするモジュールを制御できます。

// MyProgram.h
import std.core;
#ifdef DEBUG_LOGGING
import std.filesystem;
#endif

インポートされたヘッダー ファイル

一部のヘッダーは、import キーワードを使用して取り込み可能な十分な自己完結型です。 インポートされたヘッダーとインポートされたモジュールの主な違いは、ヘッダー内のプリプロセッサ定義が import ステートメントの直後にインポート プログラムに表示される点です。

import <vector>;
import "myheader.h";

関連項目

moduleimportexport
名前付きモジュールのチュートリアル
ヘッダー ユニット、モジュール、プリコンパイル済みヘッダーを比較する