[C++]TU-local Entityをexposureするのこと(禁止)

モジュールと内部リンケージ名

内部リンケージをもつエンティティ(関数、クラスや変数)は、外部リンケージをもつ関数など翻訳単位のインターフェースの実装詳細をある粒度で分解して表現するために使用されています。それによって、コードの保守性や再利用性の向上が見込めます。

従来のヘッダファイルとソースファイルからなる一つの翻訳単位の構成においては、内部リンケージ名はソースファイル内に隠蔽されているか、ヘッダファイルに書かれていてもヘッダをインクルードした翻訳単位それぞれで内部リンケージを持つ別のものとして扱われるため、内部リンケージを持つエンティティが翻訳単位外部から参照されることはありませんでした。

/// header.h

#include <iostream>

int external_f(int);

// 翻訳単位毎に定義される
// それぞれの定義が翻訳単位を超えて参照されることはない
static void internal_f(int n) {
  std::cout << n << std::endl;
} 
/// source.cpp

#include "header.h"

namespace {
  // この翻訳単位の外から呼ぶことはできない
  int internal_g() {
    return 10;
  }
}

int external_f(int n) {
  internal_f(n);
  return n + internal_g();
}

しかしC++20以降のモジュールにおいては、そのインターフェース単位で定義されている内部リンケージ名がそのモジュール(インターフェース単位)をインポートした先の翻訳単位からも参照できてしまいます。

ヘッダファイルとは異なり、モジュールのインターフェース単位は一つの翻訳単位であり、そのインポートはインターフェース単位にあるすべての宣言を(インポートした側から)到達可能にし、エクスポートされている宣言を可視(名前探索で見えるように)にします。この到達可能性は、内部リンケージを持つ名前であってもインポートした側の翻訳単位の宣言に影響を及ぼす可能性があります。

また、モジュールで定義されたテンプレートがインスタンス化されたとき、インスタンス化が発生した地点から定義に至る一連のインスタンス化経路上で可視になっている宣言を参照することができます。このインスタンス化経路は翻訳単位を超えて定義され、そこでは内部リンケージを持つ名前を参照することができます。

/// mymodule.cpp
module;
#include <iostream>
export module mymodule;

// 内部リンケージ名を翻訳単位外から参照できないのは変わらないが
// インポートした側から宣言は到達可能となる
static void internal_f(int n) {
  std::cout << n << std::endl;
}

namespace {
  int internal_g() {
    return 10;
  }
}

// エクスポートされている、外部リンケージ
export inline int external_f(int n) {
  // 外部リンケージを持つ定義内での内部リンケージ名の使用
  // 例えばこの関数がインライン展開されたとき、何が起こる・・・?
  internal_f(n);
  return n + internal_g();
}

直接的に別の翻訳単位にある内部リンケージを持つ名前を参照できるわけではありませんが、間接的に内部リンケージを持つ名前が翻訳単位から露出してしまいます。例えばエクスポートされた関数が使われた先でインライン展開されるとき、内部リンケージの名前が参照されていたらどうするべきでしょうか・・・?

そのため、最終的なC++20モジュール仕様では、内部リンケージを含めた翻訳単位ローカルのエンティティ(TU-local Entities)が翻訳単位の外から間接的にも直接的にも参照されることが禁止されました。内部リンケージ名は実装の整理や分割のために用いられるものであり、そのような実装詳細はモジュールの外部に公開されるべきではない、という判断です。

export module M;

// 内部リンケージの関数
static constexpr int f() { return 0; }

static int f_internal() { return f(); } // 内部リンケージ、OK
       int f_module()   { return f(); } // モジュールリンケージ、OK
export int f_exported() { return f(); } // 外部リンケージ、OK

// 外部orモジュールリンケージを持つinline関数はTU-localエンティティを参照できない
static inline int f_internal_inline() { return f(); } // OK
       inline int f_module_inline()   { return f(); } // ERROR
