[C++]expression-equivalentのお気持ち

expression-equivalent??

標準ライブラリへのRangeの導入に伴って新たに追加された言葉で、次のように定義されています。

expressions that all have the same effects, either are all potentially-throwing ([except.spec]) or are all not potentially-throwing, and either are all constant subexpressions or are all not constant subexpressions

何となく噛み砕くと以下のような意味合いです

ある2つ以上の式は、次の全てを満たす場合にexpression-equivalentである

  • 式は同じ効果を持つ
  • 例外を投げるかどうかが同一
    • 全ての式は例外を投げない
    • もしくは、全ての式は例外を投げうる
  • 式が定数式で実行可能であるかも同一
    • 全ての式は、(部分式としても)定数実行可能である
    • もしくは、全ての式は(部分式としても)定数実行不可

これは要するに、式の効果と例外を投げるかどうか、および定数式で実行可能かどうか、が全く同一である時にexpression-equivalentの関係にある、という事です。

これは何?

これは主にRangeライブラリのカスタマイぜーションポイントオブジェクト(以下CPO)の効果の定義において頻出します。
大体以下の様な形式で書かれています。

The expression CPO-name(E) for some subexpression E is expression-equivalent to:

  • expression-equivalentとなる式 if 条件
  • Otherwise, expression-equivalentとなる式 if 条件
  • ...
  • Otherwise, CPO-name(E) is ill-formed.

ここでのCPO-nameは任意のカスタマイぜーションポイントオブジェクト名で、EとはそのCPOの呼び出しに引数として与えられている式のことです。
そしてこの文章は、引数EによってCPO-name(E)の呼び出しがどのような効果を持つか?をつらつらと書いています(大体最後はill-formedとなりますが、ならない場合もあります)。

これは、これまでの標準ライブラリ関数等ならばその効果(Effects)の定義において、Equivalent to :以下に書かれていたものです。
つまり、expression-equivalentはこれまで説明に使われていたEquivalent toをCPO用に置き換えているものだと言えます。

Equivalent toとの違い

Equivalent toでは、ある関数等の効果を別の式の効果と等価であるとして定義します。この時、その効果には式が例外を投げるのかどうか、また部分的にでも定数式で実行可能であるか、が含まれてはいないようです。
それらは式の効果ではなく、関数に指定されているものだからです。

Rangeライブラリのカスタマイゼーションポイントオブジェクトは名前空間スコープに定義された関数オブジェクトであり、その呼び出しではADL等の機構によりユーザーが定義した任意の型に対してさえも目的となる処理を行おうとします。 その結果として、CPOの効果は一通りではありません。

Rangeライブラリ利用ユーザーはCPOの持つ効果のどれかに引っかかるように巧妙に自分の持つ型をカスタマイズすれば、Rangeライブラリに定義されている処理に任意の型をアダプトできます。

効果が複数あり、しかも入ってくる型がどのようにその効果のいずれかに対してアダプトされているかはわかりません。そのため、CPOの呼び出しは定数実行できるのか?呼び出しに伴って例外を投げるのか?は実行される処理によります。

Equivalent toでは指定した式のconstexpr性及びnoexcept性は伝播されないので、Equivalent toで効果を指定するだけではCPOの呼び出しがconstexprであるかnoexceptであるかは未規定になってしまいます。

そのため、expression-equivalentが必要になります。expression-equivalentな関係にある2つの式は、効果だけではなくconstexpr性及びnoexcept性に関しても同一です。
従って、CPOの呼び出しの効果としてexpression-equivalentとされている式に呼び出された型を当てはめることで、その効果と定数実行可能であるか?例外を投げうるか?をも含めて表明することができます。

なにいってんだこいつという感じなので例として、std::ranges::begin()を見てみましょう。
std::begin()は関数ですが、これはカスタマイゼーションポイントオブジェクトです。その効果は次のようにあります。

The name ranges​::​begin denotes a customization point object. The expression ranges​::​​begin(E) for some subexpression E is expression-equivalent to:

  • E + 0 if E is an lvalue of array type ([basic.compound]).
  • Otherwise, if E is an lvalue, decay-copy(E.begin()) if it is a valid expression and its type I models input_­or_­output_­iterator.
  • Otherwise, decay-copy(begin(E)) if it is a valid expression and its type I models input_­or_­output_­iterator with overload resolution performed in a context that includes the declarations: template<class T> void begin(T&&) = delete;
    template<class T> void begin(initializer_list<T>&&) = delete;
    and does not include a declaration of ranges​::​begin.
  • Otherwise, ranges​::​begin(E) is ill-formed.

引数の式をEとしてstd::ranges::begin(E)のように呼び出したとき、その効果はEに応じてその下に書かれている4つのいずれかと等価(expression-equivalent)、という事を言っています。

ユーザーが自作する任意の型に対して作用するのはおそらく2つ目と3つ目のものです。それぞれ以下のように定義されます。

  • 2つ目は、引数Eの(結果となるオブジェクトの)メンバ関数として定義されているE.begin()を呼び出す。
  • 3つ目は、Eの(結果となるオブジェクトの)関連名前空間からADLで見つかるbegin()か、std::begin()を呼び出す。

このbegin()をアダプトしたつもりの自作の型Tのオブジェクトaで呼び出したときにexpression-equivalentであるとは

2つ目の形式にアダプトした場合、std::ranges::begin(a)の呼び出しがconstexprとなるかは(あなたが書いた)a.begin()の定義によって決まり、例外を投げるかも(あなたが書いた)a.begin()によって決まるという事です。

同様に、3つ目の形式にアダプトした場合も、ユーザーが定義した(あなたが書いた)フリー関数のbegin(a)constexprなのかnoexceptなのかでそれらが決定される訳です。

さらに、どちらの場合も結果となるイテレータdecay_copyされて返されますが、このdecay_copyの処理が同様にconstexprなのかnoexceptなのかも(おそらくあなたが定義しているであろう)返されたイテレータ型によるわけです。

つまりはとっても他力本願な定義の仕方なのです。

参考文献

謝辞

この記事の9割は以下の方によるご指摘によって成り立っています。

この記事のMarkdownソース

[C++]ラムダ式から関数参照への変換(単項*演算子)

状態を持たないラムダ式は単項+演算子を頭につけると明示的に対応する関数ポインタに変換することができます。では、ポインタではなく関数への参照に変換したい時はどうすれば良いのでしょうか?
+をつけないだけでは関数参照にはなってくれません・・・

template<typename F>
void invoke(F&& f) {
  std::cout << "call functor." << std::endl;
  std::cout << f() << std::endl;
}

void invoke(int(&f)(void)) {
  std::cout << "call function reference." << std::endl;
  std::cout << f() << std::endl;
}

int main()
{
  invoke( []{ return 10; });
  invoke(+[]{ return 20; });
}

/* 出力
call functor.
10
call functor.
20
*/

[Wandbox]三へ( へ՞ਊ ՞)へ ハッハッ

あるのかわからないですが、こういう時に関数の参照をとるオーバーロードが選ばれてほしい時、わざわざ関数ポインタの型をusingしてキャストして...としなくても次のように頭に*を付けることで解決できます。

template<typename F>
void invoke(F&& f) {
  std::cout << "call functor." << std::endl;
  std::cout << f() << std::endl;
}

void invoke(int(&f)(void)) {
  std::cout << "call function reference." << std::endl;
  std::cout << f() << std::endl;
}

int main()
{
  invoke( []{ return 10; });
  invoke(+[]{ return 20; });
  invoke(*[]{ return 30; });
}

/* 出力
call functor.
10
call functor.
20
call function reference.
30
*/

[Wandbox]三へ( へ՞ਊ ՞)へ ハッハッ

見た目からすると、ラムダの頭に*を付けることで明示的に関数参照へ変換しているように見えます。

なお、同じシグネチャの関数ポインタをとるオーバーロードがあるとオーバーロード解決に失敗します。関数参照は関数ポインタに暗黙変換可能であり、その変換はオーバーロード順位に影響を及ぼさない為です(これはC++17以上からの挙動のようです)。

[Wandbox]三へ( へ՞ਊ ՞)へ ハッハッ

組み込みの単項*演算子

C++17規格の組み込み単項*演算子について見てみると次のようにあります。

he unary * operator performs indirection: the expression to which it is applied shall be a pointer to an object type, or a pointer to a function type and the result is an lvalue referring to the object or function to which the expression points.

