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