export inline int f_exported_inline() { return f(); } // ERROR

// constexpr/consteval関数は暗黙inline
static constexpr int f_internal_constexpr() { return f(); } // OK
       constexpr int f_module_constexpr()   { return f(); } // ERROR
export constexpr int f_exported_constexpr() { return f(); } // ERROR

static consteval int f_internal_consteval() { return f(); } // OK
       consteval int f_module_consteval()   { return f(); } // ERROR
export consteval int f_exported_consteval() { return f(); } // ERROR

// 戻り値型に露出しているのも禁止
static decltype(f()) f_internal_decltype() { return 0; } // OK
       decltype(f()) f_module_decltype()   { return 0; } // ERROR
export decltype(f()) f_exported_decltype() { return 0; } // ERROR


namespace {
  struct c_internal {
    int mf();
    int mf_internal_inline() { return f(); } // OK
  };
  int c_internal::mf() { return f(); } // OK
} // namespace

// モジュールリンケージのクラス定義
struct c_module {
  int mf_module();
  int mf_module_inline() { return f(); }  // OK、暗黙inlineではない
};
int c_module::mf_module() { return f(); } // OK

// 外部リンケージのクラス定義
export struct c_exported {
  int mf_exported();
  int mf_exported_inline() { return f(); } // OK、暗黙inlineではない
};
int c_exported::mf_exported() { return f(); } // OK


static int v_internal = f(); // OK
       int v_module   = f(); // OK
export int v_exported = f(); // OK

static inline int v_internal_inline = f(); // OK
       inline int v_module_inline   = f(); // ERROR
export inline int v_exported_inline = f(); // ERROR

struct c_sdm_module {
  static int sdm_module;
  static constexpr int sdm_module_constexpr = f(); // ERROR
};
int c_sdm_module::sdm_module = f(); // OK

より深遠なサンプルコードはP1498R1をご覧ください。ただし、メンバ関数とテンプレートの例は最終的な仕様とは異なります。

このようなことを規格書では、TU-local Entitiesとその曝露(exposure)として表現しています。

TU-local Entities?

TU-localとは、翻訳単位内にローカルな、みたいな意味です。

TU-localとなるエンティティは基本的には内部リンケージなものを指しています。より正確には次のものです

  1. 内部リンケージ名をもつ関数、型、変数、テンプレート
  2. TU-localエンティティの定義内で、ラムダ式によって導入または宣言された、リンケージ名を持たない関数、型、変数、テンプレート
  3. クラスの宣言・定義、関数本体、初期化子、の外側で定義されている名前のない型
  4. TU-localエンティティを宣言するためだけに使用される、名前のない型
  5. TU-localテンプレートの特殊化
  6. TU-localテンプレートを実引数として与えられたテンプレートの特殊化
  7. その宣言が曝露されているテンプレートの特殊化
    • 特殊化は、暗黙的あるいは明示的なインスタンスによって生成される

基本的には1と3が1次TU-localエンティティであり、他はそれによって副次的にTU-localとなっています。つまり、ほとんどの場合に気にすべき対象は内部リンケージ名を持つものです。

7だけは意味が分かりませんが、テンプレートの事を考えるとおぼろげながら浮かんでくるものがあります。後程振り返ることにします。

また、値やオブジェクトは次の場合にTU-localとなります

  1. TU-local関数またはTU-local変数に関連付けられているオブジェクトであるか、そのポインタ型の場合
  2. クラスか配列のオブジェクトであり、そのサブオブジェクト(メンバ、基底クラス、要素)のいずれかがTU-localである
  3. クラスか配列のオブジェクトであり、その参照型の非静的データメンバが参照するオブジェクトまたは関数のいずれかがTU-localである

ややこしいですが、TU-localなものの中にある変数や値はTU-localで、TU-localなものを参照しているものも、それを含むものもTU-localという事です。