なんとなく訳すと(powered by google翻訳

単項*演算子は間接参照を行う。引数型はオブジェクト型へのポインタ、もしくは関数へのポインタでなければならず、結果は引数が指すオブジェクトまたは関数を参照するlvalueとなる。

つまり、ラムダ式*を適用すると、暗黙の型変換により関数ポインタに変換され、その関数ポインタ参照先の実体が参照として返されており、戻り値型は関数参照型となるわけです。

使いどころ?

正直わかりません。ほぼノーコストで対応する関数ポインタへ暗黙変換されるのでラムダ式に単項+を使いたくなるときに替わりに使っても良いかもしれません。
そのようなコードを持っていけば誰かにドヤ顔できるかもしれません・・・・

小ネタ

単項+は関数ポインタから関数ポインタへ、単項*は関数ポインタから関数参照へ、それぞれ変換するので、それらを複数組み合わせることができるはずです、そう幾つでも・・・

int main()
{
  invoke(*+*+*+*[]{ return 10; });
  invoke(+*+*+*[]{ return 20; });
  invoke(*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*+*[]{ return 30; });
  invoke(**************+ + + + + + + + + + + + + + + + + + + + + + +**************+*+ + +*+*+ + +[]{ return 40; });
}

[Wandbox]三へ( へ՞ਊ ՞)へ ハッハッ

残念ながら、+を複数繋げるとインクリメントになってしまうので間にスペースが必要です。*の連続適用は関数ポインタへ暗黙変換されるので問題ありません。

参考文献

この記事のMarkdownソース

[C++]constinit?🤔

constinit指定子

constinit指定子はC++20より変数に付けることができるようになるもので、constexpr変数がコンパイル時に初期化される事を保証するように、constinit変数が静的初期化、特に 定数初期化 されている事を保証します。

しかし、constexprとどう違うのか、なにが嬉しいのか、などは中々理解しづらいものがあります。

提案文書より、サンプルコード。

const char *g() { return "dynamic initialization"; }
constexpr const char *f(bool p) { return p ? "constant initializer" : g(); }
  
constinit const char *c = f(true);  // OK.
constinit const char *d = f(false); // ill-formed

一見すれば、初期化式が定数式で実行可能ではない時にエラーを起こしているように見えます。とするとやはり、constexpr指定との差がよくわかりません・・・

静的変数の初期化

グローバル(名前空間スコープの)変数やクラスの静的メンバ変数、関数ローカルのstatic変数など静的ストレージにあるものはプログラムの開始前に、スレッドローカルストレージにあるものはスレッドの開始前に、それぞれ初期化が完了しています。

それらの変数はまずコンパイル時に 静的初期化 され、実行時(プログラムロード時、スレッド起動時)に 動的初期化 されます。結果として、プログラム開始時もしくはスレッド開始時には初期化が完了しているように見えているわけです(正確には実装は変数が使用される直前まで初期化を遅延させることが許可されています)。

動的初期化はその初期化式が定数式で実行できない場合に実行時に行われるものです。ここでは重要では無いので深掘りしません。

静的初期化はコンパイル完了時までになんらかの値で初期化しておくもので、 定数初期化ゼロ初期化 の2段階で行われます。

静的初期化においてはまず、定数初期化が可能であるならば変数は定数初期化されます。これによってその変数の初期値は確定し、以降の初期化はスキップされます。
次に、定数初期化できなかった残り全ての変数をゼロ初期化します。ゼロ初期化は変数をゼロに相当する値によって初期化するものです(例えば、浮動小数点型の0.0、ポインタ型のnullptr等)。この時、クラス型のコンストラクタは無視され、その型を構成する全ての型が再帰的にゼロ初期化されます。
これにより、静的ストレージ・スレッドローカルストレージにある変数は全てとりあえずはなんらかの値で初期化されている状態でコンパイルが終了します。

静的初期化された値はプログラムイメージ(実行ファイルバイナリ、アセンブリ)の一部としてプログラム内のどこかに埋め込まれています。

動的初期化はこのように初期化されている変数に対して、実行時に実際の初期化式によって初期化を行います。

定数初期化(constant initialization)

定数初期化は静的初期化の中で、他のあらゆる初期化に先行して行われます。そして、定数初期化が完了すればその変数の初期化はそこで完了しており、その後一切の初期化処理は行われません。

定数初期化を行うためには、変数が静的ストレージかスレッドローカルストレージにあり、その初期化式が定数式でなくてはなりません。
それを満たせば全ての変数は定数初期化できます。constexprが付いていなくてもいいですし、constがなくても大丈夫です。初期化式がconstexpr/consteval関数を呼び出していても、初期化に当たって一時オブジェクトを生成しても構いません。

ただし、定数初期化されているconstな整数型と列挙型だけが他の定数式で利用でき、その他の種類の変数は定数初期化されただけでは定数式で使えません。

定数初期化コンストラク

定数初期化はもちろん任意のクラス型に対しても行えます。
そのクラスがリテラル型でなかったとしても、constexprコンストラクタを持ち、そのコンストラクタからメンバ変数を全て定数式で初期化できれば、そのクラスのオブジェクトは定数初期化できます。

これを利用しているクラスはSTLにも存在しており、std::mutexのデフォルトコンストラクタ、std::unique_ptrのデフォルトおよびnullptrを受けるコンストラクタなどが定数初期化コンストラクタを持っています(std::unique_ptrのこれらのコンストラクタになぜconstexprが付いているのか不思議に思った人は多いのではと思います、こういう事です)。

上でも述べていますが、定数初期化(静的初期化)は静的ストレージかスレッドローカルストレージにあるものが対象で、ローカル変数に対しては適用されません。

#include <mutex>
#include <memory>
#include <thread>

struct C {
  C() = default;

  C(int n) : m(n) {}

  operator int() const noexcept {
    return m;
  }

private:
  int m;
};

//全て定数初期化される
std::mutex m{};
std::unique_ptr<int> p1; 
std::unique_ptr<int> p2 = nullptr;
C c1{};

//これは動的初期化になる(切り替えられるかもしれない)
C c2{10};

int main() {
  std::lock_guard lock{m};

  int n = c1;
  int m = c2;

  //ローカル変数の初期化はまた別の話・・・
  std::unique_ptr<int> p3;
}

出力アセンブリ例 - Compiler Explorer

コンパイル結果を見ても静的初期化されてるのか動的初期化されてるのかはよくわからないですね・・・。gccの方は__static_initialization_and_destruction_0(int, int):なるセクションに突っ込まれているのはわかりますが・・・

動的初期化の静的初期化への切り替え

静的初期化(定数 or ゼロ初期化)は必ず行われた上でコンパイルが完了しています。
そして追加で、コンパイラは次の条件を満たす場合に動的初期化を静的初期化に切り替えることが許されています。

  • 動的初期化で実行される予定の初期化式は副作用を持たない
  • 静的初期化に切り替えても、動的初期化した場合と全く同じ値で初期化できることが保証できる
    • 他の静的変数の初期化式に依存している・されている場合でも結果が同じにならなくてはならない
    • すなわち、プログラム全体として初期化後の結果は切り替え前と全く同一でなければならない

すなわち、通常の初期化順序に沿って動的初期化を行った結果と(プログラム全体として)全く同じ結果になることがコンパイル時に分かる場合に、動的初期化を静的初期化に切り替えることが許されます。

動的初期化から切り替えてゼロ初期化するというケースは無いと思うので、実質的に定数初期化されることになります。
この初期化タイミングの切り替えは可能であっても必ず行われるとは限りません。コンパイラによります。

constinitの効能

変数を静的初期化、特に定数初期化しておくことのメリットは、その変数の初期化に関してデータ競合などを考える必要がないことです。

動的初期化では初期化処理はプログラム実行時(スレッド起動時)の一番最初に起こり、その初期化の順序及び初期値に依存するようなコードではデータ競合によって思わぬバグを仕込むことになる可能性があります。
翻訳単位を超えて動的初期化される変数を使っている場合などはその初期化順およびそれに起因するバグを理解することは非常に困難になるでしょう・・・

静的初期化では初期化処理はコンパイル時に完了しており、そこでは翻訳単位毎に宣言順で初期化されるためデータ競合は発生しません。プログラム開始時、動的初期化開始前にはその初期値は確定しており静的初期化された変数の初期値に依存するような処理を少しだけ安全に書くことができます。

通常の静的初期化は少なくともゼロ初期化が行われている事は間違い無いのですが、定数初期化が行なわれたかどうか、つまり変数が動的初期化されるのかどうかをコンパイル後に知る事は困難です。
前項の動的初期化からの切り替えも必ず行われるとは限りませんし、思わぬ変更から定数初期化しているつもりの初期化式が動的初期化になってしまっている事もありえます。本当に定数初期化されたかを見るのはアセンブリを確認するしかありません・・・

これはconsteval関数が導入された理由の一つと同じ問題です。つまり、constexpr変数の初期化以外の所でconstexpr関数が本当にコンパイル時に実行されたかどうかは容易には分からなかったのです。

constexpr変数・consteval関数と似たように、constinit変数は変数が動的初期化される場合にコンパイルエラーを起こします。別の言い方をすると、constinit指定子はconstinit変数が動的初期化されないことを保証します。
そして、constinit変数は確実に静的初期化によってコンパイル時に初期化が完了します。

利用例

前述のように、constinit指定は静的・スレッドローカルストレージにある変数に指定でき、その初期化式が定数式でなければなりません。
初期化式が無い場合、静的・スレッドローカルストレージにある変数はゼロ初期化され、実行すべき初期化式が無いために動的初期化されません。したがって、constinit変数に初期化式がない場合はゼロ初期化が保証されます。

静的初期化されるかどうかはリンケージ指定(static, extern)とは無関係ですが、別の翻訳単位で定義されている変数のextern宣言に対してのconstinit指定は未定義動作を引き起こすので注意が必要です(おそらくエラーにはなりません)。

#include <mutex>
#include <memory>
#include <random>

constinit const int N = 1;      //ok
constinit unsigned int M = N;   //ok、constな整数型は定数式で利用可能
constinit constexpr int L = 1;  //ng、constinitとconstexprを同時に指定できない

constinit thread_local static int Counter = 0; //ok

constinit const double PI = 3.1415; //ok
constinit double PI2 = PI + PI;     //ng、変数PIは定数式で利用不可

constinit static int O; //ok、ゼロ初期化される
constinit int Array[3]; //ok、ゼロ初期化される

constinit std::mutex m{};           //ok、定数初期化コンストラクタ呼び出し
constinit std::unique_ptr<int> p1;  //ok、定数初期化コンストラクタ呼び出し

constinit extern int def = 10;  //ok
constinit extern int ext;       //ng、おそらくエラーにはならないが未定義動作(診断不要)


struct S {
  constinit static const int x;
  static const int y;
  static constexpr int z = 56;
};

const int S::x = 12;            //ok、constinit変数なので定数初期化される
constinit const int S::y = 34;  //ok、constinit変数なので定数初期化される
constinit constexpr int S::z;   //ng、インライン変数に対する多重定義
                                //constexpr静的メンバ変数に対するクラス外定義はC++17以降非推奨

int main() {
  constinit static std::unique_ptr<int> ptr = nullptr;                //ok、静的ローカル変数
  constinit thread_local std::mt19937 engine(std::random_device{}()); //ng、定数式で初期化できない

  constinit int local = 0;  //ng、ローカル変数
}

[Wandbox]三へ( へ՞ਊ ՞)へ ハッハッ

constinit指定は変数宣言に指定でき、その効果はその変数の初期化宣言に対して適用されます。通常の変数はその2つを分かつことができませんが、extern変数や静的メンバ変数のように宣言と定義(初期化宣言)が別れる場合、定義からconstinit宣言が到達不可能となると未定義動作(診断不要)です。

なお、名前にconstが付いているので紛らわしいかもしれませんが、constinit変数は暗黙constではなくconst変数にしか付けられないわけでもありません。
constinit変数はconstexpr変数とは異なり、明示的にconst修飾されていなければ実行時に値を変更することができます。

const一族

const constexpr consteval constinit
誕生時期 神代の頃 C++11 C++20 C++20
変数に付加 🔺
関数に付加 🔺
変数への効果 immutable化 定数式でも使用可能
実行時にconst
コンパイルエラー 静的初期化保証
関数への効果 const変数からのみ呼出可能 定数式でも使用可能 定数式でのみ使用可能 コンパイルエラー

参考文献

この記事のMarkdownソース

[C++]std::regexでパターンにマッチするすべての文字列を抽出する

std::regexを使う時多くの場合はstd::regex_searchを使うと思われますが、std::regex_searchはそのままだとマッチする1番最初の文字列しか得ることができません。
しかし、文字列の中からマッチする全ての要素が欲しいということはよくあることでしょう。調べても頑張ってずらしてもう一回searchだ!という方法しか出てきません。やはりスマートにやりたい・・・

std::match_results::suffix()

std::regex_searchの結果として得られるstd::match_resultssuffix()というメンバ関数を持っています。この関数は結果としてマッチした文字列を除いた残りの文字列(への参照)を返します。
その文字列に対してもう一度searchすれば2番目にマッチする部分文字列が得られ、それを繰り返せば残りのマッチング文字列を得ることができます。

#include <regex>

int main()
{
  std::string str = "1421, 34353, 7685, 12765, 976754";
  std::regex patern{R"(\d+)"};
  std::smatch match{};
  
  while (std::regex_search(str, match, patern)) {
    std::cout << match[0].str() << std::endl;
    str = match.suffix();
  }
}

[Wandbox]三へ( へ՞ਊ ՞)へ ハッハッ

とても簡単な方法ではありますが、一々コピーが発生するのと変更可能なstd::string等でしか使えないのが少し残念なところです。

std::sub_match::second

頑張ってずらしてもう一回searchだ!という方法を頑張らないでやる方法です。

std::match_resultsstd::sub_matchとしてパターン内各グループの結果を保持しており、一番最初のstd::sub_matchは見つかった文字列全体が得られます。
そして、std::sub_match::secondはそのサブマッチ文字列の次の位置を指すイテレータです。

つまり、一番最初のstd::sub_match::secondを始点としてもう一度std::regex_searchをすれば2番目にマッチする文字列が得られ、それを繰り返せば文字列内からパターンに一致する全ての部分文字列を抽出することができます。

#include <regex>

int main()
{
  constexpr char str[] = "1421, 34353, 7685, 12765, 976754";
  std::regex patern{R"(\d+)"};
  std::match_results<const char*> match{};
  
  for (bool ismatch = std::regex_search(str, match, patern); ismatch != false; ismatch = std::regex_search(match[0].second, match.suffix().second, match, patern)) {
    std::cout << match[0].str() << std::endl;
  }
}

[Wandbox]三へ( へ՞ਊ ՞)へ ハッハッ

for文の宣言部がとても長くて見づらいですが、std::regex_searchを繰り返し毎に実行し、その結果のbool値を見て終了判定しています。

そして、ループ中のstd::regex_searchmatch[0].secondから始めることでそれまでに一致した部分を飛ばして探索しています。なお、match.suffix().secondというのは元の文字列の終端(std::end(str)相当)に当たります(std::end(str)でも良いはずですが、型が合わないと怒られたのでこうしました・・・)。

この方法はイテレータを用いて元の文字列の参照範囲を変更して再検索しているだけなので、文字列のコピーは発生しません。

std::regex_iterator

上記std::sub_match::secondを用いる方法をラッピングしたイテレータstd::regex_iteratorとして標準に用意されています。

int main()
{
  constexpr char str[] = "1421, 34353, 7685, 12765, 976754";
  std::regex patern{R"(\d+)"};
  
  for (std::regex_iterator<const char*> itr{std::begin(str), std::end(str), patern}, last{}; itr != last; ++itr) {
    std::cout << (*itr).str() << std::endl;
  }
}

[Wandbox]三へ( へ՞ਊ ՞)へ ハッハッ

多少スッキリとし、基本的なイテレータ操作で書けているので処理も分かりやすいです。
先ほどのstd::sub_match::secondを使って次を検索、という部分をstd::regex_iteratorが中でよろしくやってくれています。

regex_searchesを作る

やはり、regex_searchのように一発でやりたいし、なんなら範囲for文使いたいです。なので綺麗にラッピングして少し便利にしてやりましょう。

template<typename Str>
auto regex_searches(Str& str, const std::regex& patern) {
  using std::begin;
  using std::end;
  using str_iterator_t = decltype(begin(str));
  
  struct wrap_regex_iterator {
    using iterator = std::regex_iterator<str_iterator_t>;
  
    auto begin() const noexcept -> iterator {
      return first;
    }
    
    auto end() const noexcept -> iterator {
      return last;
    }
    
    explicit operator bool() const noexcept {
      return first != last;
    }
    
    iterator first;
    iterator last;
  };
  
  return wrap_regex_iterator{{begin(str), end(str), patern}, {}};
}


int main() {
  const std::string str = "1421, 34353, 7685, 12765, 976754";
  std::regex patern{R"(\d+)"};
  
  for (auto&& match : regex_searches(str, patern)) {
    std::cout << match.str() << std::endl;
  }
}

[Wandbox]三へ( へ՞ਊ ՞)へ ハッハッ

ローカルクラス大好きなのでローカルクラスを使いましたが、外にあっても構いません。あとoperator boolはあくまでregex_searchのように戻り値を利用するために付けただけなのでなくてもいいです。というか範囲forで使う分にはほぼ無意味です。

入力となる型などを厳密にする場合は、std::regex_searchの各オーバーロードと同様にする必要がありますが、ここでは割愛・・・

書くことが多くなるので、数カ所で使うとかどうしても範囲forでーというときに多少便利になるかもしれません。

参考文献

謝辞

この記事の7割は以下の方によるご指摘によって成り立っています。

この記事のMarkdownソース

[C++]モジュール理論 上級編(魔境編)

※この記事はC++20を相談しながら調べる会 #2の成果です。

※この内容はC++20より有効なものです。C++20正式策定までの間に内容が変化する可能性があります。

前回記事で説明していることは説明しなおしません。

onihusube.hatenablog.com

importmoduleというキーワード

exportC++11以前に規定されていたテンプレートのエクスポートという機能のために使用されていたキーワードだったため、C++11以降もユーザーが使用することは出来ませんでした。
しかし、importmoduleというキーワードは予約もなにもされていなかったので以前から自由に使えます(C++20においても使用可能)。
しかしモジュールにおいても文脈依存キーワードとして使用するため以前の挙動が変化することがあります。

template<typename>
class import {};  //ok

import<int> f();  //ng!(C++17以前はok)
                  //C++20より、ヘッダーユニットのインポート宣言として解釈され、エラー
::import<int> g();//ok


class module;     //ok
module *m1;       //ng、モジュール宣言とみなされる(C++17以前はok
::module *m2;     //ok


class import {};  //ok(再宣言とかはとりあえず気にせず・・・
import j1;        //j1というモジュールをインポート(C++17以前は変数宣言
::import j2;      //変数宣言

詳細には、moduleexport moduleimportexport importのどれかで始まりその後に::が続かない宣言は、必ずモジュール宣言もしくはインポート宣言として扱われます。

すなわち、以下の様なものが存在している可能性があるのです・・・

module::C f1();         //module::C型を返す関数f1の宣言

export module::C f2();  //module::C型を返す関数f2のエクスポート宣言

import::T g1();         //import::T型を返すg1()の宣言

export import::T g2();  //import::T型を返すg2()のエクスポート宣言

これからはimportmoduleを何らかの名前にするのはやめましょう。

プリプロセッサの扱い

モジュール内部でも#include #defineをはじめとするプリプロセッサは使用可能です。importexportをマクロで生成・切り替えすることは特に禁止されていません(グローバルモジュールフラグメントの後のモジュール宣言は除く)。

しかし、従来のヘッダファイルのようにインクルード前に特定の#defineをしておくことでヘッダ内の挙動を変更する、というようなことは出来ません。

なぜなら、モジュールはそれそのものが一つの翻訳単位であり、importするころには個別に何らかの形にビルドされているからです。さらに言えば、import宣言はインポートする翻訳単位内宣言の可視・到達可能性にのみ影響するため、マクロがそれを介してモジュール内部に影響を与えることは不可能です。

ただし、モジュールのビルドに関しては何ら規定がなく完全に実装に一任されています。どの段階までビルドされているかは実装依存ですが、モジュール宣言の識別やexport宣言を確定させなければならない事から、少なくとも翻訳フェーズの4(プリプロセッサの実行とプリプロセッサディレクティブの削除)の完了まではビルドされているはずです。

マクロのエクスポート

通常の名前付きモジュール内でのexport宣言ではマクロをエクスポートすることは出来ません。

なぜなら、export出来るのは何らかの名前を導入する宣言のみでありマクロ定義そのものは宣言ではありません。もちろんマクロがプリプロセスの結果としてexport可能な宣言に置換される場合はその宣言の導入する名前がエクスポートされます。

///Mymodule.cpp
export module Mymodule;

export #define PI 3.14159     //ng、名前を導入する宣言でないのでコンパイルエラー
export #define STR(str) #str  //ng、名前を導入する宣言でないのでコンパイルエラー

//上記2つはエクスポート宣言として解釈される頃には以下のようになっている
//export
//export

#define E 2.718281

export const double e = E;    //ok、グローバル変数eのエクスポート

#ifdef PREDEFINED

export int f(); //ok、実装略

#else

export int g(); //ok、実装略

#endif


///main.cpp
#define PREDEFINED
import Mymodule;

int main() {
  //マクロのエクスポートがコンパイルエラーとなっていなかったとして
  double pi = PI;             //ng
  char str[] = STR(Modules);  //ng
  double e2 = 2.0 * e;        //ok
  int n = f();                //ng
  int m = g();                //ok
}

なお、ヘッダーユニットでは例外的にマクロをエクスポートすることができます。ヘッダーユニットの場合は、ヘッダーファイルを翻訳フェーズ7完了(テンプレート以外のコンパイル完了)までコンパイルされたモジュールとしてインポートすることになり、マクロはその際の翻訳フェーズ4終了直前にヘッダーユニット内に残っているものがエクスポート(ヘッダーユニットインポートの直後で再定義)されます。

そのため、結局ヘッダーユニットでもマクロの事前定義によって内部に影響を与えることは出来ません。

名前が可視でなければ定義が到達可能でもないクラスの利用

一体何を言っているのでしょうか・・・

モジュール仕様の重箱の隅としてこのようなクラスが割と容易に存在できてしまいます。

///Othermodule.cpp
export module Other;

//宣言(型名)のみエクスポート
export struct hidden_def;

export hidden_def get_hidden_def();

module : private;

struct hidden_def {
  int n = 10;
};

hidden_def get_hidden_def() {
  return {20};
}


///Mymodule.cpp
export module Mymodule;
import Other;

//エクスポートしない
struct hidden_name {
  double d = 1.0;
};

export hidden_name get_hidden_name() {
  return {3.14};
}

export hidden_def* get_ptr() {
  static auto hd = get_hidden_def();
  return &hd;
}


///main.cpp
import Mymodule;

int main() {
  auto hn = get_hidden_name();  //ok
  auto* hd = get_ptr();         //ok

  hidden_name hn2 = get_hidden_name();  //ng、hidden_nameは可視ではない
  hidden_def* hd2 = get_ptr();          //ng、hidden_defは可視ではない

  double d = hn.d;  //ok、定義は到達可能
  int n = hd->n;    //ng、定義は到達可能ではない
}

このように、関数の戻り値型として付属してエクスポートされた型は、必ずしもその名前が可視にはならず、場合によっては定義も到達可能になりません。
例にあるように、autoで受けることで使用可能にはなるようです。そのメンバを利用するには定義が到達可能でなくてはなりません。

詳細は後述しますが、ADLによってこうした型に対する関数の探索は不思議な大ジャンプをすることがあります。

///Othermodule.cpp
export module Other;

namespace Other {

  //宣言(型名)のみエクスポート
  export struct hidden_def;

  export hidden_def get_hidden_def();

  export int f(hidden_def* phd);
}

module : private;

namespace Other {
  struct hidden_def {
    int n = 10;
  };

  hidden_def get_hidden_def() {
    return {20};
  }

  int f(hidden_def* phd) {
    return phd->n;
  }
}


///Mymodule.cpp
export module Mymodule;
import Other;

export hidden_def* get_ptr() {
  static auto hd = get_hidden_def();
  return &hd;
}


///main.cpp
import Mymodule;

int main() {
  auto* hd = get_ptr(); //ok

  int n = f(hd);        //ok、ADLによってモジュールOther内のOther::f()の宣言が見つかる
                        //Other::f()は外部リンケージを持つため呼び出し可能
}

このように、直接インポートしているわけではないのにも関わらず、翻訳単位を飛び越えてADLが関連名前空間の探索を行い、関数を見つけてきています。
この場合は探索のきっかけとなった型が直接所属する名前空間の範囲内にのみ起こり、見つかった宣言は外部リンケージを持っていないと呼び出せません。

inline変数・関数

 モジュールにおいてもinline変数・関数の性質や意味合いに変化はありませんが、名前付きモジュールに属する宣言は同一かどうかに関わらず複数の定義を持つことは出来ません。

 モジュールにおいてのinline変数・関数は以下の規則の下宣言・定義できます。

  1. 名前付きモジュールに属する定義は厳密に唯一つでなければならない
  2. ある変数・関数の定義が、それに対する最初のinline宣言の時点で到達可能であってはならない
    • inline指定は定義そのもの、もしくはそれより前の宣言で行うこと
  3. 外部・モジュールリンケージを持つinline変数・関数の宣言は、宣言されている定義領域末尾から到達可能でなければならない
    • 定義領域とは、プライベートモジュールフラグメントがある場合はプライベートモジュールフラグメントを境界とした2つの領域、ない場合は翻訳単位全体
  4. 名前付きモジュールに属しているinline変数・関数は、宣言されている定義領域内で定義されていなければならない

重要なのは1つ目と4つ目の規則で、残りは注意点の様なものです。

これらの規則とODRから、モジュール内部でinline変数・関数を使用しようとする場合はその定義はそのモジュール(の翻訳単位)内部でなされなければならず、モジュール外部で定義された、もしくは外部にも定義がある、という事は許されない事が分かります。

///Mymodule_interfacepart.cpp(インターフェースパーティション
export module Mymodule:Interface;

export inline double g(); // ng、この翻訳単位内に定義がない

double use_g() {
  return g(); //ng、定義が到達可能でない
}

///Mymodule_implpart.cpp(実装パーティション
export module Mymodule:Part;
import :Interface;

//:Part内でのみ参照可能(インポートされない限り)
inline constexpr double PI = 3.141592;

//:Interfaceにg()の宣言がなければok
inline double g() {
  return PI;
}


///Mymodule.cpp(プライマリインターフェース単位
export module Mymodule;
export import :Interface;

export int f(); //非inlineでエクスポート、この時点ではf()は非inline関数

inline int f(); //再宣言、以降f()はinline関数、この宣言はインポート先で到達可能

double use_f() {
  return f(); //ok、翻訳単位末尾から定義が到達可能
}

inline int f() {
  return 10;
}


///Mymodule_impl.cpp(実装単位
module Mymodule;

//この実装単位内でのみ参照可能
inline constexpr int N = 10;


///OnefileModule.cpp
export module One;

export inline int h();  //ng、プライベートモジュールフラグメントに入る前に定義が必要

module : private;

int h_impl() {
  return 1;
}

inline int h() {
  return h_impl();
}


///main.cpp
import MyModule;
import One;

int main() {
  int n = f();      //ok、定義はプライマリインターフェース単位にあり到達可能
  double pi = g();  //ng
  int m = h();      //ng
}

なお、グローバルモジュール(すなわちモジュール外部)においてはこれまで通りにヘッダを用いるなどしてinline変数・関数を定義し、使用できます。ただし、その宣言・定義がモジュール内部のものとかち合ってしまうとODR違反となります。

グローバルモジュールフラグメントの利用

モジュール内でinline変数・関数を利用しようとすると、これまでのようにヘッダを用いることはできず、必ずモジュール内部で定義すること!と言うことを言っていましたが、実はこれには抜け穴があります。グローバルモジュールフラグメントを利用すると、上記の規則を完全に満たしつつ今までとほぼ同様の書き方ができてしまいます。

///inline.hpp(ヘッダファイル
inline constexpr double PI = 3.141592;

inline int f() {
  return 10;
}

inline int g() {
  return 100;
}

///Mymodule.cpp
module;

//グローバルモジュールフラグメント内に展開された宣言はグローバルモジュールに属する
#include "inline.hpp"
//定義はこの場所、グローバルモジュールにおいてなされる

export module MyModule;

export bool check_addr(const double* p) {
  return &PI == p;  //ok、inline宣言も定義も到達可能
}

int use_f() {
  return f(); //ok、inline宣言も定義も到達可能
}

///main.cpp
import MyModule;      //PIとf()は可視ではないが、到達可能かは未規定(g()は破棄されている)
#include "inline.hpp" //PIとf(),g()の宣言と定義がコピペされ可視になる
                      //MyModule内定義が到達可能だったとしてもODR違反にはならない

int main() {
  double s = PI * 2 * 2;    //ok
  int n = f();              //ok
  bool b = check_addr(&PI); //ok、b == true
}

グローバルモジュール、すなわちモジュール外コードではこれまでにできていたことが出来るようになっています。すなわち、グローバルモジュールでは定義はその文字列及び意味が完全に同一ならば複数存在することができます。
それを踏まえて上記規則をそれぞれチェックしてみれば、問題ない事がわかるでしょう。

この様にすると、今までの様にヘッダ定義をインクルードすることによってinline変数・関数を定義でき、複数のモジュールおよび翻訳単位に渡ってその定義を1つにすることができます。
逆に言うと、複数のモジュールで同一のinline変数・関数を使用するにはこうする他にありません。

モジュールにおけるテンプレートのインスタンス

当然ですが、テンプレートはモジュールにおいても使用可能ですし、エクスポートもできます。エクスポートしたテンプレートはモジュールの外で使用されるタイミングでインスタンス化されます。

そのインスタンス化の際、そのテンプレートの内部(定義)から参照できるものがその定義したところ(モジュール内部)だけだとモジュールでエクスポートしたいテンプレートを書くときの制限がとても多くなってしまいます。
そのため、インスタンス化経路path of instantiation)というルールを導入し、この経路上にあるものがインスタンス化時に参照可能になっています。

