C++20のモジュールは確かにある一つの提案がベースになっているのですが、その後C++20策定完了までの間に複数の提案やIssue報告によってそこそこ大きく変化しています。その結果、C++20のモジュールはその全体像を把握するためにどれか一つの提案を読めばわかるものではなく、関連する提案を追うのもC++20のDIS(N4861)を読み解くのも辛いものがあり、ただでさえ難解な仕様を余計に分かりづらくしています。
この記事は、C++20の最初のモジュールの一歩手前から時系列に沿って、モジュールがどのように変化していったのかを眺めるものです。
- Working Draft, Extensions to C++ for Modules (Module TS)
- Another take on Modules (ATOM Proposal)
- Merging Modules (最初期のC++20モジュール)
- Relaxing redefinition restrictions for re-exportation robustness
- Mitigating minor modules maladies
- Recognizing Header Unit Imports Requires Full Preprocessing
- Standard library header units for C++20
- NBコメントへの対応1
- Core Language Changes for NB Comments at the November, 2019 (Belfast) meeting
- Resolution to US086
- Dynamic Initialization Order of Non-Local Variables in Modules
- Core Language Changes for NB Comments at the February, 2020 (Prague) meeting
- Translation-unit-local entities
- ABI isolation for member functions
- Modules Dependency Discovery
- Issueの解決3
- おわり
Working Draft, Extensions to C++ for Modules (Module TS)
Module TSと呼ばれているもので、最終的なC++20の仕様のベースとなっているものです。
意味論の細かい差異はあれど、現在書くことのできる基本的なモジュールの構文や仕様(export/import/module
宣言など)はここで決定されました。
Another take on Modules (ATOM Proposal)
この提案は、モジュールシステムを別の観点から見つめ直し、モジュールTSにあったいくつかの問題を修正しようとするものです。
TSからの変更点としては
export/module
の非キーワード化module
宣言は翻訳単位の先頭になければならない- モジュールパーティションの導入
private/public
なimport
- マクロの
export
- ヘッダユニット(Legacy header unitsと呼ばれていた)
- ヘッダの
import
#include
のimport
への置換
- ヘッダの
- インスタンス化経路(path of instantiation)
これはあくまでモジュールTSをベースとしており、対案と言うよりはモジュールTSを補間し修正しようとする提案です。他の提案からは、ATOM Proposalと呼ばれます。
Merging Modules (最初期のC++20モジュール)
この提案はC++20に最初に導入されたモジュール仕様です。モジュールTS(N4720)にATOM提案(P0947R1)をマージする形でモジュールは導入されました。
ここで、新たに次のものが追加されました
- グローバルモジュールフラグメントの導入
- 正確には、グローバルモジュールフラグメントは明示的に導入するものとなった(TSでは
#include
によるヘッダインクルードが実質的にグローバルモジュールフラグメントを導入していた)
- 正確には、グローバルモジュールフラグメントは明示的に導入するものとなった(TSでは
- プライベートモジュールフラグメントの導入
- semantic boundaries ruleの導入
- 「(定義を持つものは)以前の定義が到達可能なところで再定義されてはならない」と言うルール
- ヘッダユニットからのマクロの定義位置
import
宣言の直後と規定
また、次のものは導入されませんでした
- モジュールTS
- Proclaimed ownership declaration
- ATOM提案
export/module
の非キーワード化private/public
なimport
- マクロの
export
参考資料
Relaxing redefinition restrictions for re-exportation robustness
当初のモジュールでは、従来の#include
は可能ならすべてヘッダユニットのimport
に置き換えられていました。そして、ヘッダユニットの宣言はグローバルモジュールに属する宣言として扱われます。
またODR要件が緩和されており、以前の定義が到達可能でなければ定義は複数あってもいい、とされていました。ある場所から定義が到達可能というのは、その場所で直接見えているimport
宣言をたどっていった先で定義が見つかることで、この到達可能な定義の集合に同じ宣言に対する定義が複数含まれているとエラーとなります。
C++17以前と同様に、グローバルモジュール(非モジュール内)においては、その宣言が同一であれば異なる翻訳単位で定義が重複しても良い、というルールがあります。ただし、それらの定義がモジュールのインポートによって到達可能となってしまう場合は定義の重複は許されません。
/// M.cpp module; #include "a.h" // struct A {}; export module M; // b.hはインポート可能なヘッダ export import "b.h"; // struct B {}; // 宣言が破棄されないようにする export A f();
/// src.cpp import M; // 構造体AとBはともにこの場所から到達可能 #include "a.h" // error, Aの再定義 #include "b.h" // OK, b.hのインクルードはimportに変換され、Bの定義は再定義とならない
この時、b.h
が次のようになっていると多重定義エラーが発生する可能性があります。
/// b-impl.h (インポート可能なヘッダではない #ifndef B_IMPL_H #define B_IMPL_H struct B {}; #endif
/// b.h (インポート可能なヘッダ #include "b-impl.h"
/// src.cpp import M; #include "b-impl.h" // error, Bの再定義
インポート可能なヘッダというのは実装定義であり、このようなエラーはコンパイラによって発生したりしなかったりするかもしれません。また、従来の#include
であれば、このようなケースはODRの例外規定によって特別扱いされていたはずです。
このようなグローバルモジュールに属するエンティティの再エクスポート時の不可思議な振る舞いを避けるために、この提案によって次のように仕様が調整されました。
- グローバルモジュールに属するエンティティの定義は、定義が到達可能かどうかに関係なく、各翻訳単位で最大1つの定義の存在を許可する。
- 名前付きモジュールに属するエンティティに対するODRの例外規定の削除。
- ODRの例外規定とは、定義が同一であれば複数の翻訳単位に現れてもいい、というルール。
- テンプレートをヘッダに書いてコンパイルする際に実装を容易にするための特殊なルールだったが、モジュールの
import
は宣言をコピペしないのでモジュールでは不要。
- インポート可能なヘッダの
#include
は、import
に置き換えても よい という表現に変更- そもそも、インポート可能なヘッダを常に
import
していたのは、先程のb.h
の最初の例のようなケースで再定義エラーが起こらないようにするため。この提案の変更によって前提となる問題が解決された。
- そもそも、インポート可能なヘッダを常に
// この提案適用後では import M; #include "b-impl.h" // OK, Bの定義は到達可能だが、この翻訳単位では最初の定義
この提案によってグローバルモジュールにおけるODR周りの事はC++17までとほとんど同様となり、名前付きモジュール内でだけODR要件が厳しくなります。
- グローバルモジュール : 宣言に対する定義は各翻訳単位で唯一つであり、全ての定義は同一でなければならない
- 名前付きモジュール : 宣言に対する定義はプログラム内で唯一つでなければならない
どちらの場合でも、同じ翻訳単位内での多重定義はコンパイルエラーとなりますが、翻訳単位が分かれている場合にこのルールに違反していると必ずしもコンパイルエラーとはならず、未定義動作となります。
参考資料
Mitigating minor modules maladies
この提案はモジュールによって問題となる、3つの特殊なケースのバグを修正するものです。
1. using/typedef
※この問題はコンパイラの実装に大きくかかわる物で、今一よくわかりませんでしたので、結論のみを書いておきます・・・
この提案では、リンケージを与えるためのtypedef
の対象となる構造体は次のものを含むことができないようにします。
この提案によって、リンケージを与えるためのtypedef
はC言語互換のためだけの機能であることが明確となり、その対象となる構造体はC互換の構造体に限定されるようになります。また、typedef/using
によって名前のリンケージを変更できないことが明確となります。
これはモジュールの内外を問わず適用されるため、破壊的変更となります。
2. エクスポートブロック内でのstatic_assert
モジュールにおけるexport
宣言では、名前を導入しないタイプの宣言をexport
することができません。
export static_assert(true); // error、エクスポートできない export { struct Foo { /*...*/ }; static_assert(std::is_trivially_copyable_v<Foo>); // error、エクスポートできない struct Bar { /*...*/ }; template<typename T> struct X { T t; }; template<typename T> X(T) -> X<T>; // error、エクスポートできない // ... #define STR(x) constexpr char x[] = #x; // セミコロン(;)が余計に一つ付くが、エクスポートできないのでエラー STR(foo); STR(bar); #undef X }
この提案では、このようなエクスポートブロックの内部でのみ、宣言が少なくとも1つの名前を導入しなければならない、というルールを削除します。
export static_assert(true); // error、エクスポートできない export { struct Foo { /*...*/ }; static_assert(std::is_trivially_copyable_v<Foo>); // OK struct Bar { /*...*/ }; template<typename T> struct X { T t; }; template<typename T> X(T) -> X<T>; // OK // ... #define STR(x) constexpr char x[] = #x; // 両方OK STR(foo); STR(bar); #undef X }
ただし、ブロックではない通常のexport
宣言においては名前を導入しない宣言をエクスポートできないのは変わりません。
3. デフォルト引数の不一致
inline
ではない関数では、デフォルト引数を翻訳単位ごとに異なるものとすることができます。また、テンプレートのデフォルトテンプレート引数も翻訳単位ごとに異なるものとすることができます。
/// a.h int f(int a = 123);
/// b.h int f(int a = 45);
/// main.cpp import A; // a.hを間接的にインクルードしているが、エクスポートはしていない import "b.h"; // a.hのf()は到達可能 // b.hのf()は可視であり到達可能 int main() { int n = f(); // 結果は・・・? }
同じ宣言に対して異なるデフォルト引数が与えられた複数の宣言が同じ翻訳単位内で出現する場合はコンパイルエラーとなりますが、モジュールにおいては一方のみが名前探索で可視であるが、両方の宣言に到達可能となる場合があります。当初の仕様ではこの場合にどう振舞うかは未規定でした。
この提案では、異なる翻訳単位の同じ名前空間スコープの2つの宣言が、同じ関数引数に異なるデフォルト引数を、あるいは同じテンプレート引数に異なるデフォルトテンプレート引数を指定することをそれぞれ禁止します。ただし、異なるデフォルト引数を持つ複数の宣言が同時に到達可能とならない限り、コンパイルエラーとならない可能性があります。
/// main.cpp import A; // a.hを間接的にインクルードしているが、エクスポートはしていない import "b.h"; // a.hのf()は到達可能 // b.hのf()は可視であり到達可能 int main() { int n = f(); // NG、コンパイルエラー }
この変更は、その宣言がモジュールにあるかどうかにかかわらず適用されます。つまり、これは破壊的変更となります。
Recognizing Header Unit Imports Requires Full Preprocessing
この提案は、依存関係スキャンを簡易化・高速化するために、ヘッダユニットのインポートを#include
とほとんど同等に扱えるようにするものです。
当初のモジュールでは、import
宣言はほとんどC++のコードとして解釈され、プリプロセス時にはヘッダユニットのインポートに対してマクロのエクスポートを行う以外のことをしていませんでした。そのため、ヘッダユニットのインポートを識別するには翻訳フェーズ4(プリプロセスの実行)を完了する必要がありました。
すなわち、import
宣言はほとんどどこにでも現れる可能性があり、マクロ展開を完了しなければimport
宣言を抽出することができません。
これは従来#include
に対して行われていた依存関係スキャンに対して、実装が困難になるだけではなく、速度の面でも明らかに劣ることになります。例えば、#include
に対する依存関係スキャンでは、プリプロセッシングディレクティブ以外の行は何もせず無視することができ、#include
は1行で書く必要があるため行をまたぐような複雑なマクロ展開をしなくても良くなります。
この提案では、(export) import
によって開始される行をプリプロセッシングディレクティブとして扱うようにします。それによって、(export) import
をマクロ展開によって導入する事ができなくなり、(export) import
は空白を除いて行の先頭に来ていなければならず、import
宣言は1行で書かなければならなくなります。
プリプロセスの最初の段階ではモジュールのインポートもヘッダユニットのインポートもまとめて扱われ、その後ヘッダユニットのインポートに対してエクスポートされたマクロのインポートを行います。最後に、import
トークンを実装定義のimport-keyword
に置き換えて、import
ディレクティブのプリプロセスは終了します。
翻訳フェーズ5以降、つまりC++コードのコンパイル時には、このように導入されたimport-keyword
によるものだけがimport
宣言として扱われるようになります。
なお、(export) import
のトークンおよびimport
ディレクティブを終了する;
と改行だけがマクロで導入できないだけで、import
対象のヘッダ・モジュール名はマクロによって導入することができます。
この提案によって可能な記述は制限される事になります。
Before | After |
---|---|
// 行中にあっても良かった int x; import <map>; int y; |
// importディレクティブは1行で独立 int x; import <map>; int y; |
Before | After |
---|---|
import <map>; import <set>; |
// それぞれ1行づつ書く
import <map>;
import <set>;
|
Before | After |
---|---|
// 複数行に渡っていても良かった export import <map>; |
// importディレクティブは1行で完結する export import <map>; |
Before | After |
---|---|
// プリプロセッサによる切り替えが可能だった #ifdef MAYBE_EXPORT export #endif import <map>; |
// importディレクティブの一部だけを#ifで変更できない #ifdef MAYBE_EXPORT export import <map>; #else import <map>; #endif |
Before | After |
---|---|
#define MAYBE_EXPORT export MAYBE_EXPORT import <map>; |
// (export) importはマクロによって導入できない #define MAYBE_EXPORT #ifdef MAYBE_EXPORT export import <map>; #else import <map>; #endif |
この提案の内容はのちにP1857R3によって大幅に(より制限する方向に)拡張されることになります。
参考資料
Standard library header units for C++20
この提案は、少なくとも標準ライブラリのヘッダはヘッダユニットとしてインポート可能であることを規定するものです。
C++20にモジュールが導入されるのは確定的で、そうなると標準ライブラリをモジュールとして提供する(できる)必要が生じます。この提案の時点ではその作業が間に合うかは不透明であり(実際間に合わなかった)、間に合わなかった場合は、それぞれのベンダーからそれぞれの(互換性のない)方法でモジュール化された標準ライブラリが提供され、C++エコシステムに分断をもたらす事になりかねません。
この提案では、既存の標準ライブラリをモジュールとして提供するための最低限のメカニズムを提供しつつ、将来的な標準ライブラリの完全なモジュール化を妨げる事が無いようにするものです。
そのために、C++の 標準ヘッダは全てヘッダユニットとしてインポート可能であると規定し、標準ライブラリへのアクセス手段としての標準ヘッダのインポートを規定します。そして、モジュール単位(名前付きモジュール)の中での標準ライブラリヘッダの#include
はグローバルモジュールフラグメントの中でのみ行える事が規定されました(診断は不要とあるので、これに従わなくてもコンパイルエラーとはならない可能性があります)。
なお、C互換の標準ヘッダ(<cmath>, <cassert>
などの<cxxx>
系のヘッダ)はインポート可能ではありません。これらのヘッダは事前のマクロ定義に大きく影響を受けますが、ヘッダユニットも含めたモジュールは外で定義されたマクロが内部に影響を及ぼさないため、インポータブルでは無いためです。
また同時に、std
から始まる全てのモジュール名を将来の標準ライブラリモジュールのために予約します。
NBコメントへの対応1
このIssueは、規格文書中で標準ライブラリのエンティティ名にアクセスする手段を記述している所にヘッダユニットのimport
を加えるものです。P1502R1の内容を補強するもので、P1502R1ではおそらく見落とされていたものです。
このIssueは、モジュールのインターフェースという言葉を使用していたために、エクスポートしていない関数名がADLを介して表示されるかのように読めてしまっていた部分の表現を修正するものです。
意味するところはこの前後で変わらず、モジュールの内部にあるエクスポートされた宣言はテンプレートのインスタンス化経路上で可視となりますが、エクスポートされていない宣言はいかなる場合にも可視になりません。
このIssueは、予約するモジュール名について名前空間の予約と記述を一貫させるものです。
意味するところは変わらず、std
に数字が続く名前空間名、std
から始まるモジュール名は全て予約されます。
このIssueは、グローバルモジュールフラグメントに関する箇所の規格参照用のラベルが[module.global]だったり[cpp.glob.frag]だったりしていたのを、[xxx.global.frag]に一貫させるものです。
このIssueは、モジュールに関するサンプルコードで翻訳単位の境界が曖昧だった所を明確にするものです。
Core Language Changes for NB Comments at the November, 2019 (Belfast) meeting
この提案は2019年11月のベルファストの会議において採択されたコア言語のIssue解決(NBコメントについて)をまとめたものです。モジュールに関連するものは4件あります。
これは、それまで規格としての記述のみで利用法が不明瞭だったプライベートモジュールフラグメントについて、サンプルコードを追加するものです。
これは、ヘッダユニットのインポートが再起して巡回する事が無いことを明確に記述するものです。
それまで、モジュールのインポートはインターフェース依存関係という言葉を用いて巡回インポートが禁止されていましたが、ヘッダユニットについては特に規定がありませんでした。
ここでは、インターフェース依存関係の対象にヘッダユニットを含めることで、モジュールと同様に巡回インポートを禁止します。あらゆる巡回インポートはコンパイラによって検出され、コンパイルエラーとなります。
これは、コマンドラインオプション(-D
など)によって定義されたマクロ名がヘッダユニットからエクスポートされないことを規定するものです。
これによって、そのようなマクロ名が重複したり、それがコンパイラによって異なったりする事が防止されます。ただ、これはどうやら文面として強制するものでは無いようです・・・
これは、グローバルnew/delete
を使うための<new>
や<=>
の戻り値型を使用するための<compare>
など、言語機能の利用のために標準ライブラリヘッダのインクルードの必要が規定されているものについて、ヘッダユニットのimport
も認めるようにするものです。
Resolution to US086
この提案によって解決されるIssueは、あるモジュール単位I
を同じモジュール内の他のモジュール単位M
がインポートする時に、I
のグローバルモジュールフラグメントにあるインポート宣言を暗黙的にインポートしないようにするものです。
同じモジュール内にあるモジュール単位をインポートするとき、インポート対象のモジュール単位内でインポートされているすべての翻訳単位をインポートします。2つのモジュール単位が別々のモジュールに属する場合のインポートは再エクスポート(export imprt
)されている翻訳単位のみをインポートしますが、同じモジュール内ではインポート宣言がより多くの翻訳単位をインポートすることになります。
グローバルモジュールフラグメントは#include
をモジュール内で安全に行うための宣言領域であり、そこにあるインポート宣言は#include
のimport
への置換によって導入されたものでしょう。それらはグローバルモジュールに属するものでありI
の一部ではなく、I
からエクスポートされM
から到達可能となるのは不適切です。
export
宣言はグローバルモジュールフラグメントに直接的にも間接的(#include
やマクロ展開など)にも書くことはできないので、グローバルモジュールフラグメントでimport
されている翻訳単位をインポートしてしまう可能性があるのは同じモジュール内でのモジュール単位のインポート時だけです。
当初の仕様ではその考慮は抜けており(モジュールTSでは考慮されていましたが、ATOMとのマージ時にグローバルモジュールフラグメントが導入されたことで見落とされていた様子)、グローバルモジュールフラグメントのインポート宣言がモジュールのインターフェースの一部となってしまっていたため、この提案では明示的にそうならないことを規定しています。
/// uses_vector.h import <vector>; // #includeからの置換である可能性がある
/// partition.cpp module; #include "uses_vector.h" // import <vector>; と展開される module A:partition; // この中でstd::vector<int>を使っているとする。
/// interface.cpp module A; import :partition; // 必ずコンパイルエラーになるようになる // ここでは<vector>はインポートもインクルードもされていない std::vector<int> x;
以前の仕様では、最後のstd::vector
の使用がwell-definedとなってしまっていました。
Dynamic Initialization Order of Non-Local Variables in Modules
当初の仕様では、モジュールとそれをインポートする翻訳単位の間で静的記憶域期間を持つオブジェクト(すなわちグローバル変数)の動的初期化順序が規定されていなかったために、std::cout
の利用すら未定義動作を引き起こす可能性が潜んでいました。
import <iostream>; // <iostream>ヘッダユニットのインポート struct G { G() { std::cout << "Constructing\n"; } }; G g{}; // Undefined Behaior!?
このような場合でも安全に利用できるようにするために、モジュールを含めた翻訳単位間での静的オブジェクトの動的初期化に一定の順序付けを規定するようにします。
ある翻訳単位がヘッダユニットも含めてモジュールをインポートする時、そのモジュールに対してインターフェース依存関係が発生します。インポートが絡む場合の動的初期化順序はこのインターフェース依存関係を1つの順序として初期化順序を規定します。ただし、この初期化順序は半順序となります(すなわち、順序が規定されない場合があります)。
同じ翻訳単位内での動的初期化順序はその宣言順で変わりありません。これは、別の翻訳単位をインポートしたときに、インポート先にある静的変数とインポート元の静的変数との間の動的初期化順序を最低限規定するものです。
参考資料
Core Language Changes for NB Comments at the February, 2020 (Prague) meeting
これは、言語リンケージ指定を伴うブロック内でのimport
宣言を許可するものです。
例えばextern "C"
なブロック内でCのヘッダを#include
している場合にも、そのファイルがC++としてコンパイルされていればそのヘッダをimport
に置換することができるはずです。しかし以前の仕様ではimport
のリンケージ指定もリンケージブロック内でのimport
も許可されていなかった(import
宣言はグローバル名前空間スコープにのみ現れることができた)ため、その場合は常に#include
するしかありませんでした。
このIssueの解決では、直接的に書くことができないのは従来通りですが、#include
変換の結果としてヘッダユニットのimport
が現れるのが許可されるようになります。ただし、C++
言語リンケージ指定以外に現れるimport
宣言は実装定義の意味論で条件付きのサポートとなります。
extern "C" import "importable_header.h" // NG、直接書けない extern "C" { #include "importable_header.h" // OK、ヘッダユニットのインポートに変換可能 // ただし、実装依存のサポート import "importable_header.h" // NG、直接書けない } extern "C++" { #include "importable_header.h" // OK、ヘッダユニットのインポートに変換可能 import "importable_header.h" // NG、直接書けない }
このことは、構文定義を変更してインポート宣言をおよそ宣言が書ける場所にどこでも書けるようにしたうえで、文書でインポート宣言を書ける場所をグローバル名前空間スコープに限定しておき、リンケージ指定ブロック内で(#include
置換の結果として)インポート宣言が間接的に現れることを許可する形で表現されており、少しややこしいです。
Translation-unit-local entities
これは名前付きモジュールのインターフェースにある内部リンケージ名がそのモジュールの外部へ露出する事を禁止するものです。
これは特に、モジュール外部でインライン展開されうる関数にて問題になっていました。
/// mymodule.cpp module; #include <iostream> export module mymodule; // 内部リンケージ static void internal_f(int n) { std::cout << n << std::endl; } namespace { // 内部リンケージ int internal_g() { return 10; } } // エクスポートされている、外部リンケージ export inline int external_f(int n) { // この関数がインライン展開されると・・・ internal_f(n); return n + internal_g(); }
名前付きモジュールにおけるinline
関数がexport
される場合、その定義はそのモジュールのインターフェースに無ければなりません。そのため、export
されたinline
関数はinline
指定の本来の効果(関数のインライン展開の指示)の適用対象となります。
インライン展開される関数の本体から内部リンケージ名を参照していると、本来翻訳単位を超えて参照できないはずの内部リンケージ名がインライン展開によって翻訳単位の外側から参照されてしまう事になります。内部リンケージ名の翻訳単位外への暴露は望ましい動作では無いため、この提案によって禁止されました。
名前付きモジュールのインターフェースに存在する外部への露出が禁止されるもののことを、翻訳単位ローカルのエンティティ(TU-local Entities)と呼びます。TU-localエンティティの正確な定義は複雑ですが、ほぼ内部リンケージ名を持つ関数・変数・型のことを指します。
それらTU-localエンティティがinline
関数などによって翻訳単位の外に曝露する可能性のある時、コンパイルエラーとなります。注意なのは、TU-localエンティティが曝露された時ではなく、その可能性がある段階でコンパイルエラーとなる事です。
export module M; // 内部リンケージの関数 static constexpr int f() { return 0; } static int f_internal() { return f(); } // 内部リンケージ、OK int f_module() { return f(); } // モジュールリンケージ、OK export int f_exported() { return f(); } // 外部リンケージ、OK // 外部orモジュールリンケージを持つinline関数は内部リンケージ名を参照できない static inline int f_internal_inline() { return f(); } // OK inline int f_module_inline() { return f(); } // NG export inline int f_exported_inline() { return f(); } // NG
もう一つ、inline
関数では無いけれどほぼ同じ振る舞いをするものにテンプレートがあります。テンプレートの厄介なところは、インスタンス化されるまで何を参照しているかが確定しない事にあります。そのため、テンプレートでは、インスタンス化された時に内部リンケージ名を参照する可能性がある場合にコンパイルエラーとなります。
/// mymodule.cpp export module mymodule; export struct S1 {}; // 内部リンケージ static void f(S1); // (1) export template<typename T> void f(T t); // (2) // インスタンス化前はエラーにならない export template<typename T> void external_f(T t) { f(t); }
/// main,cpp import mymodule; struct S2{}; void f(S2); // (3) int main() { S1 s1{}; S2 s2{}; external_f(10); // OK、(2)を呼ぶ external_f(s2); // OK、(3)を呼ぶ external_f(s1); // NG、(1)を呼ぶ }
内部リンケージ名を参照する可能性がある場合というのは、直接的に現れていなかったとしても、関数オーバーロードの候補集合に内部リンケージな関数が含まれている場合です。その場合使用する関数の決定を待たずにコンパイルエラーとなります。
テンプレートに関しては例外があり、内部リンケージ名を外部から参照しエラーとなる宣言であっても、モジュールのインターフェース内で予め特殊化されインスタンス化済みである時はエラーとなりません(inline
指定が無ければ)。
/// mymodule.cpp export module mymodule; export struct S1 {}; static void f(S1 s); // (1) // 宣言はOK export template<typename T> void external_f(T t) { f(t); } // S1に対するexternal_f()の明示的インスタンス化 template void external_f<S1>(S1); // (2)
/// main,cpp import mymodule; int main() { S1 s1{}; external_f(s1); // OK、(2)でインスタンス化済 }
この場合、モジュールは予めコンパイルされているはずなので、インスタンス化済のテンプレートのインスタンス化を省略し、通常の関数と同様にシグネチャのみで参照することができます。テンプレートはinline
指定がなければインライン展開されるとは限らず、その必要がありません。
このようにこの提案の後では、名前付きモジュールにおける関数に対するinline
指定はインライン展開の対象であることをコンパイラに伝えるマーカーとしての本来の役割のみを担うようになります。
参考資料
ABI isolation for member functions
これは、名前付きモジュール内で定義されたクラスについて、その定義内で定義されているメンバ関数の暗黙inline
をしなくするものです。
先ほどのP1815R2の変更によって、モジュールのインターフェース内のinline
関数内での内部リンケージ名の使用がコンパイルエラーとなるようになります。これによって大きな影響を受けるのは、クラスの定義内で定義されているメンバ関数です。
クラス定義内で定義されているメンバ関数は暗黙inline
であり、P1815R2の影響を強く受けることになります。
export module M; // 内部リンケージの関数 static constexpr int f() { return 0; } // エクスポートされ外部リンケージを持つクラス定義 export struct c_exported { int mf_exported(); int mf_exported_inline() { return f(); } // NG、暗黙inline }; int c_exported::mf_exported() { return f(); } // OK、暗黙inlineではない
これを避けようとすると、メンバ関数は全てクラス外で定義することになってしまい、冗長な記述が増え、非メンバ関数との一貫性がなくなります。この辺りの仕様は複雑なので、このことはユーザーにとって意味がわからないエラーとなるかもしれません。
クラス定義内で定義されたメンバ関数が暗黙inline
なのは、ヘッダに定義を書いて複数の翻訳単位でインクルードした時に多重定義エラーを起こさないためなので、モジュールの利用においてはほとんど必要ありません。
そのため、この提案では名前付きモジュール内に限って、クラス定義内で定義されたメンバ関数に対する暗黙inline
を行わないようにします。これによって、モジュールにおけるクラスの定義は今まで通りに行う事ができ、複雑なことを気にする必要は無くなります。
export module M; ... export struct c_exported { int mf_exported(); int mf_exported_inline() { return f(); } // OK、暗黙inlineではない };
ただし、このことはモジュールの外側(グローバルモジュール)においては従来通りです。モジュールではないところで定義されたクラスのクラス定義内で定義されたメンバ関数は相変わらず暗黙inline
です。
Modules Dependency Discovery
これは、module
とimport
を書くことのできる場所や形式を制限するものです。
P1703R1も同様の目的の変更でしたが、module
はなんら制限されておらず、import
を使用する既存のコードへの影響が小さくありませんでした。この提案はP1703R1のアプローチをさらに進めて、module
を用いる構文についても書き方や書ける場所を制限し、かつimport
とmodule
を使用している既存のコードへの影響を減らそうとするものです。
この提案では、次の条件を満たすもので始まる行は、inport
ディレクティブとmodule
ディレクティブとして扱われるようになります。
import
: 以下のいずれかが同じ行で後に続くもの<
- 識別子(identifier)
- 文字列リテラル
:
(::
とは区別される)
module
: 以下のいずれかが同じ行で後に続くもの- 識別子
:
(::
とは区別される);
export
: 上記2つの形式のどちらかの前に現れるもの
これらは新しいプリプロセッシングディレクティブとして扱われますが、プリプロセッシングディレクティブの扱いは従来通りであるため、ここでのimport, module, export
はマクロによって置換されたり導入されたりせず、ディレクティブは1行で書く必要があります。
// これらは各行がプリプロセッシングディレクティブとみなされる # module ; export module leftpad; import <string>; export import "squee"; import rightpad; import :part; // これらの行はプリプロセッシングディレクティブではない module ; export import foo; export import foo; import :: import ->
これによってまず、インポート宣言、モジュール宣言、グローバルモジュールフラグメント、プライベートモジュールフラグメントの構文は、マクロによって導入されず、1行で書かなければなりません。ただし、インポート対象の名前やモジュール名はマクロによって導入することができます。
なお、通常のexport
宣言はこれらの処理の対象ではありません。export
から始まるプリプロセッシングディレクティブはあくまで、すぐ後にimport/module
が現れるものです。
これらのディレクティブに含まれるimport, module, export
はプリプロセッサによってimport-keyword, module-keyword, export-keyword
に置き換えられ、この*-keyword
によるものがC++コードとしてのインポート宣言やモジュール宣言として扱われるようになります。
例えば次のようなコードは
module; // グローバルモジュールフラグメント #include <iosream> export module sample_module; // モジュール宣言 // インポート宣言 import <vector>; export import <type_traits>; // エクスポート宣言 export int f(); // プライベートモジュールフラグメント module : private; int f() { return 20; }
プリプロセス後(翻訳フェーズ4の後)に、おおよそ次のようになります。
__module_keyword; #include <iosream> __export_keyword __module_keyword sample_module; __import_keyword <vector>; __export_keyword __import_keyword <type_traits>; // export宣言はプリプロセッシングディレクティブでは無い export int f(); __module_keyword : private; int f() { return 20; }
__import_keyword
などは実装定義なので実際にどう書き換えられるかは分からず、それを直接書くための構文は用意されていません。
そしてもう一つの大きな変更は、プリプロセスの一番最初の段階でソースファイルがモジュールファイルなのか通常のファイルなのかを判定し、モジュール宣言、グローバルモジュールフラグメントとプライベートモジュールフラグメントはモジュールファイルだけに現れることが出来るように規定されている事です。
この判定は、ファイルの一番最初に現れる非空白文字がmodule
あるいはexport module
で始まっているかどうかをチェックすることで行われ、それがある場合にのみモジュールディレクティブに対応するディレクティブ(の処理方法)が定義されます。
通常のファイルとして処理された場合でもインポートディレクティブを処理することはできますが、モジュールディレクティブは対応するディレクティブが定義されないため、コンパイルエラーとなります。
この事は、#ifdef
などによってあるファイルがモジュールであるかヘッダファイルであるかを切り替える、ような事ができないことを意味しています。
これらの事はCプリプロセッサのEBNFによる構文定義の中で表現されており、少し複雑です。
Before | After |
---|---|
// OK、モジュール宣言 export module x ; // -Dm="export module x;" m // OK module; #define m x export module m; // OK module; #if FOO export module foo; // OK #else export module bar; // OK #endif module; #define EMPTY EMPTY export module m; // OK #if MODULES module; export module m; // NG #endif #if MODULES export module m; // OK #endif module y = {}; // NG ::import x = {}; // OK ::module y = {}; // OK import::inner xi = {}; // NG、インポートディレクティブ module::inner yi = {}; // OK namespace N { module a; // OK import b; // NG、インポートディレクティブ } #define MAYBE_IMPORT(x) x MAYBE_IMPORT( import <a>; // UB ) #define EAT(x) EAT( import <a>; // UB ) void f(Import *import) { import->doImport(); // NG、インポートディレクティブ } |
// NG export module x // モジュールディレクティブ ; // -Dm="export module x;" m // NG module; #define m x export module m; // OK module; #if FOO export module foo; // NG #else export module bar; // NG #endif module; #define EMPTY EMPTY export module m; // NG #if MODULES module; export module m; // NG #endif #if MODULES export module m; // NG #endif module y = {}; // NG ::import x = {}; // OK ::module y = {}; // OK import::inner xi = {}; // OK module::inner yi = {}; // OK namespace N { module a; // NG、モジュールディレクティブ import b; // NG、インポートディレクティブ } #define MAYBE_IMPORT(x) x MAYBE_IMPORT( import <a>; // UB ) #define EAT(x) EAT( import <a>; // UB ) void f(Import *import) { import->doImport(); // NG、インポートディレクティブ } |
さらに、モジュールファイル内においては#include
によってimport
ディレクティブが現れる事が禁止されます。これは#include
によって展開されたファイル内にimport
ディレクティブがあってはならないという事ですが、インポート可能ヘッダの#include
をヘッダユニットのインポートに置換する事が行われないことも意味します。
/// header.hpp
import <iostream>;
/// mymodule.cpp module: #include "header.h" // NG export module mymodule; #include "header.h" // NG import "header.h"; // OK module : private; #include "header.h" // NG
/// main.cpp #include "header.h" // OK import "header.h"; // OK int main(){}
これは検出されコンパイルエラーとなります。
参考資料
Issueの解決3
これは、エクスポートされたインポート宣言がモジュールインターフェース以外の場所で現れることを禁止するものです。
/// M.cpp export module M; // OK、モジュールAをインポートしつつエクスポート export import A;
/// M_impl.cpp module M; // Mの実装単位(not インターフェース単位) // NG、export importはここにかけない export import B;
/// main.cpp // NG、export importはここにかけない export import M; int main() {}
以前は特にケアされていなかったのでコンパイルエラーになっていませんでしたが、この変更によって明確にエラーにされます。
これは、異なるモジュールから到達可能となっている同じ無名のスコープ無し列挙型の定義をマージするためのルールを定めるものです。
無名のスコープ無し列挙型はヘッダファイルで一般的であり、同じものが複数のモジュールから到達可能となるとき、それが同一であることを判定しマージできなければODR違反となります。
これはヘッダユニットのインポートで特に問題となり得るため、そのような複数の定義が同じものであることを認識する方法を指定することでODR違反とならないようにします。
/// importable.h namespace X { enum { A }; // 無名のスコープ無し列挙型 (1) enum {}; // (2) }
/// M.cpp module; #include "importable.h" export module M; // 宣言が破棄されないようにする constexpr int N = X::A;
/// main.cpp // (1)の定義が異なる経路で到達可能となる // どちらも全く同じ定義を参照しているためマージされODR違反は起きない // (2)は翻訳単位毎に別の型として扱われるためODR違反は起きない import "importable.h" import M; int main() {}
これは従来からあるテンプレートのためのODRの例外規則を拡張する形で表現されています。つまり、定義が異なる翻訳単位から到達可能になっているときでも、その定義が意味的に完全に同一である場合にのみODRの例外を適用し定義が一つにマージされます。
列挙型としての名前がないので、これらの識別では最初の列挙子がその列挙型の名前として使用されます。そのため、列挙子を持たない無名の列挙型は常に異なる型として扱われます。
おわり
おおよそ時系列に沿っているはずです。細かいIssue解決は見逃しているかもしれません。あとGithubのリポジトリに直接コミットする形のeditorialな修正は追い切れていません・・・
あと、P1787R6 Declarations and where to find themの変更は直接的にはモジュールに対するものではなく、C++23での採択であるので省いています(それでも影響はそこそこありますが)。
もし見落としや間違いなどを発見されたら教えてくださいませ・・・