説明のため、以降TU-localと言ったらTU-localエンティティとTU-localな値(オブジェクト)両方を指すことにします。ですが、分かり辛かったらTU-local=内部リンケージ名と思っても差し支えありません。

TU-local Entityの曝露(exposure

ある宣言は次の場合にTU-localなものを曝露(exposure)しています

  1. TU-localな値によって初期化されたconstexpr変数を定義する場合
  2. 次の場合を除いて、TU-localエンティティを参照する場合
    1. . 非inline関数、または関数テンプレートの本体
      • TU-localな型が、プレースホルダによる戻り値型で宣言された関数の推定された戻り値型となる場合を除く
    2. . 変数または変数テンプレートの初期化子
    3. . クラス定義内フレンド宣言
    4. . 非volatileconstオブジェクトへの参照、またはodr-useされておらず定数式で初期化された内部リンケージかリンケージ名の無い参照

TU-localなものが曝露されているとはすなわち、TU-localなものが翻訳単位外部から参照できうる場所に現れている事です。

たとえば1のケース、constexpr変数は定数伝播の結果、その初期化子が参照先にインライン展開される可能性があります。それがTU-localな値を使って初期化されている場合、そのTU-localな値が翻訳単位外に曝される可能性があります。

注意としては、TU-localなものを一切含まない宣言は、外部リンケージを持つものであっても何かを曝露しているわけではありません。曝露されているというのはTU-localなものに対してのことで、TU-localなものを含んでいてかつそれが翻訳単位外に曝される可能性がある場合です。

また、TU-localエンティティを曝露(exposure)するのは常にTU-localではないものです。そして、inline関数がTU-localエンティティを参照する場合、常に曝露する事になります。

export module tu_locale.sample;

// TU-localなもの
namespace {
  constexpr int tul_n = 10;

  void tul_f();

  struct tul_C {};
}


// 曝露していない例

export void ext_f() {
  tul_f();  // 暴露していない(条件2-1)
}

export int ext_n = tul_n; // 暴露していない(条件2-2)

export struct S {
  friend tul_C; // 暴露していない(条件2-3)

  friend void mem_f(tul_C); // 暴露していない(条件2-3)
};

export const int& ext_ref = tul_n; // 暴露していない(条件2-4)


// 曝露している例

constexpr int mod_n = tul_n;  // 曝露している

export inline void ext_f() {
  tul_f();  // 曝露している
}

export decltype(tul_n) ext_g();  // 曝露している

この例で示されていることは、exportの有無で変わらないはずです。つまり、外部リンケージとモジュールリンケージの違いでは曝露するかしないかは変化しません。

TU-local Entityを曝露してたら・・・

モジュールインターフェース単位(プライベートモジュールフラグメントの外側)、あるいはモジュールパーティションにおいて、TU-localではない宣言(あるいはそのための推論補助)がTU-localエンティティを曝露している場合、コンパイルエラーとなります。

TU-localな宣言が単にモジュールのインターフェース単位に書いてあるだけではコンパイルエラーとはなりません。それらの宣言が別の宣言を介して翻訳単位の外から参照される 可能性がある 場合にコンパイルエラーとなります。実際に参照されたときではなく、参照することができる状態になっていたらエラーです。

モジュール単位も一つの翻訳単位をなすため、あるモジュール単位のTU-localなものは同じモジュール内の他の翻訳単位に対しても曝露されてはなりません。

ただし、非モジュールなところ(グローバルモジュール)、あるいはモジュール実装単位(notパーティション)においては、この事は単に非推奨とされコンパイルエラーとはなりません。

そしてもう一つ、ある宣言が、ヘッダユニットではない別の翻訳単位のTU-localエンティティを参照する場合もコンパイルエラーとなります。

こちらの条件はモジュールであるかどうかにかかわらずすべての所に適用されます。ヘッダユニットが例外として除かれているのは、#includeから置換されたときでも後方互換を損ねないようにするためだと思われます。つまりほとんど、モジュールをインポートした時にインポート先にあるTU-localエンティティを参照することを禁ずるものです。

/// mymoudle.cpp
export module mymodule;

static int f() { /* ... */ }
/// main.cpp
import mymodule;
// f()が到達可能となる

int f();  // f()が可視になる

int main() {
  int n = f();  // NG!
}

まとめると、次のどちらかの場合にTU-localエンティティを参照することはコンパイルエラーとなります

  1. ヘッダユニットを除いたインポート可能なモジュール単位において、TU-localではない宣言(あるいはそのための推論補助)がTU-localエンティティを曝露している
  2. ある宣言が、ヘッダユニットではない別の翻訳単位のTU-localエンティティを参照している

テンプレート

TU-localエンティティを曝露してはならないのはテンプレートも同様です。しかし、テンプレートがTU-localエンティティを曝露するのかどうかはインスタンス化されるまでわかりません。そのため、テンプレートがTU-localなものを曝露しているかの判定はテンプレートがインスタンス化される時まで延期されます。

そして、インスタンス化される時、以下のどちらかに該当する場合にコンパイルエラーとなります。

  1. 現れる名前が内部リンケージ名である
  2. 関数名のオーバーロード候補集合に内部リンケージ名が含まれている
/// mymodule.cpp
export module mymodule;

export struct S1 {};

static void f(S1);  // (1)

export template<typename T>
void f(T t);  // (2)

// 宣言はOK
export template<typename T>
void external_f(T t) {
  f(t);
}
/// main,cpp
import mymodule;

struct S2{};

void f(S2);  // (3)

int main() {
  S1 s1{};
  S2 s2{};

  external_f(10);  // OK、(2)を呼ぶ
  external_f(s2);  // OK、(3)を呼ぶ
  external_f(s1);  // NG、(1)を呼ぶ
}

勘のいい人は引っかかるかもしれません。さっきと言ってたこと違わない?と

  • 次の場合を除いて、TU-localエンティティを参照する場合
    • inline関数、または 関数テンプレートの本体

これはインスタンス化が発生する前は非依存名であっても、とりあえず内部リンケージを参照する式を書いてもいいよ、という事を言っているにすぎません。インポートした先でインスタンス化が発生したとき、そこでの名前解決の結果、あるいはオーバーロード候補集合にインポート元の内部リンケージなものが含まれているとエラーになります。これはどうやら、TU-localなものの曝露とは別ルートの規定の様です。

明示的インスタンス

テンプレートがその翻訳単位で明示的インスタンス化されていれば、本体で内部リンケージ名を参照していてもコンパイルエラーにはなりません。

/// mymodule.cpp
export module mymodule;

export struct S1 {};

static void f(S1 s);  // (1)

// 宣言はOK
export template<typename T>
void external_f(T t) {
  f(t);
}

// S1に対するexternal_f()の明示的インスタンス化
template void external_f<S1>(S1);
/// main,cpp
import mymodule;

int main() {
  S1 s1{};

  external_f(s1);  // OK
}

この様な場合、普通の関数をその宣言によって参照しているのと同じとみなすことができます。どうやら、インポート元に明示的インスタンス化の定義がある場合、インポートした側ではそれに対応する特殊化の暗黙的インスタンス化は発生しない様です(明確に発生しないとされているわけではないですが)。

これを踏まえると、先ほどのTU-localなものの中の条件に羅列されていた謎が一つ解決されます

  • その宣言が曝露されているテンプレートの特殊化
    • 特殊化は、暗黙的あるいは明示的なインスタンスによって生成される

つまりは、TU-localなものを曝露しているテンプレートがその翻訳単位で明示的インスタンス化されているとき、それに対応する特殊化もまたTU-localとなり、それを曝露することも禁止です。

/// mymodule.cpp
export module mymodule;

export struct S1 {};

static void f(S1 s);  // (1)

// 宣言はOK
export template<typename T>
void external_f(T) {
  f();
}

// S1に対するexternal_f()の明示的インスタンス化
template void external_f<S1>(S1);

export void g() {
  S1 s1{};
  f(S1);  // NG、TU-localな特殊化external_f<S1>()の曝露
}

なお、この明示的インスタンス化による例外は、inlineとマークされている関数テンプレートでは無効です。inlineの示すとおりにインライン展開された場合、結局その定義がインポートした側に展開されることになるためです。

/// mymodule.cpp
export module mymodule;

export struct S1 {};

static void f(S1 s);  // (1)

// 宣言はOK
export template<typename T>
inline void external_f(T t) {
  f(t);
}

// S1に対するexternal_f()の明示的インスタンス化
template void external_f<S1>(S1);
/// main,cpp
import mymodule;

int main() {
  S1 s1{};

  external_f(s1);  // NG!
}

メンバ関数の暗黙inline

ここまで見てきたようにおおよそinline関数(テンプレート)においては、TU-localなものの曝露がごく簡単に起きます。それで困ってしまうのが、クラスのメンバ関数が暗黙的にinlineになることです。

export module mymodule;

static void internal_f();

// モジュールリンケージのクラス定義
struct c_module {

  // inlineではない
  int mf_module();

  // 暗黙inline
  int mf_module_inline() { 
    return internal_f();  // NG、内部リンケージ名を曝露している
  }
};

int c_module::mf_module() { 
  return internal_f();  // OK、曝露していない
}

モジュール内でいつものようにクラスを書いたとき、ヘッダファイルからモジュールへ移行するとき、この様なエラーに遭遇する可能性は非常に高いでしょう。これを回避しようとすると、内部リンケージ名を使用しているメンバ関数は全てクラス外で定義しなければなりません。明らかに冗長な記述が増え、とても面倒くさいです・・・

C++20の最終仕様では、モジュール内でのみメンバ関数の暗黙inline化は行われなくなります。これによって、クラスの定義は今まで通りに行うことができるようになります。

export module mymodule;

static void internal_f();

// モジュールリンケージのクラス定義
struct c_module {

  // inlineではない
  int mf_module();

  // inlineではない
  int mf_module_inline() { 
    return internal_f();  // OK、曝露していない
  }

  // inliine
  inline int inline_f() {
    // ...
  }
};

int c_module::mf_module() { 
  return internal_f();  // OK、曝露していない
}

暗黙のinlineが行われない事によってインライン展開がされなくなり、パフォーマンスが低下する可能性は無くもありません。inlineが必要な場合は明示的に指定するようにしましょう。

なお、ここでのモジュールにはグローバルモジュールを含んでいません。モジュールの外ではこれまで通りにメンバ関数は暗黙inlineです。

さんぷるこーど

規格書より、複雑な例。

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

static void f() {}

inline void it() { f(); }           // error: fを曝露している
static inline void its() { f(); }   // OK

template<int>
void g() { its(); }   // とりあえずはOK、これはモジュールリンケージ
template void g<0>();

decltype(f) *fp;                    // error: fはTU-local(fの型ではない)
auto &fr = f;                       // OK
constexpr auto &fr2 = fr;           // error: fを曝露している(fのアドレスはTU-localな値)
constexpr static auto fp2 = fr;     // OK

struct S { void (&ref)(); } s{f};               // OK, 値(fのアドレス)はTU-local
constexpr extern struct W { S &s; } wrap{s};    // OK, 値(sのアドレス)はTU-localではない

static auto x = []{f();};           // OK
auto x2 = x;                        // error: decltype(x)を曝露している(クロージャ型はTU-local)
int y = ([]{f();}(),0);             // error: fを曝露している(クロージャ型はTU-localではない)
int y2 = (x,0);                     // OK

namespace N {
  struct A {};
  void adl(A);
  static void adl(int);
}
void adl(double);

inline void h(auto x) { adl(x); }   // OK, ただしその特殊化はN::adl(int)を曝露しうる
/// A_impl.cpp(実装単位)
module A;
// Aのインターフェースを暗黙的にインポートしている

void other() {
  g<0>();                   // OK, 特殊化g<0>()は明示的にインスタンス化されている
  g<1>();                   // error: 特殊化の実体は、TU-localなits()を使用している
  h(N::A{});                // error: オーバーロード候補集合はTU-localなN::adl(int)を含んでいる
  h(0);                     // OK, adl(double)を呼ぶ
  adl(N::A{});              // OK; N::adl(N::A)を呼び、N::adl(int)は見つからない
  fr();                     // OK, f()を呼ぶ
  constexpr auto ptr = fr;  // error: frは定数式で使用可能ではない
}

テンプレートの例。

/// moduleM.cpp
export module M;

namespace R {
  export struct X {};
  export void f(X);
}
namespace S {
  export void f(R::X, R::X);  // (1)
}
/// moduleN.cpp
export module N;
import M;

export R::X make();

namespace R {
  static int g(X);  // (2)
}

// 宣言まではOK
export template<typename T, typename U>
void apply(T t, U u) {
  f(t, u);  // (1)を参照、OK
  g(t);     // (2)を参照、内部リンケージ名の曝露、NG
}
/// main.cpp
module Q;
import N;

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

int main() {
  auto x = make();  // OK、decltype(x)はR::Xでmodule Mにあり、可視ではないが名前を参照していない

  apply(x, S::Z()); // NG、S::fはインスタンス化コンテキストで可視
                    // しかし、R::gは内部リンケージであり、翻訳単位の外からは呼べない
}

モジュールにおけるinlineの意味

これらの変更によってモジュールにおけるinlineはある意味で本来の意味と役割を取り戻します。すなわち、inlineとマークされた関数のみをインライン展開するという意味になり、その他の効果はほぼ意味をなさなくなります。

意味をなさなくなるというのはinlineの持つ定義の唯一性などの効果がなくなるわけではなく、モジュールにおいてはその意味がなくなるということです。例えば、モジュールでエクスポートされているinline関数・変数はインポートされた側から可視かつ到達可能となり参照できるようになりますが、そこでは#includeの時のように定義が翻訳単位ごとにコピペされる事はないので、inlineの定義を一つに畳み込む効果は必要ありません。

モジュール内部ではODRが厳しくなっており、モジュール内で定義されたinline関数の定義はただ一つでなくてはならず、参照する場合はその定義に到達可能となっている必要があります。このことにリンケージは関係なく、1つのモジュール内においてもinlineはインライン展開のためのコンパイラへの口添え以外の意味を持っていません。

ただし、ここでモジュールと言っているものにグローバルモジュールは含んでいません。すなわち、モジュールの外側では従来と変わりありません。

また、モジュールにおいてinlineと宣言されていない関数をインライン展開してはいけないという規定はありません。どうやらそのような実装を可能にするために意図的に空白を設けているようです。

モジュールリンケージ

これらの変更によって、内部リンケージ名はモジュールのABIの一部とはなることは無くなり、完全に翻訳単位内に隠蔽されるようになります。

一方、モジュールリンケージ名はそうではなく、エクスポートされたinline関数の内部など、使用される場所によってはモジュールのABIの一部となる事があります。

export module M;

// 内部リンケージ
static void internal_f() { /* ... */ }

// モジュールリンケージ
void module_f() { /* ... */ }

export inline void ng() {
  internal_f();  // NG
}

export inline void ok() {
  module_f();    // OK
}

なおどちらもAPIの一部となることはありません。

わからん、3行で

モジュールでは内部リンケージ名を
inline関数や関数テンプレートから
参照するのはやめましょう

参考文献

この記事のMarkdownソース