インスタンス化時に利用されるものは、その定義されたところでもインスタンス化したところでも可視又は到達可能でなかったとしても、インスタンス化経路上のどこかの点で可視又は到達可能であれば使用することができます。
ただし、そのようなものはテンプレート引数に依存しているものだけです(依存名のみ)。そうでないものはこれまで通りに1度目の名前解決時に確定されます。

///S.hpp
struct S { 
  void f(); //実装略
};


///moduleA.cpp
export module A;

export template<typename T, typename U>
void f(T t, U u) { 
  t.f();
}


///moduleB.cpp
module;

//グローバルモジュールフラグメント内宣言、モジュールBからのみ可視
#include "S.hpp"

export module B;

import A;  //モジュールAのインポート(not エクスポート)

export template<typename U>
void g(U u) { 
  S s;
  f(s, u);
}


///moduleC.cpp
export module C;

import B;  //モジュールBのインポート(not エクスポート)

export template<typename U>
void h(const U &u) {
  g(u);
}

///main.cpp
import C;

int main() { 
  h(0);
}

このh(0)呼び出しに伴うインスタンス化経路は以下のようになり、h(0)呼び出しに伴うテンプレートのインスタンス化においてはこの経路上の各点で可視・到達可能な宣言がそのまま可視・到達可能とされます。

  1. void f(T t, U u)定義点(moduleA.cpp内、定義)
  2. モジュールBの末尾
    • f(s, u);(moduleB.cpp内、呼び出し)
    • void g(U u)(moduleB.cpp内、定義)
  3. モジュールCの末尾
    • g(u)(moduleC.cpp内、呼び出し)
    • void h(const U &u)(moduleC.cpp内、定義)
  4. h(0)呼び出し地点(main.cpp内、インスタンス化の起点)

この例でインスタンス化経路が効いているのは、モジュールA内のf<S, int>(S t, int u)の定義内(t.f();)です。
その点からは構造体Sの宣言及び定義は可視でも到達可能でもありませんのでそのメンバは可視ではありません。しかし、インスタンス化経路上の2番目の点、モジュールBからはグローバルモジュールフラグメントの宣言を介して可視であり到達可能です。
結果、インスタンス化経路を通してモジュールA内f<S, int>(S t, int u)の内部からもSの定義が可視となり、そのメンバも可視になります。
従って、このコードは無事にコンパイルできます。

あるいは、インスタンス化コンテキスト

インスタンス化経路はテンプレートのインスタンス化起点から構築していきます。そのグラフ上のあるテンプレートにおいてのインスタンス化経路はそのテンプレートの定義を終端として、そこから起点までを逆に辿った経路になります。
つまり、経路が分岐しているときにその分岐先の経路上からお互いの経路が参照可能になるわけではありません。

また、経路と言いつつ参照可能な宣言が決まるのはその経路上の点であるインスタンス化地点と呼ばれるあるポイントにおいてであり、重要なのはそのインスタンス化地点の方です。

なので、規格においてはインスタンス化地点に着目し、インスタンス化地点の集まりとしての インスタンス化コンテキストinstantiation context)によってインスタンス化経路は説明されています。

インスタンス化地点は基本的にはそのままの意味で、テンプレートが実体化されるソースコード上の点の事です。テンプレートの全てのテンプレートパラメータが確定するポイントの事で、テンプレートを使用した所とほぼ同じ意味です。

それに加えて、(メンバ)関数テンプレート及びクラステンプレートのメンバ関数(静的含む)は、次のどちらかをもう一つのインスタンス化地点として持ちます

  • インスタンス化地点がプライベートモジュールフラグメントの外にある場合、その翻訳単位内のプライベートモジュールフラグメントの直前の地点
    • プライベートモジュールフラグメントが無い(もしくはモジュール単位でない)場合は翻訳単位の末尾
  • インスタンス化地点がプライベートモジュールフラグメントの中にある場合、その翻訳単位の末尾

そして、インスタンス化コンテキストはそれらの地点に加えて次のように決定される地点の集まりとして定義されます

  1. あるテンプレートT1インスタンス化地点が別のテンプレートT2の中にあり、T2インスタンス化に伴ってT1インスタンス化されるときのインスタンス化コンテキストは以下の両方を含む
    • T2インスタンス化コンテキスト
    • T1がモジュールMのインターフェース単位で定義されており、Mのインターフェース単位内部でインスタンス化されていないとき、Mのプライマリインターフェース単位の末尾(プライベートモジュールフラグメントがあるならその直前)
  2. default指定されたクラスの特殊メンバ関数が暗黙に定義されるとき、そのインスタンス化コンテキストは以下の両方を含む
    • クラス定義のインスタンス化コンテキスト(通常1点)
    • 特殊メンバ関数の暗黙定義のきっかけとなった構文からのインスタンス化コンテキスト
      • クラスの特殊メンバ関数odr-usedされるときに暗黙の定義がなされる。そのodr-usedしている構文のこと(C++20以降実は正しくない)
  3. default指定されたクラスの特殊メンバ関数の定義内で暗黙的にインスタンス化されるテンプレートのインスタンス化コンテキストは、そのdefault指定された特殊メンバ関数インスタンス化コンテキストを共有する
  4. 上記に当てはまらないテンプレートのインスタンス化コンテキストは、そのテンプレートのインスタンス化地点
    • 他のテンプレートとは無関係に独立してインスタンス化した場合など
  5. それ以外のケースの場合、プログラム内のある地点でのインスタンス化コンテキストはその地点を含む

あるテンプレートのインスタンス化においては、このように決定されたインスタンス化コンテキスト内の各点で可視・到達可能な宣言がそのまま可視・到達可能となります。
そして、これをテンプレートの定義点から順番に並べたものがインスタンス化経路となります。

なお、インスタンス化コンテキスト内の各点でODRに違反してはいないが同じ宣言に対する複数の異なる定義が見つかる可能性があります。そうなった場合はill-formedなのですが、コンパイルエラーにはならない可能性があります。すなわち未定義動作の世界・・・

