Modules (since C++20)
ほとんどのC++プロジェクトは複数の翻訳単位を使用するため、それらの単位間で 宣言 と 定義 を共有する必要があります。この目的のために ヘッダー の使用が顕著であり、その例として 標準ライブラリ があり、その宣言は 対応するヘッダーをインクルードする ことで提供されます。
モジュールは、翻訳単位間で宣言と定義を共有するための言語機能です。 これらはヘッダーの一部のユースケースに対する代替手段となります。
モジュールは namespaces と直交する概念です。
// helloworld.cpp export module helloworld; // モジュール宣言 import <iostream>; // インポート宣言 export void hello() // エクスポート宣言 { std::cout << "Hello world!\n"; }
// main.cpp import helloworld; // import宣言 int main() { hello(); }
目次 |
構文
export
(オプション)
module
module-name module-partition
(オプション)
attr
(オプション)
;
|
(1) | ||||||||
export
declaration
|
(2) | ||||||||
export {
declaration-seq
(オプション)
}
|
(3) | ||||||||
export
(オプション)
import
module-name attr
(オプション)
;
|
(4) | ||||||||
export
(オプション)
import
module-partition attr
(オプション)
;
|
(5) | ||||||||
export
(オプション)
import
header-name attr
(オプション)
;
|
(6) | ||||||||
module;
|
(7) | ||||||||
module : private;
|
(8) | ||||||||
モジュール宣言
翻訳単位はモジュール宣言を持つことができ、その場合それは モジュール単位 と見なされます。 提供される モジュール宣言 は、翻訳単位の最初の宣言でなければなりません(後述する グローバルモジュールフラグメント を除く)。各モジュール単位は、モジュール宣言で提供される モジュール名 (およびオプションでパーティション)に関連付けられます。
export
(オプション)
module
module-name module-partition
(オプション)
attr
(オプション)
;
|
|||||||||
モジュール名は、ドットで区切られた1つ以上の識別子で構成されます(例:
mymodule
、
mymodule.mysubmodule
、
mymodule2
...)。ドット自体に固有の意味はありませんが、慣例的に階層構造を表現するために使用されます。
モジュール名またはモジュールパーティション内のいずれかの識別子が object-like macro として定義されている場合、プログラムは不適格となります。
名前付きモジュールは、同じモジュール名を持つモジュールユニットの集合です。
export キーワードを持つ宣言を含むモジュール単位は モジュールインターフェース単位 と呼ばれ、その他のモジュール単位は モジュール実装単位 と呼ばれます。
名前付きモジュールごとに、モジュールパーティションを指定しないモジュールインターフェース単位が厳密に1つ存在しなければならない。このモジュール単位は primary module interface unit と呼ばれる。そのエクスポート内容は、対応する名前付きモジュールをインポートする際に利用可能となる。
// (各行は個別の翻訳単位を表します) export module A; // 名前付きモジュール 'A' のプライマリモジュールインターフェース単位を宣言 module A; // 名前付きモジュール 'A' のモジュール実装単位を宣言 module A; // 名前付きモジュール 'A' の別のモジュール実装単位を宣言 export module A.B; // 名前付きモジュール 'A.B' のプライマリモジュールインターフェース単位を宣言 module A.B; // 名前付きモジュール 'A.B' のモジュール実装単位を宣言
宣言と定義のエクスポート
モジュールインターフェース単位は、他の翻訳単位からインポート可能な宣言(定義を含む)をエクスポートできます。宣言をエクスポートするには、 export キーワードを前置するか、あるいは export ブロック内に配置します。
export
宣言
|
|||||||||
export {
宣言シーケンス
(オプション)
}
|
|||||||||
export module A; // 名前付きモジュール 'A' のプライマリモジュールインターフェース単位を宣言 // hello() はモジュール 'A' をインポートする翻訳単位から可視となる export char const* hello() { return "hello"; } // world() は可視とならない char const* world() { return "world"; } // one() と zero() の両方が可視となる export { int one() { return 1; } int zero() { return 0; } } // 名前空間のエクスポートも機能する: hi::english() と hi::french() が可視となる export namespace hi { char const* english() { return "Hi!"; } char const* french() { return "Salut!"; } }
モジュールとヘッダーユニットのインポート
モジュールは import宣言 によってインポートされます:
export
(オプション)
import
module-name attr
(オプション)
;
|
|||||||||
指定された名前付きモジュールのモジュールインターフェース単位でエクスポートされるすべての宣言と定義は、import宣言を使用する翻訳単位で利用可能になります。
モジュールインターフェース単位ではインポート宣言をエクスポートできます。つまり、モジュール
B
が
A
をexport-importする場合、
B
をインポートすると
A
からのすべてのエクスポートも可視になります。
モジュールユニットでは、すべてのインポート宣言(エクスポート-インポートを含む)は、モジュール宣言の後かつ他のすべての宣言の前にグループ化する必要があります。
/////// A.cpp (モジュール'A'のプライマリモジュールインターフェース単位) export module A; export char const* hello() { return "hello"; } /////// B.cpp (モジュール'B'のプライマリモジュールインターフェース単位) export module B; export import A; export char const* world() { return "world"; } /////// main.cpp (モジュール単位ではない) #include <iostream> import B; int main() { std::cout << hello() << ' ' << world() << '\n'; }
#include はモジュール単位( global module fragment の外部)では使用すべきではありません。なぜなら、インクルードされたすべての宣言と定義がモジュールの一部と見なされるためです。代わりに、ヘッダーは header units として import declaration を使用してインポートすることもできます:
export
(オプション)
import
header-name attr
(オプション)
;
|
|||||||||
ヘッダーユニットは、ヘッダーから合成された個別の翻訳単位です。ヘッダーユニットをインポートすると、そのすべての定義と宣言が利用可能になります。プリプロセッサマクロも利用可能です(インポート宣言はプリプロセッサによって認識されるため)。
しかしながら、 #include とは異なり、インポート宣言時点で既に定義されているプリプロセスマクロはヘッダーの処理に影響を与えません。これは場合によっては不便なことがあります(一部のヘッダーはプリプロセスマクロを設定の形式として使用しています)。そのような場合には global module fragment の使用が必要となります。
/////// A.cpp ('A'のプライマリモジュールインターフェースユニット) export module A; import <iostream>; export import <string_view>; export void print(std::string_view message) { std::cout << message << std::endl; } /////// main.cpp (モジュールユニットではない) import A; int main() { std::string_view message = "Hello, world!"; print(message); }
グローバルモジュールフラグメント
モジュール単位は global module fragment で始めることができ、これはヘッダーのインポートが不可能な場合(特にヘッダーが設定用のプリプロセスマクロを使用している場合)にヘッダーを含めるために使用できます。
module;
プリプロセッサ指令 (オプション) モジュール宣言 |
|||||||||
モジュール単位がグローバルモジュールフラグメントを持つ場合、その最初の宣言は
module;
でなければなりません。その後、グローバルモジュールフラグメント内には
プリプロセッサディレクティブ
のみが記述できます。その後、標準モジュール宣言がグローバルモジュールフラグメントの終了とモジュール内容の開始を示します。
/////// A.cpp ('A'のプライマリモジュールインターフェースユニット) module; // _POSIX_C_SOURCEを定義すると、POSIX標準に従って標準ヘッダーに関数が追加されます #define _POSIX_C_SOURCE 200809L #include <stdlib.h> export module A; import <ctime>; // デモンストレーション専用(乱数の質が低い) // 代わりにC++の<random>を使用してください export double weak_random() { std::timespec ts; std::timespec_get(&ts, TIME_UTC); // <ctime>から // POSIX標準に従って<stdlib.h>で提供されます srand48(ts.tv_nsec); // drand48()は0から1の間の乱数を返します return drand48(); } /////// main.cpp (モジュールユニットではありません) import <iostream>; import A; int main() { std::cout << "0から1の間の乱数値: " << weak_random() << '\n'; }
プライベートモジュールフラグメント
プライマリモジュールインターフェースユニットは private module fragment で終了することができ、これによりモジュールを単一の翻訳単位として表現しながら、モジュールの内容すべてをインポーターがアクセス可能にすることなく実現できます。
module : private;
declaration-seq (optional) |
|||||||||
プライベートモジュールフラグメント は、他の翻訳単位の動作に影響を与える可能性があるモジュールインターフェース単位の部分を終了します。モジュール単位が プライベートモジュールフラグメント を含む場合、それはそのモジュールの唯一のモジュール単位となります。
export module foo; export int f(); module : private; // 他の翻訳単位の動作に影響を与える可能性のある // モジュールインターフェース単位の部分を終了 // プライベートモジュールフラグメントを開始 int f() // fooのインポーターから到達できない定義 { return 42; }
モジュール分割
モジュールは
モジュールパーティション単位
を持つことができます。これらはモジュール宣言にモジュールパーティションを含むモジュール単位であり、モジュール名の後にコロン
:
で始まるパーティションが配置されます。
export module A:B; // モジュール 'A' のパーティション ':B' に対するモジュールインターフェースユニットを宣言します。
モジュールパーティションは正確に1つのモジュール単位を表します(2つのモジュール単位が同じモジュールパーティションを指定することはできません)。これらは名前付きモジュール内部からのみ可視です(名前付きモジュール外部の翻訳単位はモジュールパーティションを直接インポートできません)。
モジュールパーティションは、同じ名前のモジュールのモジュールユニットによってインポートできます。
export
(オプション)
import
モジュールパーティション attr
(オプション)
;
|
|||||||||
/////// A-B.cpp export module A:B; ... /////// A-C.cpp module A:C; ... /////// A.cpp export module A; import :C; export import :B; ...
モジュールパーティション内のすべての定義と宣言は、エクスポートされているかどうかに関わらず、インポートするモジュールユニットから可視です。
モジュールパーティションは、モジュールインターフェースユニットになることができます(それらのモジュール宣言に
export
が含まれている場合)。これらはプライマリモジュールインターフェースユニットによってエクスポート-インポートされなければならず、エクスポートされた宣言はモジュールがインポートされたときに可視になります。
export
(オプション)
import
モジュールパーティション attr
(オプション)
;
|
|||||||||
/////// A.cpp export module A; // プライマリモジュールインターフェースユニット export import :B; // Hello()は'A'をインポートする際に可視となる import :C; // WorldImpl()は'A.cpp'のみで可視となる // export import :C; // エラー: モジュール実装ユニットをエクスポートできない // World()は'A'をインポートする任意の翻訳単位で可視となる export char const* World() { return WorldImpl(); }
/////// A-B.cpp export module A:B; // パーティションモジュールインターフェースユニット // Hello()は'A'をインポートする任意の翻訳単位から可視です export char const* Hello() { return "Hello"; }
/////// A-C.cpp module A:C; // パーティションモジュール実装ユニット // WorldImpl()は':C'をインポートする'A'の任意のモジュールユニットから可視です char const* WorldImpl() { return "World"; }
/////// main.cpp import A; import <iostream>; int main() { std::cout << Hello() << ' ' << World() << '\n'; // WorldImpl(); // エラー: WorldImpl()は可視ではありません。 }
モジュール所有権
一般的に、モジュール単位でモジュール宣言の後に宣言が現れる場合、それはそのモジュールに 付属する ことになります。
エンティティの宣言が名前付きモジュールに付属している場合、そのエンティティはそのモジュール内でのみ定義できます。そのようなエンティティのすべての宣言は、同じモジュールに付属している必要があります。
宣言が名前付きモジュールに付属しており、かつエクスポートされていない場合、宣言された名前は module linkage を持ちます。
export module lib_A; int f() { return 0; } // fはモジュールリンケージを持つ export int x = f(); // xは0に等しい
export module lib_B; int f() { return 1; } // OK、lib_Aのfとlib_Bのfは異なるエンティティを参照する export int y = f(); // yは1に等しい
あるエンティティの 2つの宣言 が異なるモジュールに属している場合、プログラムは不適格(ill-formed)となる。互いに到達可能でない場合、診断メッセージは要求されない。
/////// decls.h int f(); // #1, グローバルモジュールに所属 int g(); // #2, グローバルモジュールに所属
/////// モジュールMのインターフェース module; #include "decls.h" export module M; export using ::f; // OK: エンティティを宣言せず、#1をエクスポート int g(); // エラー: #2に一致するが、Mにアタッチされている export int h(); // #3 export int k(); // #4
/////// 他の翻訳単位 import M; static int h(); // エラー: #3に一致 int k(); // エラー: #4に一致
以下の宣言は名前付きモジュールに属していません(したがって、宣言されたエンティティはモジュール外で定義できます):
export module lib_A; namespace ns // nsはlib_Aにアタッチされない { export extern "C++" int f(); // fはlib_Aにアタッチされない extern "C++" int g(); // gはlib_Aにアタッチされない export int h(); // hはlib_Aにアタッチされる } // ns::hはlib_A内で定義されなければならないが、ns::fとns::gは他の場所(例えば // 従来のソースファイル内)で定義可能
注記
| 機能テスト マクロ | 値 | 標準 | 機能 |
|---|---|---|---|
__cpp_modules
|
201907L
|
(C++20) | モジュール — コア言語サポート |
__cpp_lib_modules
|
202207L
|
(C++23) | 標準ライブラリモジュール std および std. compat |
キーワード
private 、 module 、 import 、 export
不具合報告
以下の動作変更の欠陥報告書は、以前に公開されたC++規格に対して遡及的に適用されました。
| DR | 適用対象 | 公開時の動作 | 正しい動作 |
|---|---|---|---|
| CWG 2732 | C++20 |
インポート可能ヘッダーがインポート時点の
プリプロセッサ状態に反応するか不明確だった |
反応しない |
| P3034R1 | C++20 |
モジュール名とモジュールパーティションに
オブジェクト形式マクロとして定義された識別子を含められた |
禁止 |