[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ソース