規格書より、単純なサンプルコード。

///X.h
struct X { 
  int n;
};

X operator+(X x1, X x2) {
  return {x1.n + x2.n};
}


///moduleF.cpp
export module F;

export template<typename T>
void f(T t) {
  t + t;  //Xに対する二項+演算子はここでは可視でも到達可能でもない
}


///moduleM.cpp
module;

#include "X.h"  //クラスXの定義とXに対する二項+演算子が可視になる

export module M;
import F;

void g(X x) {
  f(x); //ok、モジュールFのf()がインスタンス化される
        //Xのoperator+はインスタンス化コンテキストで可視である
        //この場合はここの呼びだし地点で可視
}

インスタンス化コンテキスト決定の例。

///Ohtermodule.cpp(プライマリインターフェース単位
export module Other;

export template<typename T>
auto f(T* t) -> decltype(T->n) {
  return t->n;
}

struct S;

int use_f1(S* s) {
  return f(s);  //ng
  /*
  f<S>()のインスタンス化コンテキストは以下の点
  0. f<S>()の定義点
  1. この呼び出し地点
  2. この翻訳単位内プライベートモジュールフラグメント直前
  クラスSの定義はプライベートモジュールフラグメント内にあり、どの点からも可視でも到達可能でもない
  */
}

module : private; //これをコメントアウトするとuse_f1()はコンパイルできる

int use_f2(S* s) {
  return f(s);  //ok
  /*
  f<S>()のインスタンス化コンテキストは以下の点
  0. f<S>()の定義点
  1. この呼び出し地点
  2. この翻訳単位(Ohtermodule.cpp)末尾
  クラスSの定義は2の点から可視であるので、0の定義点からも可視かつ到達可能となる
  */
}

struct S {
  int n = 10;
};


///Mymodule.cpp(プライマリインターフェース単位
export module Mymodule;
import Other;

export template<typename T, typename U>
T* in_f(T* t, U* u) {
  t->n += f(u);  //u.nをt.nに足しこむ
  /*
  ここでのf<T>()のインスタンス化コンテキストは以下の点
  0. f<T>()の定義点(Ohtermodule.cpp内)
  1. この呼び出し地点
  2. in_f<T>()のインスタンス化コンテキスト(複数点)
  3. in_fがこのファイル以外でインスタンス化されるとき、この翻訳単位(Mymodule.cpp)末尾
  型引数Tの実引数の定義は、ここのどこかで可視であれば0の定義点から可視かつ到達可能
  */
  return t;
}

//宣言のみ
struct S2;

double use_f3(S2* s) {
  return f(s);  //ng
  /*
  f<S2>()のインスタンス化コンテキストは以下の点
  0. f<S2>()の定義点(Ohtermodule.cpp内)
  1. この呼び出し地点
  2. この翻訳単位(Mymodule.cpp)末尾
  クラスS2の定義は実装単位内にあり、どの点からも可視でも到達可能でもない
  */
}

export struct C {
  unsigned short n = 80;
};


///Mymodule_impl.cpp(実装単位
module Mymodule;

double use_f4(S2* s) {
  return f(s);  //ok
  /*
  f<S2>()のインスタンス化コンテキストは以下の点
  0. f<S2>()の定義点(Ohtermodule.cpp内)
  1. この呼び出し地点
  2. この翻訳単位(Mymodule_impl.cpp)末尾
  クラスS2の定義は2の点から可視であるので、0の定義点からも可視かつ到達可能となる
  */
}

struct S2 {
  double n = 1.0;
};


///main.cpp
import Myodule;

struct S3;

S3* use_in_f(S3* s, C* c) {
  return in_f(ss, c); //ok
  /*
  in_f<S3, C>()のインスタンス化コンテキストは以下の点
  0. in_f<S3, C>()の定義点(Ohtermodule.cpp内)
  1. この呼び出し地点
  2. この翻訳単位(main.cpp)末尾
  in_f<S3, C>()定義内では1及び2の点からCの定義が、2の点からS3の定義がそれぞれ可視となる
  
  その内側のf<C>()のインスタンス化コンテキストは上記を含んだ以下となる
  0. f<C>()の定義点(Ohtermodule.cpp内)
  1. in_f<S3, C>()内の呼び出し地点
  2. in_f<S3, C>()のインスタンス化コンテキスト
    0. in_f<S3, C>()の定義点(Mymodule.cpp内)
    1. この呼び出し地点
    2. この翻訳単位(main.cpp)末尾
  3. Mymodule.cpp末尾
  このうち、2.1, 2.2, 3の各点からクラスCの同一の定義が可視となるので、f<C>()の定義点からもCの定義が可視となる
  */
}

struct S3 {
  unsigned short n = 443;
};

int main() {
  S3 s{};
  C c{};

  S3 s3 = use_in_f(&s, &c); //ok
}

テンプレートのインスタンス化時にプライベートモジュールフラグメント内部へ侵入するには、そのテンプレートのインスタンス化がプライベートモジュールフラグメント内部で行われている必要があります。
これは、プライベートモジュールフラグメントに対応する(複数ファイル時)概念である実装単位においても同様です(実装パーティションの場合はインポートしてしまうという手もあります)。

推移的にインポートされたグローバルモジュールフラグメント・ヘッダーユニット

あるインスタンス化地点において、推移的にインポートされているグローバルモジュールフラグメントおよびヘッダーユニット内の宣言が可視及び到達可能となるかどうかは未規定であり、処理系に一任されています。

///S.hpp
struct S { 
  void f(); //実装略
};


///moduleB.cpp
module;

//グローバルモジュールフラグメント内宣言、モジュールBからのみ可視
#include "S.hpp"

export module B;

void use_s(const S& s) {
  s.f();
}


///moduleC.cpp
export module C;

import B;  //モジュールBのインポート(not エクスポート)

export template<typename U>
void h(const U &u) {
  u.f();
}

///main.cpp
import C; //モジュールBは推移的にインポートされている

struct S;

void q(const S& s) {
  h(s);
}

int main() {
}

この例では、main.cppからはクラスSの定義が到達可能ではないですが、インポートしているモジュールCから推移的にインポートされているモジュールBでは定義が可視であり到達可能となります。

しかし、Sの定義はB内部のグローバルモジュールフラグメント内にあり、グローバルモジュールフラグメント内の宣言が推移的にインポートされた翻訳単位から到達可能となるかは未規定(実装依存)とされています。ただし、そのような宣言は可視でるときは到達可能となります。

この場合、Sの宣言はmain.cpp内では可視ですがモジュールC内では可視ではなく、インスタンス化コンテキスト全体としてみればSの宣言は可視となります。しかし、それによって推移的なグローバルモジュールフラグメント内宣言が到達可能となるかは規定されず実装依存となります。

もう一つ例を見てみましょう。

//stuff.cpp
export module stuff;

export template<typename T, typename U>
void foo(T, U u) { 
  auto v = u;
}

export template<typename T, typename U>
void bar(T, U u) {
  auto v = *u;
}

//m1.cpp
export module M1;
import "defn.h";        // struct X {};の宣言と定義がある
import stuff;

export template<typename T>
void f(T t) {
  X x;
  foo(t, x);
}

//m2.cpp
export module M2;
import "decl.h";        // struct X;の宣言のみがある
import stuff;

export template<typename T>
void g(T t) {
  X *x;
  bar(t, x);
}

//Translation unit #4:
import M1;
import M2;

void test() {
  f(0); //ok
  g(0); //未規定
}

f(0)の呼び出しは有効であり何の問題もありません。

g(0)の呼び出しが有効かどうかは未規定であり、それはSの定義に到達可能かどうかが未規定であることによります。

g(0)の呼び出しにまつわるインスタンス化コンテキストは以下の点が含まれています。

  1. モジュールstuffの末尾
  2. モジュールM2の末尾
  3. g(0)の呼び出し地点

このうち、Sの宣言は2番目の地点(モジュールM2の末尾)からは可視ですが、定義はどこからも可視ではありません。
ただし、3番目の地点(main.cpp内)からはM1のインポートを通じてSの定義はヘッダーユニットから推移的にインポートされており、これに到達可能となるかは未規定です。 そのため、このインスタンス化コンテキスト全体としてSの定義が到達可能となるかも未規定となります。

どちらのケースも実装はこれを到達可能としても良いことになっています。ポータブルなコードを書くのであれば、これに依存しないように気を付ける必要があるかもしれません。

ADLとモジュールとテンプレートと

モジュール時代においてもADLは使用可能でほぼ従来通りに動作します。
ただ、モジュールに対してADLは以下のように少し深めの探索を行います

  • テンプレートのインスタンス化時にはそのインスタンス化コンテキスト内宣言を探索する(依存名に対して)
    • その際、別の翻訳単位でグローバルモジュールに属す形で宣言され、内部リンケージを持つか破棄されている宣言は無視される
  • 名前付きモジュールMのインターフェース内の名前空間を探索する時、関連エンティティと同じ名前空間内にある(別の名前空間に包まれていない)Mに属する宣言はすべて可視となる
    • inline名前空間になら包まれていてもいい(宣言を囲む最も内側の非inline名前空間に関連エンティティがあればいい)
    • 関連エンティティとは、ADLの足掛かりとなった実引数(の型)のこと
///Str.hpp(ヘッダーファイル
#include <iostream>

namespace STR {
  struct Str {
    const char* str = "hello world";
  };

  static void out(const Str& str) {
    std::cout << str.str << std::endl;
  }

  void out(const Str* str) {
    std::cout << str->str << std::endl;
  }
}


///Mymodule.cpp
module;

#include "Str.hpp"  //グローバルモジュールに属する

export module Mymodule;

namespace Mymodule {

  export struct S {
    int n = 10;
  };

  export int f(S s) {
    return s.n;
  }

  inline namespace Detail {
    export template<typename T>
    int g(S s, T t) {
      return s.n + int(t);
    }
  }

  namespace Inner {
    export int h(S s) {
      return s.n + 10;
    }
  }

}

export template<typename T>
auto f(T t1, T t2) {
  return get_n(t1) + get_n(t2);
}

export using string = STR::Str; //STR::Strは参照された

export template<typneame Str>
void print_ptr(const Str& str){
  out(&str);
}

export template<typneame Str>
void print_ref(const Str& str){
  out(str);
}
//STR::out(Str*)は参照されておらず破棄された


///main.cpp
import Mymodule;

#include "Str.hpp"  //グローバルモジュールに属する

namespace Main {
  struct C {
    unsigned short n = 80;

    operator int() const {
      return int(n);
    }
  };

  unsigned short get_n(C c) {
    return c.n;
  }
}

int main() {
  Mymodule::S s{100};
  Main::C c{};

  int n = f(s);     //ok、Sの定義名前空間で見つかる
  int m = g(s, c);  //ok、Sの定義名前空間で見つかる(inline名前空間)
  int l = h(s);     //ng、Sの定義名前空間に無い(さらに内側にある)
  auto s = f(c, c); //ok、エクスポートされているテンプレートf<C>()を呼び出す
                    //f<C>()内からはインスタンス化コンテキストに対するADLによりget_n()が見つかる
  
  string str{};     //ok
  print_ptr(str);   //ng、out(Str*)は破棄されているためADLで見つからない
  print_ref(str);   //ng、out(const Str&)は内部リンケージを持つためADLで見つからない
}

ADLで可視になるとはいっても、別の翻訳単位にある場合は外部リンケージを持つものしか呼び出すことは出来ません。

規格書より、少し複雑なサンプルコード。

///moduleM.cpp(Mのインターフェース
export module M;

namespace R {
  export struct X {};

  export void f(X);
}

namespace S {
  export void f(X, X);
}


///moduleN.cpp(Nのインターフェース
export module N;
import M;

export R::X make();

namespace R {
  static int g(X);
}

export template<typename T, typename U>
void apply(T t, U u) {
  f(t, u);
  g(t);
}

///moduleQ.cpp(Qの実装単位
module Q;
import N;

namespace S {
  struct Z { 
    template<typename T>
    operator T();
  };
}

void test() {
  auto x = make();  //ok、decltype(x) = R::XはモジュールMにあり可視ではないが名前を参照していないのでok
  R::f(x);          //ng、名前空間RもR::f()も可視では無い
  f(x);             //ok、R::f()がADLによってモジュールMのインターフェースから見つかる
  f(x, S::Z());     //ng、名前空間Sは探索の対象だがモジュールM内まで及ばず、S::f()は見つからない
  apply(x, S::Z()); //ok、S::f()はインスタンス化コンテキスト内で可視
                    //R::g()は内部リンケージを持つが、apply()定義内からは呼び出し可能
}

この例から分かるように、名前空間は翻訳単位を超えて定義できます(名前空間定義そのものはグローバルモジュールに属している)がADLはそれを超えるとは限りません。ただし、3番目の例のように型が直接所属する名前空間は翻訳単位を飛び越えて探索されます。

4番目の例のように、直接の名前空間になく、その翻訳単位から見える名前空間(この場合S)内にも見当たらない場合、翻訳単位を飛び越えて可視でない名前空間定義を探索はしません。

5番目の例では、モジュールN内ではM名前空間Sが可視であり、インスタンス化コンテキスト内でQ名前空間Sが可視になります。そのため、applyインスタンス化にあたっては両方の翻訳単位の名前空間Sが探索され、S::f()を見つけることができます。

規格書より、ADLとテンプレートによるモジュール内部への侵入サンプル。

//Module interface unit of Std:
export module Std;

export template<typename Iter>
void indirect_swap(Iter lhs, Iter rhs)
{
  swap(*lhs, *rhs); // このswapは非修飾名探索では見つからない、見つかるとしたらADLでのみ
}


//Module interface unit of M:
export module M;
import Std;

//共に実装省略
struct S { /* ...*/ };
void swap(S&, S&);      // #1

void f(S* p, S* q)
{
  indirect_swap(p, q);  //インスタンス化コンテキストを探索するADLを介して、 #1が見つかる
                        //indirect_swapはインスタンス化コンテキストにこの点とモジュール内の定義点を含む
                        //ここの点においてSに対するswapは可視であるのでindirect_swap内からのADLにおいても可視
}

この様に、モジュール内部でカスタマイゼーションポイントを提供し、それを使用するにはテンプレートとADLを用いる必要があります。

おまけ、モジュール史(2004 - 2019)

参考文献

この記事のMarkdownソース

[C++]モジュール理論 基礎編

※この記事はC++20を相談しながら調べる会 #2の成果です。

※この内容はC++20より有効なものです。C++20正式策定までの間に内容が変化する可能性があります。

より読みやすい解説がすでにあるのでそちらにも目を通されるといいと思われます。→ モジュール - cpprefjp

モジュールのインターフェースと実装

あるファイルをモジュールである!と宣言するには、モジュール宣言module モジュール名;) をファイル先頭で行います。モジュール宣言を行うことでそのファイルは1つの モジュール単位、かつ1つの翻訳単位となります。

通常のC++コードがヘッダファイル(宣言)とソースファイル(定義)に分割できる様に、モジュールにおいても宣言と定義を別のファイル(別々のモジュール単位)に分けて実装する事ができます。その時、それらのモジュール単位はそれぞれ モジュールインターフェース単位モジュール実装単位 と呼ばれます。

以下、コメントでファイル名が書かれている部分は、そこから別のファイルにあるものとして書いています。

///mymodule.cpp
//(プライマリ)モジュールインターフェース単位の宣言
export module MyModule;

//export宣言、宣言をモジュール外部で使用可能にする
export int f(int n);

///mymodule_impl.cpp
//モジュール実装単位の宣言
module MyModule;
//ここではプライマリモジュールインターフェース単位内のものが見えている

//exportされたf()の再宣言・定義
int f(int n) {
  return n;
}

///main.cpp(非モジュール)
//モジュールMyModuleのインポート宣言、exportされているものを取り込む
import MyModule;

int main() {
  int n = f(10);  //ok, f()は使用可能
}

このように、export moduleの形のモジュール宣言によってインターフェース単位を宣言します。このインターフェース単位は プライマリモジュールインターフェース単位 と呼ばれ、モジュールには必ず 唯一つだけ 含まれていなければなりません。
対して、モジュール実装単位はいくつあっても構いません。

その名の通り、(モジュール)インターフェース単位にはモジュールのインターフェース、すなわち外部に公開する宣言を、実装単位にはそれら宣言の実装をそれぞれ書く、という事を想定しています。

どちらの宣言においてもmoduleの後に来るのがモジュール名で、一部の予約語stdから始まる名前を除いて好きな名前を付けることができます。

モジュールのインターフェースでは、モジュールの外側に提供したい宣言をエクスポート宣言(export 宣言;)によって外部へ公開します。
そして、モジュールを利用する側(翻訳単位)ではインポート宣言(import モジュール名;)によってモジュールからエクスポートされている宣言を取り込みます。

モジュール実装単位とインターフェース単位は同じ名前を持ちます。すると、実装単位においてインターフェース単位をインポートしようとすると自分自身をインポートすることになってしまいます。
いかなるモジュールも自分自身をインポートすることはできません。そのため、モジュール実装単位は対応するプライマリモジュールインターフェース単位を暗黙的にインポートします。
そして、これらのことからモジュール実装単位そのものはエクスポートもインポートもできない事がわかります。

なお、ヘッダーやソースファイルがそうである様に、モジュールファイルの拡張子は規定されていません。コンパイラがそれと認識すれば、拡張子は自由です。
現在の所、MSVCでは.ixx、clangでは.cppmが使われているようです(両コンパイラ共、それ以外のファイルも利用可能)。

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

そしてさらに、モジュール内部のファイルを複数のファイルに分けて実装する事ができます。
これは例えば、自作のライブラリを1つのモジュールとして提供したいが、その内部の実装においては整理のためにも複数のファイルに分けたい、という際に役立ちます。

その様にモジュール内部で分割したファイル(これもまたモジュール単位)は モジュールパーテイション と呼ばれます。
そして、モジュールパーティションもまたインターフェースと実装に分割する事ができ、分割後のモジュール単位はそれぞれ モジュールインターフェースパーティションモジュール実装パーティション と呼ばれ、それぞれモジュールインターフェース単位とモジュール実装単位でもあります。

///mymodule_part.cpp
//モジュールインターフェースパーティションの宣言
export module MyModule:InterfacePart;

export double g(double v);

///mymodule_part_impl.cpp
//モジュール実装パーティションの宣言
module MyModule:ImplPart;
import :InterfacePart;  //import宣言、モジュールパーティションMyModule:InterfacePartを取り込む

//実装パーティションでは、export宣言を行えない

double g_impl(double v) {
  return v + v;
}

//パーティション:InterfacePart内のgの再宣言・定義
double g(double v) {
  return g_impl(v);
}

///mymodule.cpp
//(プライマリー)モジュールインターフェース単位の宣言
export module MyModule;
export import :InterfacePart;  //インターフェースパーティション:InterfacePartの再エクスポート、必須

export int f(int n);

///mymodule_impl.cpp
//モジュール実装単位の宣言
module MyModule;

int f(int n) {
  return n;
}

///main.cpp
import MyModule; 

int main() {
  //共に使用可能
  int n = f(10);
  double v = g(1.0);

  //ng!呼び出し不可能
  int m = g_impl(10);
}

モジュールパーティションの宣言はモジュール宣言とほぼ同様ですが、モジュール名の後に:パーティション名を指定します。:がモジュールパーティションである証です。
通常のインターフェース単位は実質プライマリモジュールインターフェース単位の一つしか作ることができませんが、インターフェースパーティションならばいくつでも作ることができます。

ただし、同じモジュール内で複数のモジュールパーティションが同じ名前を持つことは出来ません。従って、インターフェースパーティションと実装パーティションでは異なる名前を付ける必要があります(このため、あるパーティションは使用する別のパーティションを明示的にインポートする必要があります)。

このようなモジュールのパーティションへの分割はモジュールの外からは観測することができません。したがって、モジュールパーティションはモジュール外でインポート出来ません。
そのため、インターフェースパーティション内でエクスポートした宣言は全て、プライマリーモジュールインターフェース単位から再エクスポートされなければなりません(これはしなければならないが、していなくてもエラーにはならない)。

また、モジュールパーティションをインポートするときは、(:も含めた)そのパーティション名だけを指定する必要があります。モジュール名は要らず、むしろコンパイルエラーになります。

上記の例では以下のように書いてはいけません。

///mymodule_part.cpp
//モジュールインターフェースパーティションの宣言
export module MyModule:InterfacePart;
import MyModule:ImplPart;  //compile error!

///mymodule.cpp
//(プライマリ)モジュールインターフェース単位の宣言
export module MyModule;
export import MyModule:InterfacePart;  //モジュール名を指定してはいけない

パーティションかどうかは関係なく、モジュール実装単位ではexport宣言を行うことができず、実装単位をエクスポート(export import)することもできません。実装単位にあるものはインターフェースでエクスポートしておかない限り、モジュール外部からは利用不可能です。
また、モジュールパーティションを(同モジュール内で)インポートした際は通常のインポートとは異なり、そのパーティションのすべての宣言が(エクスポートされていなくても)利用可能になります。

モジュール宣言、まとめ

ここまで4種類のモジュールが出てきましたが、モジュールはその種別にかかわらず1ファイル1モジュール単位であり、モジュール単位1つは1つの翻訳単位を成しています(すなわち、個別にコンパイルされます)。
そのような複数のモジュール単位は、そのモジュール名によって1つのモジュールとしてまとめて扱われます。

4つしかないとはいえ名前がこんがらがってとてもややこしいですが、インターフェースか実装か、パーティションか否か、という2つの軸から分割できます。そして、モジュール宣言にexportが含まれていればインターフェース、:が含まれていればパーティション、という風に見分けられます。

モジュール名をMパーティション名を:Partとして、それぞれのモジュール(宣言)は以下の様な関係にあります。

パーティション? \ インターフェース? インターフェース 実装
モジュール export module M; module M;
パーティション export module M:Part; module M:Part;

インターフェースオンリーなモジュール

ヘッダオンリーライブラリの様に、ファイル分割とかしないから1ファイルにまとめたい!という事もあるでしょう。単にパーティションを使わないだけでは、実装単位と(プライマリな)インターフェース単位の2つが必要になってしまうのでそれを達成できません。

その様な場合は、プライマリモジュールインターフェース単位のみを用いると似た様な事ができます。

///mymodule.cpp
//Mymoduleのプライマリモジュールインターフェース単位の宣言、かつMymoduleの唯一のモジュール単位
export module Mymodule;

int f_impl(int n) {
  return n * n;
}

export int f(int n) {
  return f_impl(n);
}

///main.cpp
import MyModule;

int main() {
  int n = f(10);
}

ただし、プライマリモジュールインターフェース単位に書かれた宣言と定義は、エクスポートしているかに関わらずインポートした側の翻訳単位に公開されます。 エクスポートしていないものは名前探索において見つからないだけで(そのため利用は出来ないが)インポートした翻訳単位から見えています。これは思わぬバグの原因になるかもしれません。

///main.cpp
import MyModule;

//ODR違反!モジュールの中身を知らずに定義してしまった
int f_impl(int n) {
  return n + n;
}

int main() {
  int n = f(10);
  int m = f_impl(10);  //おそらくコンパイルエラー
}

モジュール実装単位を用いる場合、実装単位内にあるものはそのモジュール外部に一切公開されず、インポートによってはそのプライマリモジュールインターフェース単位のみを取り込むことになります。そのためこの様な問題は起こらないのです。

これらのことをあとで説明する言葉を用いて表現すると、モジュール単位内のエクスポートされていないものはそれをインポートした翻訳単位において、「到達可能だが可視ではない」状態に置かれる、と言えます。

それでは、それらの問題を解決した上でモジュールを1ファイルで定義する方法は無いのでしょうか??
もちろんそれは用意されています。詳細は下の方で説明しますが、 プライベートモジュールフラグメント を用いる事で、不要なものを隠蔽しながらモジュールを1ファイルで定義する事ができます。

///mymodule.cpp
//Mymoduleのプライマリモジュールインターフェース単位の宣言、かつMymoduleの唯一のモジュール単位
export module Mymodule;

//f()の宣言のエクスポート
export int f(int n);

//プライベートモジュールフラグメントの開始
module : private;

//f_impl()の宣言・定義はモジュール外から見えない(到達可能でない)
int f_impl(int n) {
  return n * n;
}

//エクスポートされている宣言の再宣言は外部リンケージを持っている
int f(int n) {
  return f_impl(n);
}

///main.cpp
import MyModule;

//ok、以前の定義は見えてない(到達可能でない)
int f_impl(int n) {
  return n + n;
}

int main() {
  int n = f(10);      //ok
  int m = f_impl(10); //ok, m == 20
}

module : private;というマーカーによってプライベートモジュールフラグメントが開始され、それ以降の宣言・定義はモジュール外からは一切観測できません(以前にエクスポートされていない限り)。そのため、プライベートモジュールフラグメント内ではエクスポート宣言を行えません。

この様に、プライベートモジュールフラグメントを利用することで宣言と定義を適切に分離・隠蔽しつつ1ファイルでモジュールを構成することができます。

可視と到達可能(Visible and Reachable)

可視到達可能 はモジュール内の宣言や定義の参照についての2つの重要な概念です。この言葉をぬいてモジュールを説明していくのは少し難しいのでここでそれらの説明しておきます。

  • 可視(Visible)
    • ある宣言は(いずれかの)名前探索において見つかる(候補に上がる)時、そのコンテキストにおいて可視となる
  • 到達可能(Reachable)
    • ある宣言は、(名前探索とは無関係に)その宣言の持つ意味論的な性質が利用可能である時、そのコンテキストにおいて到達可能となる

可視の方は新しく導入されたのではなく同じ様な意味合いで前からあった様です。こちらは問題無いでしょう。

到達可能の方は何をいっているのかわかりづらいですが、ほぼその名前の通りの意味です。
宣言の持つ意味論的な性質とはその宣言の持つC++コードとして規定された効果のことです。
たとえば、クラスの定義の持つ効果はクラスを完全型にしてそのメンバを利用可能にします。逆にいうと、クラスの定義が到達可能であるときそのクラスは完全型となりそのメンバが利用可能になります。

定義は必ず宣言を含むので、宣言が定義を兼ねている場合はその宣言に到達可能=定義に到達可能、ということになります。

可視と到達可能という2つの概念の間には次の様な関係性があります。

  • 宣言が可視 → 宣言は到達可能:常に成り立つ
  • 宣言が到達可能 → 宣言は可視:成り立たない事がある

例えば、モジュールをインポートするとそのインターフェース(プライマリーインターフェース単位)内でエクスポートされている宣言はインポートした側で可視となり、同時に到達可能になります。
しかし、そのインターフェース内でエクスポートされていない宣言は到達可能ではありますが、可視ではありません。
このために、上のインターフェースオンリーなモジュールの項で上げたような意図しないODR違反が起こってしまう可能性があります。

///mymodule2.cpp
export module Mymodule2;

#include <iostream>

namespace Mymodule2 {

  export void print(int n);

  export void print(double v);

  void print(const char* str) {
    std::cout << str << std::endl;
  }

}

module : private;

namespace MyModule2 {

  void print(int n) {
    std::cout << n << std::endl;
  }

  void print(double v) {
    std::cout << v << std::endl;
  }
}


///mymodule_part.cpp(インターフェースパーティション)
export module MyModule:InterfacePart;
//他モジュールの再エクスポート
export import Mymodule2;

export double h(double v);

///mymodule_part_impl.cpp(実装パーティション)
module MyModule:ImplPart;
import :InterfacePart;

double h(double v) {
  MyModule2::print(v); //ok、可視であり到達可能

  return v + v;
}


///mymodule.cpp(プライマリモジュールインターフェース単位)
export module Mymodule;
export import :InterfacePart;

export struct S;

export class C;

export int f(int n);

int g() {
  return 1;
}

//定義(再宣言)、以前にエクスポートされているため暗黙にエクスポート
class C {
  int m = 10;
public:
  C() = default;

  operator int() const noexcept {
    return m;
  }

  int set(int n);
};


///mymodule_impl.cpp(実装単位)
module Mymodule;

struct S {
  int n;
  double v;
};

//定義(再宣言)、外部リンケージを持つ
int f(int n) {
  return n * n;
}

//定義(再宣言)、外部リンケージを持つ
int C::set(int n) const noexcept {
  int b = m;
  m = n;
  return b;
}

char get_a() {
  return 'a';
}


///main.cpp
import Mymodule;

int main() {
  int n = f(2);         //ok、可視であり到達可能
  double d = h(0.1)     //ok、可視であり到達可能
  MyModule2::print(n);  //ok、可視であり到達可能
  S* ps = nullptr;      //ok、型名`S`は可視であり到達可能
  C c{};                //ok、クラス`C`の定義は到達可能
  int m = c;            //ok、`C`の定義は到達可能なのでその全メンバの宣言も可視であり到達可能
  m = c.set(20);        //ok、`C`の宣言はエクスポートされており外部リンケージを持ち、そのメンバも外部リンケージを持つ
                        //そして`C`の定義は到達可能なので全メンバの宣言は可視
                        //実装単位内部の定義(再宣言)は外部リンケージを持つため、可視な宣言から呼び出しが可能

  int l = g();          //ng、到達可能だが可視ではない
  print("Hello World.");//ng、到達可能だが可視ではない

  S s = {10, 3.14};     //ng、`S`の型名は可視だが定義は到達可能ではない

  char a = get_a();     //ng、可視でも無く到達可能でもない
}

//到達可能な定義と同名のものに対して定義をしてしまうとODR違反!
int g() {
  return 1;
}

可視ではないということは名前探索で見つからないということなので、可視でなければその宣言は使用不可です。そして、その宣言の持つ期待する効果を利用するためには、その宣言が到達可能である必要があります。
すなわち、モジュール内部の宣言をモジュール外で利用するための必要十分条件は、(importを前提として)その宣言が可視かつ到達可能であることです。

ただしその定義の利用に関しては関数とそれ以外とで少し異なります。関数の場合は宣言だけが予めエクスポートされてさえいれば、再宣言で定義をしてもその宣言は外部リンケージを持つため、通常の翻訳単位越えのルールにより定義を呼び出すことができます。
それ以外のもの(例えばクラス定義)はそうではなく、名前のエクスポート宣言の後、定義が到達可能なところでなされないとモジュール外で定義を利用できません。

ODR(One-definition rule)の強化

これまではヘッダに定義されてインクルードされるもの(テンプレート、inline変数・関数やクラスの定義など)のように、その定義の文字列とプログラムとしての意味が全く同一であるとき、宣言は複数の定義を持つことが許されていました。

基本的にそれは変わりませんが、名前付きモジュールに対しては適用されません。名前付きモジュールに属するものが複数の定義を持つことはできず、前の宣言が到達可能であるときに同じ宣言を行うことは(たとえ同一であっても)許されません。

このことは次のように規定されています。

  • 名前付きモジュールに属するエンティティは複数の定義を持ってはならない。その場合、後の定義が現れるときに前の定義が到達可能でなければ診断は不要
  • ある宣言が別のモジュールに属した到達可能な宣言を再宣言する場合、プログラムはill-formd

これらの規則より、次の結論が導かれます。

  • 1つのエンティティに対する宣言・定義は同じモジュールに属しており、名前付きモジュール内では定義は唯一つでなければならない
    • グローバルモジュールにおいては依然としてODRの例外規定が有効

幸い、多くのケースではこれに違反するとコンパイラによってエラーとされるので気づく事ができます。診断不要と書いてある条件に引っかかるのは、異なるモジュールで同じ名前・シグネチャの関数などが定義されていて、それを1つのプログラム内の異なる翻訳単位から使用した、ような場合でしょう。

export

ここまでモジュール外部に宣言を公開するんだよーくらいの意味でexportを説明していましたが、その詳細を見て行きます。

モジュール内でexportによって宣言を外部公開する構文は、export宣言(エクスポート宣言)と呼びます。 export宣言は伴う宣言の宣言としての効果(名前の導入等)を持ちます。逆に言うと、宣言できるものは基本的にexportできます。

モジュールによるexport宣言によって導入・再宣言された名前はそのモジュールによって エクスポートされている と言われます。エクスポートされた名前はそのモジュールをインポートしている翻訳単位内で(の名前探索において)可視となります。
なお、クラスのメンバ名はそのクラスの定義が到達可能である場合に可視となります。

///Mymodule.cpp
export module Mymodule;  //これはエクスポート宣言ではない

//エクスポート宣言の例

export struct S {
  int n = 10;
};

export int f() {
  return 0;
}

export int g();

int g() {
  return -1;
}

export inline constexpr double PI = 3.14159;

export宣言はモジュールインターフェース単位の本文内の名前空間スコープ(グローバル名前空間を含む)で現れることができ、必ず何らかの名前を導入しなければなりません。そして、その名前は外部リンケージを持つ必要があります。

///Mymodule.cpp
export module Mymodule;

//以下全てNG集

//内部リンケージ
export static int f() {
  return 0;
}

namespace {
  //内部リンケージ
  export void f(int n) {}
}

//内部リンケージ
export constexpr double PI = 3.14159; 

//名前を宣言していない
export static_assert(true);

//名前を宣言していない
export using namespace std;

export int g() {
  //名前空間スコープではない(ローカルスコープ)
  export int n = 10;
  return n;
}

class S {
  int n = 0;
public:
  //名前空間スコープではない(クラススコープ)
  export void set(int m) { n = m; }
};


///Mymodule_impl.cpp
module Mymodule;

//モジュール実装単位にあらわれている
export int h();

export宣言を出来るのはモジュールインターフェース単位内だけです。パーティションであろうとなかろうと、実装単位ではできません。

export宣言はブロックでまとめて行うことができます。この場合のブロックはスコープを導入せず、内部にexportが現れてはいけません。

///Mymodule.cpp
export module Mymodule;

//ブロックのエクスポート宣言、内部のものは暗黙的にエクスポートされる
export {
  struct S {
    int n = 10;
  };

  int f() {
    return 0;
  }

  inline constexpr double PI = 3.14159;

  //ng! exportキーワードは現れてはならない
  export int g() {
    return -1;
  }
}

//ok、ブロックのエクスポート宣言はスコープを導入しない
export inline constexpr PI_2 = 2.0 * PI;

export宣言は当然名前空間に対しても行うことができます。その場合、エクスポートされた名前空間内の宣言は暗黙的にエクスポートされることになります。従って、内部の宣言は全て外部リンケージを持つ必要があります。
すなわち、無名名前空間export宣言に関わってはいけません。

また、エクスポートしていない名前空間内部にexport宣言が現れることもできます。この時それを囲む名前空間は暗黙的にエクスポートされますが、エクスポートしていない宣言はエクスポートされません。

///Mymodule.cpp
export module Mymodule;

//名前空間のエクスポート宣言、内部のものは暗黙的にエクスポートされる
export namespace Mymodule {
  struct S {
    int n = 10;
  };

  int f() {
    return 0;
  }

  inline constexpr double PI = 3.14159;
}

namespace Detail {
  //名前空間内でのエクスポート、Detail::g(void)がエクスポートされる
  export int g() {
    return -1;
  }

  //エクスポートしていない宣言はエクスポートされない
  int g(int n) {
    return -n;
  }
}

//ダメな例
export namespace NG {
  //名前を宣言していない
  using namespace std;

  //内部リンケージ
  static int N = 10;

  //exportキーワードは現れてはならない
  export int g() {
    return -1;
  }

  //名前を宣言していない
  static_assert(true);

  //無名名前空間は名前空間のエクスポート宣言に現れてはならない
  namespace {
    int h() {
      return 1;
    }
  }
}

//無名名前空間はエクスポートできない
export namespace {
    int h() {
      return 1;
    }
}

また、export宣言はusingtypedef)宣言に対しても行えます。ただし、上記の例でも示したように名前を導入するものでなくてはならず、参照先の名前は外部リンケージを持っていなければなりません。
ただし、usingtypedef)による型エイリアスの宣言では、参照先の名前が外部リンケージを持つ必要はありません。

///OtherModule.cpp
export module Other;

export int f() {
  return 0;
}

export int g() {
  return -1;
}

namespace Other{
  export struct S {
    int n = 10;
  };
}

///Mymodule.cpp
export module Mymodule;
import Other;

export using ::f, ::g;  //ok、まとめてエクスポート!
export using Other::S;  //ok、import先では名前空間指定なしで利用できる

//ng!名前を宣言していない
export using namespace std;

static int h1() {
  return 1;
}

double h2(double v) {
  return v;
}

struct T {
  int n = 0;
  double v = 0.0;
}

//ng!内部リンケージを持つ名前のエクスポート
export using ::h1;

//ng!モジュールリンケージ(後述)を持つ名前のエクスポート
export using ::h2;
export using ::T;

//ただし型エイリアスならok
export using C1 = ::T;  //::Tのエイリアスとなる名前C1をエクスポート
export typedef ::T C2;  //同様

また、変わったところではリンケージ指定もエクスポートできます。とはいえ、出来ることできないことはこれまでと変わりありません。

///Mymodule.cpp
export module Mymodule;

//リンケージ指定のエクスポート宣言
export extern "C++" int f() {
  return 0;
}


//リンケージ指定ブロックのエクスポート宣言、内部のものは暗黙的にエクスポートされる
export extern "C" {
  int g() {
    return -1;
  }

  double PI = 3.14159;

  //ng!exportキーワードは現れてはならない
  export int h() {
    return 1;
  }

  //ng!内部リンケージ
  static int h() {
    return 1;
  }
}

//ok、ブロックのエクスポート宣言はスコープを導入しない
export PI_2 = 2.0 * PI;

再宣言におけるexport

あらゆる宣言は再宣言を行うことができます。その際、exportがあったりなかったりすることがあるでしょう。その時、どうなるかは主に2つのパターンに分かれます、

  • 以前にexport宣言によって宣言されている場合
    • その宣言は暗黙的にエクスポートされる
  • 以前の宣言はexport宣言ではない場合
    • 再宣言はexport宣言であってはならない

どちらの場合もその再宣言された名前のリンケージは、1番最初の宣言時のリンケージに従います。すなわち、export宣言は名前のリンケージを変更できません。

///Mymodule.cpp
export module Mymodule;

//エクスポート宣言によるfの導入
export int f();

//fの定義、暗黙的にエクスポート
int f() {
  return 0;
}

export struct S;

//暗黙的にエクスポート
struct S {
  int n = 10;
};

//エクスポートしないTの宣言
struct T {
  int n = 10;
  double v = 0.0;
};

//Tの再宣言、以前にエクスポートされていないのでng!
export struct T;

namespace {
  int g() {
    return -1;
  }
}

int h() {
  return 1;
}

//内部リンケージを持つgはエクスポートできない(再宣言はリンケージを変更できない)
export int g();

//モジュールリンケージを持つhはエクスポートできない(再宣言はリンケージを変更できない)
export int h();

モジュールリンケージ

モジュールに属しかつ外部リンケージを持つ宣言(定義)がexportされていないとき、これまでの規則で考えるとその宣言を用意してやりさえすればモジュール外部から参照できるはずです。
しかし、これをやられるとモジュールの意味が薄くなってしまうので出来ないようになっています。

モジュールに属している宣言によって導入される名前が外部リンケージを持ち、エクスポートされていない場合、その名前は外部リンケージではなく モジュールリンケージ を持ちます。

モジュールリンケージを持つ名前は次の場所から参照され、参照することができます。

  • 同じモジュール単位内の他のスコープにある名前
  • 同じモジュール内の他のモジュール単位のスコープにある名前

名前というのはそのままの意味で、変数名や関数名、クラス名などのことです。

つまり、モジュール内ではexport宣言のみが外部リンケージを与え、任意の翻訳単位を超えて参照することができるのは外部リンケージを持つ名前だけです。
モジュールリンケージを持つ名前は同じモジュール内でしか参照できません。

なお、これらのことは宣言や定義が到達可能であるかどうかとは関係がありません。

import

exportときたら次はimportを見て行きます。

importに続いてモジュール名を指定することでそのモジュールでエクスポートされている宣言を取り込む構文の事をimport宣言(インポート宣言)と言います。

モジュール単位内ではimport宣言は書ける場所が決まっていて、基本的にはそのモジュール単位の先頭(すなわちファイルの先頭)で、他のあらゆる宣言よりも前に来なければなりません。ただし、モジュール宣言よりは後ろになります。
非モジュールの翻訳単位ではそのような規定はなく、importはどこにでも書くことができます。ただし、あらゆる宣言の内部に書くことはできません。

//予め、モジュールA,B,Cがあるとして

///Mymodule.cpp
export module Mymodule;
import A;         //ok
export import B;  //ok

export int f();


///main.cpp
import C; //ok

struct S{};

import A; //ok

int main() {
}

import B; //ok


///ng1.cpp(ダメな例1、モジュール単位
import A; //ng、モジュール宣言よりも後にくる必要がある
export module NG;
import B; //ok

export int f();

import C; //ng、そのほかの全ての宣言よりも前に無ければならない


///ng2.cpp(ダメな例2、非モジュール
namespace N {
  import A; //ng、他の宣言内はだめ
}

struct S {
  import B; //ng、他の宣言内はだめ
};

int main() {
  import C; //ng、他の宣言内はだめ
}

import宣言がインポートするものは、エクスポートされている宣言の塊とかの抽象的なものではなくて、いくつかの翻訳単位そのものをインポートします。
インポートされる翻訳単位は次のものの集まりです

  • 指定されたモジュールのプライマリーモジュールインターフェース単位
    • (下の条件より)必然的にモジュールの全てのインターフェース単位
  • インポートされる翻訳単位内で再エクスポート(export import、後述)されている翻訳単位
    • そのような翻訳単位は再エクスポートしている翻訳単位からエクスポートされる
  • 指定されたのがモジュールパーティションなら、そのパーティション
  • ヘッダーユニット(後述)
  • 同じモジュールの他のモジュール単位をインポートしている場合、そこでインポートされているすべての翻訳単位(exportの有無によらず)
    • 同じモジュール内ではインポートした翻訳単位内のすべての宣言が可視となる

そして、そのようにインポートされた翻訳単位内でエクスポートされている宣言(名前)は、インポート先の翻訳単位で可視となり、エクスポートされていない宣言は到達可能となります。インポート宣言の持つ効果は実質これだけです。
import宣言は#includeとは異なり宣言や定義をインポート先に導入しません。インポートされると言う事は、単に宣言が可視もしくは到達可能となるかどうかだけに影響します。

あるインターフェース単位内で単にインポートされただけの(再エクスポートされていない)モジュール内の宣言(必然的にそのインターフェース内宣言)は、そのインターフェース単位をインポートした先で可視にはなりませんが到達可能となります。 これを「推移的なインポート」と呼び言い直すと、ある翻訳単位で推移的にインポートされた翻訳単位内の宣言は可視ではないが到達可能となる、という事になります。

//予め、モジュールA,B,C,Dがあるとして

///Mymodule_part.cpp(インターフェースパーティション
export module Mymodule:Part;
export import A;  //再エクスポート
import B;

///Mymodule_implpart.cpp(実装パーティション
module Mymodule:impl;
import :Part;/*
ここでインポートされている翻訳単位は3つ
1. (インターフェース)パーティション:Part(Mymodule_part.cpp)
2. モジュールA(パーティション:Partからの再エクスポート)
3. モジュールB(パーティション:Partでインポートされている、推移的なインポートではない)
*/

///Mymodule.cpp(プライマリなインターフェース単位
export module Mymodule;
export import :Part;
import C; 
export import D;/*
ここでインポートされている翻訳単位は5つ
1. (インターフェース)パーティション:Part(Mymodule_part.cpp)
2. モジュールA(パーティション:Partからの再エクスポート)
3. モジュールB(パーティション:Partでインポートされている)
4. モジュールC
5. モジュールD
*/

///Mymodule_impl.cpp(実装単位
module Mymodule;/*
ここでインポートされている翻訳単位は6つ
1. プライマリモジュールインターフェース単位(Mymodule.cpp)
2. (インターフェース)パーティション:Part(Mymodule_part.cpp)
3. モジュールA(パーティション:Partからの再エクスポート)
4. モジュールB(パーティション:Partでインポートされている)
5. モジュールC(これは推移的なインポートではない)
6. モジュールD
*/
//実装略

///main.cpp
import Mymodule;/*
ここでインポートされている翻訳単位は4つ
1. Mymoduleのプライマリーモジュールインターフェース単位(Mymodule.cpp)
2. Mymoduleのインターフェースパーティション:Part(Mymodule_part.cpp)
3. モジュールA(Mymodule:Partからの再エクスポートのMymoduleからの再エクスポート)
4. モジュールD(Mymoduleからの再エクスポート)
エクスポートされている宣言が可視となっているのはこの4つのモジュールのみ
加えて、推移的にインポートされたモジュールB,C内部の宣言が到達可能となる(可視ではない)
*/

int main() {
}

同モジュール内ではインポートするとそこでインポートされている翻訳単位をすべてインポートします。そのため、予想外にインポートする翻訳単位が膨れ上がることがあり得ます。上記だとモジュール実装単位(Mymodule_impl.cpp)がそうであるように、実装単位は特に沢山の翻訳単位をインポートすることになりがちです。

なお、パーティションでないモジュール実装単位では自分自身のモジュールを指定するインポート宣言を行えません。
その代わり、そのようなモジュール実装単位は対応するプライマリモジュールインターフェース単位を暗黙的にインポートしています。

///Mymodule.cpp
//Mymoduleのプライマリインターフェース単位
export module Mymodule;

export int f();


///Mymodule_impl.cpp
//Mymoduleの実装単位
module Mymodule;
import Mymodule;  //compile error!
                  //代わりにこの様に書いたように暗黙的にプライマリモジュールインターフェース単位をインポートしている

//エクスポートされているfの再宣言であり定義
//エクスポートされいるわけではないが外部リンケージを持つので呼び出し可能
int f() {
  return 0;
}

再エクスポート

ここまでにもちらちら出て来ていますが、インポート宣言はエクスポートすることができ、それを再エクスポートと呼びます。再エクスポート宣言は、import宣言の前にexportを付けます。
そのように再エクスポートされた翻訳単位は、import宣言の効果を持ちつつ、再エクスポートしている翻訳単位から指定した翻訳単位をエクスポートします。
ただし、モジュール実装単位(パーティション含む)はエクスポートできません。

//予め、モジュールA,Bがあるとして

///Mymodule_part.cpp
export module Mymodule:Part;
export import A;  //ok、モジュールの再エクスポート

///Mymodule_implpart.cpp
module Mymodule:impl;

///Mymodule.cpp
export module Mymodule;
export import :Part;  //ok、インターフェースパーティションの再エクスポート
export import B;      //ok
export import :impl;  //ng!実装単位(パーティション)はエクスポートできない

再インポート(複数回のインポート)

インポート宣言は同じモジュール名に対して複数回行うことができ、それは特に禁止されていません。

//予め、モジュールA,Bがあるとして
///main.cpp
import A;
import B;

import A; //ok、#includeとは違い問題は起きない

int main() {
}

同じモジュールを何回インポートしても意味はなく、特に問題も起きません。
なぜなら、先に述べたようにインポート宣言は指定したモジュール内の宣言が可視・到達可能となるかどうかのみを変更します。
再度のインポートをしたとしても、すでに可視な宣言が可視に、到達可能な宣言が到達可能になるだけです。ODR違反等を起こしません。

インターフェース依存関係

ある翻訳単位は次のいずれかの場合にあるモジュール単位Uにインターフェース依存関係を持ちます。

  • Uを指定するモジュールインポート宣言がある時
  • Uと同名のモジュール実装単位である時(パーティションではなく)
    • 必然的にUはプライマリインターフェース単位
  • Uにインターフェース依存関係を持つモジュール単位にインターフェース依存関係を持つ時
    • 推移的な依存関係のこと

基本的には1つ目の条件によりインターフェース依存関係が発生し、それは再帰的に推移します(3つ目の条件)。

そして、あるモジュール単位は自分自身にインターフェース依存関係を持ってはいけません。

//M1のインターフェース単位
export module M1;
import M2;

//M2のインターフェース単位
export module M2;
import M3;

//M3のインターフェース単位
export module M3;
import M1;  //compile error! 循環的なインターフェース依存関係の発生 M3→M1→M2→M3

前述のように、同モジュール内でのモジュール単位のインポートは再エクスポートしていなくてもそのモジュールのインポートが発生するため、予想外のインターフェース依存関係が発生することがあります。その時、この様な循環的なインターフェース依存が発生しない様に注意しなければなりません。
幸いな事に、これはコンパイラによって検出されコンパイルエラーとなるはずです。

また、パーティションではない実装単位は多くのインターフェース依存関係を持ち得ますが、自身をエクスポート/インポートできないために循環的な依存関係は発生しないでしょう。
パーティションではないモジュール実装単位はインターフェース依存関係を断ち切るのに有効利用できます。

変り者のモジュール達

ここまではほぼごく普通のモジュールだけを紹介してきましたが、世の中には少し変なモジュールが存在しています・・・

プライベートモジュールフラグメント

これは上の方でも出てきましたが、プライベートモジュールフラグメントはモジュールを単一ファイルで構成するための仕組みです。

モジュールをインポートするとインポートされるのはそのモジュールのプライマリモジュールインターフェース単位です。すなわち、プライマリモジュールインターフェース単位さえあればモジュールは構成できるわけです。

しかし、そのようなインターフェース単位に書いたエクスポートされていない宣言・定義はインポート先の翻訳単位で到達可能となり、意図しないODR違反を起こす可能性があります。

サンプルコード再掲

///mymodule .cpp
//Mymoduleのプライマリインターフェース単位の宣言、かつMymoduleの唯一のモジュール単位
export module Mymodule;

//f_implの宣言はインポート先で到達可能だが可視ではない
int f_impl(int n) {
  return n * n;
}

//fの宣言はインポート先で可視
export int f(int n) {
  return f_impl(n);
}

///main.cpp
import MyModule;

//ODR違反!モジュールの中身を知らずに定義してしまった
int f_impl(int n) {
  return n + n;
}

int main() {
  int n = f(10);
  int m = f_impl(10);  //おそらくコンパイルエラー
}

そのため、実装や公開する必要のない宣言を適切に隠蔽する仕組みが必要となり、プライベートモジュールフラグメントはそのための仕組みです。

プライベートモジュールフラグメントはプライマリモジュールインターフェース単位でのみ使用可能で、module : privete;というマーカーから開始されます。
プライベートモジュールフラグメントの内部の宣言は他の翻訳単位から到達可能ではなく、したがって可視でもありません。(プライベートモジュールフラグメントの宣言は同じプライベートモジュールフラグメント内からか、テンプレートのインスタンス化時に一定条件の下で到達可能になります。)

そして、プライベートモジュールフラグメントを持つ翻訳単位は、そのモジュールで唯一の翻訳単位となっていなければなりません。単一ファイルでモジュールを構成する仕組みですが、利用することによって単一ファイルであることを強いることにもなります。
ちなみに、この時に複数ファイルでモジュールを構成したとしても診断はされません(つまりエラーになりませんが、規格違反状態です)。

///mymodule.cpp
//Mymoduleのプライマリインターフェース単位の宣言、かつMymoduleの唯一のモジュール単位
export module Mymodule;

//f()の宣言のエクスポート
export int f(int n);

//プライベートモジュールフラグメントの開始
module : private;

//f_impl()の宣言・定義はモジュール外から到達可能でない
//モジュールリンケージを持つため参照もできない
int f_impl(int n) {
  return n * n;
}

//エクスポートされている宣言の再宣言は外部リンケージを持つため呼び出し可能
int f(int n) {
  return f_impl(n);
}

///main.cpp
import MyModule;

//ok、以前の定義は見えてない(到達可能でない)
int f_impl(int n) {
  return n + n;
}

int main() {
  int n = f(10);      //ok
  int m = f_impl(10); //ok, m == 20
}

その性質上明らかですが、プライベートモジュールフラグメント内部でexport宣言を行うことは出来ません。

しかし、import宣言を行うことは出来ます。その場合は、プライベートモジュールフラグメントの開始宣言の直後で、他のあらゆる宣言よりも前に行います。
プライベートモジュールフラグメント内部でインポートした翻訳単位内のあらゆる宣言は、モジュール外部から可視でも到達可能でもありません。

//予め、モジュールA,B,Cがあるとして

///mymodule.cpp
export module Mymodule;
export import A;
import B;/*
ここではモジュールA,Bの宣言が可視
*/

module : private;
import C;/*
ここではモジュールA,B,Cの宣言が可視
*/

///main.cpp
import MyModule;/*
ここでインポートされている翻訳単位は2つ
1. Mymoduleのプライマリーモジュールインターフェース単位(Mymodule.cpp)
2. モジュールA(Mymoduleからの再エクスポート)
エクスポートされている宣言が可視となっているのはこの2つのモジュールのみ
推移的インポートによって宣言が到達可能になるのはモジュールBのみ
モジュールCは完全に観測不可能
*/

int main() {
}

プライベートモジュールフラグメントを利用する事で、推移的なインポートによる到達可能な宣言の漏出を防止する事ができます。

プライベートモジュールフラグメントは、module : privete;という行を境目としてプライマリインターフェース単位と実装単位を1ファイル内に書いている、と見ることもできます。

グローバルモジュール

グローバルモジュールは通常の名前付きモジュール(モジュール宣言によって導入されるモジュール)ではない全てのコードとグローバルモジュールフラグメント(後述)が属しているモジュールです。
モジュール宣言の無い翻訳単位に書かれている宣言は全てグローバルモジュールに属することになります。

丁度、名前空間に包まれていないものがグローバル名前空間内にあるように、モジュール内部に無いものはグローバルモジュールに属する形になります。

そのようなグローバルモジュールには名前はなく、インターフェース単位も持たず、導入するためのモジュール宣言もありません。
すなわち、明示的にグローバルモジュールを定義する構文はなく、インポートすることもできません。

グローバルモジュールでは従来のヘッダ利用のためにモジュール内部よりもODRが緩くなっていて(というか従来通り)、定義の文字列と意味が同一ならば複数の定義が存在する事が許可されています。

///Mymodule.cpp
export module Mymodule;
//以下の宣言はすべてMymoduleに属する

///main.cpp
import Mumodule;
//以下の宣言はグローバルモジュールに属する

int main() {
}

グローバルな確保・解放関数(new/delete)はグローバルモジュールに属しており、main関数は必ずグローバルモジュールに属していなければなりません。
また、ここまでに出てきたものの中でも次の2つは実はグローバルモジュールに属しています。

  • 外部リンケージを持つ名前空間の定義
  • リンケージ指定内部に現れる宣言
///Mymodule.cpp
export module Mymodule;

//名前空間Mymoduleはグローバルモジュールに属する
namespace Mymodule {
  //Mymodule::f()はモジュール"Mymodule"に属する
  export int f() {
    return 1;
  }

  //Mymodule::PIはモジュール"Mymodule"に属する
  inline constexpr double PI = 3.14159;
}

//g()はグローバルモジュールに属する
export extern "C++" int g() {
  return 0;
}

//内部のものはグローバルモジュールに属する
export extern "C" {
  int h() {
    return -1;
  }

  double PI = 3.14159;
}

グローバルモジュールは複数のファイルに渡ってその本文を持ち得る巨大な1つのモジュールです。そして、モジュールに対して適用される規則はグローバルモジュールにも適用されます。
この記事中では(規格書中でも)、グローバルでない通常のモジュールだけを指定する必要がある時は「名前付きモジュール」と言う言葉を使いグローバルモジュールと区別します。

グローバルモジュールフラグメント

グローバルモジュールフラグメントは、モジュール内部でのヘッダーファイルの#includeをモジュール内で完結させるための宣言領域です。 グローバルモジュールフラグメントの宣言は全てグローバルモジュールに属します。

モジュールの時代が到来したとはいえ、まだまだ世の中のライブラリはヘッダベースのものがほとんどです。それは標準ライブラリも例外ではありません。そのため、モジュール内部においてもそれらのヘッダをインクルードして利用する必要があります。

#includeはコピペのため、インクルード先にヘッダの内容が展開されます。モジュール内部においてそうしたインクルードを行うとモジュールおよびそのインターフェースの肥大化(使用しないものもコンパイルされ、残る)を招くとともに、インターフェースでインクルードするとそのモジュールをインポートした先でヘッダ内の宣言が到達可能となってしまい、意図しないODR違反を引きおこす可能性が著しく増加してしまいます。

以下のコードは後述のヘッダーユニットは無いものとした例です。

///Mymodule.cpp
export module Mymodule;

#include <iostream> //ここにiostreamヘッダが展開され、それらはモジュールリンケージ(もしくは内部、なし)をもち、Mymoduleに属する

///main.cpp
import Mumodule;
//ここではiostreamヘッダの内容は可視ではないが到達可能

#include <iostream> //この時点で数多くのODR違反が発生する

int main() {
  std::cout << "Hello Compile Error!" << std::endl;  //おそらくコンパイルエラー
}

この例のようにモジュールとその利用側で同じヘッダをインクルードすると、ODR違反が大量に起こるでしょう。幸いほとんどはコンパイルエラーとされるはずです。
この時、内部リンケージを持つものは翻訳単位毎に生成されているため、それらに依存する処理は思わぬ動作をする可能性があります。運良く何も起こらなくても未定義動作の世界です・・・
この様な状況を回避し旧来のヘッダを利用するための仕組みが、グローバルモジュールフラグメントになります。

その名の通りプライベートモジュールフラグメントと似たもので、モジュール単位内部にグローバルモジュールフラグメントという領域を作ります。ファイル先頭でモジュール宣言の前にmodule;という宣言をすることでグローバルモジュールフラグメントが開始されます。

グローバルモジュールフラグメント内の宣言は全てグローバルモジュールに属し、そのモジュールには属しません。

///Mymodule.cpp
module; //グローバルモジュールフラグメント(開始)宣言

#include <iostream> //ここにiostreamヘッダが展開される

export module Mymodule; //通常のモジュール宣言によってグローバルモジュールフラグメントは終了
//ここでは、iostreamヘッダの内容が使用可能(可視になる)


///main.cpp
import Mumodule;
//ここではiostreamヘッダの内容は可視ではない

#include <iostream> //無問題

int main() {
  std::cout << "Hello World!" << std::endl;  //何の問題もない
}

グローバルモジュールフラグメントにはコンパイルの開始段階でプリプロセッサ以外のものが含まれてはいけません。また、グローバルモジュールフラグメントの終了を意味するモジュール宣言はプリプロセッサによって生成されたものであってはなりません。

///Mymodule.cpp
module; //グローバルモジュールフラグメント(開始)宣言

#include <iostream> //ok

//通常の宣言はng(ヘッダファイルのインクルードを経由するかマクロで生成すればok)
int f() {
  return 0;
}

#define END_EXP export  //ok
#define END_MOD module  //ok

END_EXP END_MOD Mymodule; //ng、モジュール宣言を生成してはならない

そして、グローバルモジュールフラグメント内の宣言のうち、その後のモジュール内から参照されないものは 破棄 されます。モジュール内から参照され、さらにそこから参照されている宣言は破棄されませんが、単にグローバルモジュールフラグメント内から参照されているだけでは破棄されてしまいます。
破棄された宣言はモジュール外部から可視でも到達可能でもなく、おそらくコンパイルされません。

宣言の破棄によってヘッダファイルを利用しながらモジュールの肥大化を抑えることができます。

///Mymodule.cpp
module;

#include <tuple>

export module Mymodule;

export using int_tuple = std::tuple<int, int, int>;

export auto f() -> std::tuple<char, short, double>;

//これ以外のstd::tuple特殊化および、すべての関数の宣言は使用されていないので破棄される
//破棄された宣言はモジュールに含まれず、コンパイルもされない
//そして、このモジュールをimportした先で到達可能ではない

///main.cpp
import Mymodule;

#include <tuple>  //無論何ら問題なし

int main() {
  int_tuple t{1, 2, 3}; //ok
}

なお、テンプレートのテンプレート引数に関わる形でグローバルモジュールフラグメント内の宣言が使用されている場合、その宣言は参照されているとみなされず破棄されることがあります。

///Mymodule.cpp
module;

#include <tuple>

export module Mymodule;

export template<typename T>
using triple = std::tuple<T, T, T>; //テンプレートパラメータが確定していないのでstd::tupleは参照されているとみなされない

export template<typename T>
triple<T> f(T t) {
  return {t, t, t}; //テンプレートパラメータが確定していないのでstd::tupleは参照されているとみなされない
}

triple<double> d_triple = f(3.14);  //ok、std::tuple<double, double, double>が参照された

///main.cpp
import Mymodule;

int main() {
  triple<int> t1{};       //compile error! std::tuple<int, int, int>は破棄されている
  triple<int> t2 = f(1);  //compile error! std::tuple<int, int, int>は破棄されている

  triple<double> t3{};          //ok、std::tuple<double, double, double>は破棄されていない
  triple<double> t4 = f(2.72);  //ok、std::tuple<double, double, double>は破棄されていない
}

テンプレートパラメータが確定するまでは、そのテンプレートは参照されているとみなされません。これは引っ掛かりやすい罠なので注意する必要があります。

ヘッダーユニット(ヘッダ単位)

ヘッダーユニットは従来のヘッダファイルをモジュールとしてインポートする仕組みです。ヘッダーユニットはそれが一つのモジュール単位であるかのようにふるまい、基本的には1つのモジュールとして扱われます。

ヘッダーユニット内の全ての宣言は抽出されインターフェースとしてエクスポートされ(いわば、プライマリなインターフェース単位を自動生成する)、それらの宣言はグローバルモジュールに属します。
その際、外部リンケージを持たないものがあっても大丈夫ですが、そう言うものはヘッダーユニット外から参照してはいけません。
つまりは結局、ヘッダユニットからエクスポートされるのは外部リンケージを持つものだけという事です。

ヘッダーユニットを利用するには、import宣言にヘッダ名を指定してやります。また、インポート可能なヘッダであるとコンパイラに認識された場合、そのヘッダに対する#includeはヘッダーユニットのインポート宣言に置き換えられる可能性があります(これは実装に任されている)。

以下サンプルコードはSTLのヘッダがインポート可能な世界です

///Mymodule.cpp
export module Mymodule;

import <tuple>;           //tupleヘッダーユニットのインポート
#include <type_traits>    //type_traitsヘッダーユニットのインポートに置き換えられる
export import <iostream>; //iostreamヘッダをインポートしつつ再エクスポート

//ここでは、tuple、type_traits、iostreamヘッダの内容が使用可能(可視になる)


///main.cpp
import Mymodule;
//ここではiostreamヘッダの内容が可視
//tuple、type_traitsヘッダの内容は到達可能でもない

//以前の定義は到達可能ではないのでok
#include <tuple>
#include <type_traits>

#include <iostream> //同じ名前を指定するヘッダーユニットのインポートは、同じヘッダーユニットをインポートする
                    //翻訳単位のインポートは宣言を導入(コピペ)せず、可視・到達可能にするだけ
                    //既に可視になってる宣言を再び可視にする効果しかなく、再度のインポートに問題はない

int main() {
  std::cout << "Hello World!" << std::endl;  //ok、Mymoduleの再エクスポートを通して可視
}

同じ名前を指定するヘッダーユニットのインポート宣言は、常に同じヘッダーユニットをインポートします。つまり、インポートする翻訳単位が異なっていたとしてもヘッダーユニットは1つ(1度)しか生成されません。
そして、通常のインポート宣言と同様、同じヘッダーユニットの複数回のインポートをしてもODR違反等の問題は起きません。

この様に、ヘッダのインクルードの問題を解決できるので、利用可能ならばグローバルモジュールフラグメントよりもヘッダーユニットを使った方が良いでしょう。

ちなみに、ヘッダーユニットはマクロをインポートする唯一の方法です。インポートされたマクロはヘッダーユニットのインポート宣言の直後で定義されています。
ただし、ヘッダーユニットの再エクスポート時はマクロはエクスポートされません。

///Mymodule.cpp
export module Mymodule;

//cmathヘッダをインポートしつつ再エクスポート、この直後cmathヘッダ内のマクロが定義される
export import <cmath>;/*
ここで、<cmath>内のマクロが定義(エクスポート)される
マクロの内容によっては以降のimport宣言に指定した名前に影響するかもしれない
*/import <tuple>;

//共にok、cmathヘッダ定義のマクロが利用できる
double huge_value = HUGE_VAL;
double nan = FP_NAN;

///main.cpp
import Mumodule;
//ここではcmathヘッダの内容が可視

int main() {
  double r2 = std::sqrt(2.0); //ok
  
  //共にng、マクロは再エクスポートされない
  double huge_value = HUGE_VAL;
  double nan = FP_NAN;
}

import <cmath>;  //再インポート、マクロが定義される

void m() {
  //共にok、<cmath>のマクロが使用可能
  double huge_value = HUGE_VAL;
  double nan = FP_NAN;
}

詳細には、ヘッダーユニットはヘッダーを一つのソースファイルとして翻訳フェーズ7(テンプレートの実体化直前)までコンパイルした1つの翻訳単位です。
マクロは、そのコンパイル時の翻訳フェーズ4(プリプロセッサの実行)終了時の段階でヘッダーユニット内に定義されているマクロがエクスポート(ヘッダーユニットインポート宣言の直後で再定義)されます。
外部から見るとヘッダーユニット内では、プリプロセスは終了していますがテンプレートは実体化されていません。そして、すべての宣言は抽出されエクスポートされているので、外部リンケージを持つもの(非テンプレート)は翻訳単位超えにより呼び出す事ができます。そして、テンプレートは最終的なコンパイル時に実体化され定義されます。

翻訳フェーズについて → 翻訳フェーズ - cppreference.com

マクロがエクスポートされインポート先で再定義される、1つのモジュールとしてコンパイルされる、という性質から明らかですが、ヘッダユニットのインポートは全てプリプロセス時(翻訳フェーズ4)に処理されます(なので厳密にはヘッダーユニットのインポートはインポート宣言ではないのです)。

また、インポート可能なヘッダは処理系定義とされているので何でもかんでもインポート出来るわけでは無いようですが、それらヘッダをどう識別するか、もしくは特定の名前のヘッダのみをインポート可能とするか、に関しても処理系定義であり、意図としてはインポート可能なヘッダの扱いについての研究が進められることを狙っているようです。
ただし、STLのヘッダのうちC言語由来でないヘッダは全てインポート可能ヘッダとなります。C言語由来なヘッダとは<cmath><cstdlib>などのcから始まるヘッダのことです。

グローバルモジュールフラグメント及びヘッダーユニットの推移的なインポート

あるモジュールインターフェース内でエクスポートせずにインポートしているものは、そのモジュールインターフェースをインポートする先に推移的にインポートされます。推移的にインポートされた宣言は可視にはなりません。
ただし、それが名前付きモジュールならそのインターフェース内宣言は到達可能になります。

そして、そのインターフェース内のグローバルモジュールフラグメントに破棄されず残った宣言、及び推移的にインポートされたものがヘッダーユニットである場合、それらの宣言が到達可能となるかは未規定であり実装は到達可能とする事が許されています。
ただ、それら宣言は可視である時には到達可能になります(これは規定されている)。

仮に到達可能とされた場合、ヘッダーユニットは必ず再インポートになりなんら問題は起きません。グローバルモジュールフラグメント内の宣言はODRの例外規定に沿っていればグローバルモジュールに属しており、定義の文字列と意味が同一ならば複数の定義を持つ事ができるためODR違反は起こりません。。

///header.hpp(ヘッダファイル
inline bool ok() {
  return true;
}

//非テンプレートの非inline関数
bool ng() {
  return false;
}

const double e = 2.72;

///Mymodule.cpp
module;

#include "header.hpp"

export module Mymodule;
import <cmath>;

//header.hpp内宣言を参照する
void use() {
  bool b1 = ok();
  bool b2 = ng();
  auto e2 = 2.0 * e;
}

///main.cpp
import Mymodule;
//<cmath>及び"header.hpp"内宣言が到達可能となるかは未規定、可視ではない
//以下、到達可能とすると

#include "header.hpp" //関数ng()がODR違反を起こす

const double r2 = std::sqrt(2.0); //ng、可視でない

import <cmath>  //ok、ヘッダーユニットの再インポート

int main(){
  bool b1 = ok(); //ok、可視であり到達可能
  bool b2 = ng(); //ng、おそらくリンクエラー
  double r5 = std::sqrt(5.0); //ok、可視であり到達可能
}

テンプレートやクラスのメンバでなければ、関数や変数はinlineでないとODRの例外には当てはまりません。

上級編

onihusube.hatenablog.com

参考文献

この記事のMarkdownソース

[C++]static constexprな配列メンバの定義

クラスの静的メンバ変数は通常クラス外にその定義が必要になります。

struct sample {
  //宣言
  static int n;
};

//定義
int sample::n = 10;

ただし、静的メンバ変数がstatic constexprな変数であるときは、多くの場合その定義は省略することができます(static constでもほぼ同様)。

struct sample {
  //宣言
  static constexpr int n = 10;
};

[Wandbox]三へ( へ՞ਊ ՞)へ ハッハッ

しかし、static constexprな配列メンバは同様の場合においても定義が必要とされます(ただし、C++14まで)。

struct sample {
  //宣言?
  static constexpr int m[] = {10, 20};
};

//定義、これが無いとリンカエラー(C++17以降は逆に非推奨)
constexpr int sample::m[];

[Wandbox]三へ( へ՞ਊ ՞)へ ハッハッ

これには変数がいつodr-usedされるのか、という難解な決まりごとが関わっています。

変数のodr-used

変数や関数などはodr-usedされたときにその定義が必要となります。逆に言えば、odr-usedされなければ定義は必要ではありません。

変数のodr-usedは以下のように規定されています(N4659 6.2.3 One-definition rule [basic.def.odr]より、C++17とC++14ではこの部分の内容に変更は無いのでC++17規格を参照します)。

A variable x whose name appears as a potentially-evaluated expression ex is odr-used by ex unless applying the lvalue-to-rvalue conversion to x yields a constant expression that does not invoke any non-trivial functions and, if x is an object, ex is an element of the set of potential results of an expression e, where either the lvalue-to-rvalue conversion is applied to e, or e is a discarded-value expression.

細かく説明するのは困難なので簡単に要約すると、ある変数xは以下の場合を除いてodr-usedされます。

xが評価されうる式exに現われていて

  • xにlvalue-rvalue変換を適用すると、非トリビアルな関数を呼び出さない定数式、で行うことができる
  • xは参照である
  • xはオブジェクトであり、かつ
    • exはlvalue-rvalue変換が適用されない結果が廃棄される式(discarded-value expression)の(想定される)結果の一つであるか
    • exはlvalue-rvalue変換が適用可能な、より大きな式の(想定される)結果の一つ

まあ良く分からないですね・・・。
今回重要なのはなんとなくわかるかもしれない1つ目の条文

xにlvalue-rvalue変換を適用すると、非トリビアルな関数を呼び出さない定数式、で行うことができる

という所です。

lvalue-rvalue変換とはその名の通り、変数を左辺値から右辺値へ変換するものです。そして、それが非トリビアルな関数(ユーザ定義された関数)を呼び出さず、定数式で行える時、変数はodr-usedされません。

実は、非配列のstatic constexprな変数はほとんどの場合この規則に当てはまっており、使っている(つもり)のところではその定数値が直接埋め込まれる形に変換されているわけです。

struct sample {
  //宣言
  static constexpr int n = 10;
};

int main() {
  //このsample::nの使用は、lvalue-rvalue変換によって
  int n = sample::n;
  //以下のように書いたかのように扱われている
  int n = 10;
}

常にこのようにすることができる場所ではその定義は必要とならないので、odr-usedである必要がない事が分かるでしょう。

ところで、lvalue-rvalue変換とはなんぞやと詳しく見に行ってみると、1番最初に以下のように書かれています(N4659 7.1.1 Lvalue-to-rvalue conversion [conv.lval])。

A glvalue of a non-function, non-array type T can be converted to a prvalue.

要約すると

関数型でも配列型でもない型Tのglvalueは、同じ型のprvalueに変換できる。

おわかりいただけたでしょうか、static constexprな配列メンバを使用すると定義を必要とされるのはこの条文によります。
つまり、配列型の変数にはlvalue-rvalue変換を適用することができない(そして、odr-usedとならない他の条件にも当てはまらない)ため、どのように使ったとしてもodr-usedになってしまうのです。

結果、クラス外に定義が必要になってしまいます。

そして、例えばコンパイルをとりあえず通すためにヘッダにそのような定義を書き、長い時間が過ぎた後でそのヘッダを複数のソースファイル(翻訳単位)からインクルードしたとき、今度は定義が重複している、ODR違反だ!というエラーに悩まされることでしょう・・・

C++17以降の世界

C++17以降は、冒頭のstatic constexprな配列メンバにおいても定義をすることなく使用することができるようになります。

[Wandbox]三へ( へ՞ਊ ՞)へ ハッハッ

これは、odr-usedの条件が変更されたわけではなく、C++17より導入されたinline変数の効果によるものです。

詳しくは → インライン変数 - cpprefjp C++日本語リファレンス

クラスの静的メンバ変数にconstexprがついているとき、暗黙的にinline指定したのと同じになり、定義がその場で生成されます(static constexprなメンバ変数については、むしろクラス外に定義を書くのは非推奨となります)。
しかも、inline変数なのでヘッダに書いて複数ファイルからインクルードしてもその定義は一つに保たれるためにODR違反に悩まされることもありません。

つまりはC++17以降を使いましょう、という事ですね・・・。

参考文献

謝辞

この記事は以下の方によるご指摘によって成り立っています。

この記事のMarkdownソース