パソコンでプログラミングしよう ウィンドウズC++プログラミング環境の構築
1.6.3.6(15)
テンプレートを考察する

C++テンプレートについてサイト作成者の理解を整理する。

テンプレートは明示的特殊化を除き、クラスあるいは関数をテンプレート実引数リストを用いて実体化し、このように実体化されたものを暗黙的特殊化と呼ぶ。これはコンパイラ(狭義のコンパイラとリンカ)によるコンパイルリンクの内部で行われる。規格は実体化位置(Point Of Instantiation、POI)で特殊化が実体化すると言うが(JTC1/SC22/WG21 N4659 17.6.4.1)、多くはこれを以下のように解釈する。

  1. プログラマが実体化を指示する場合、プログラマが指示した場所にテンプレート定義の仮引数を実引数で置き換えたコードを挿入する(明示的実体化)
  2. 明示的実体化されていない特殊化が参照された時、コンパイラが正しい場所にテンプレート定義の仮引数を実引数で置き換えたコードを挿入する(暗黙的実体化)
  3. 挿入コードを含むソースコード全体をコンパイルリンクする

これが間違いなのは明白で、なぜなら関数テンプレートのPOIは翻訳単位あるいはプログラムで複数ありうるが(17.6.4.1/p8)、その全てにコード挿入すればODR違反ではないか。またプログラマの書く明示的特殊化は通常のクラス/関数コードと何ら変わらないが、コンパイラの生成する暗黙的特殊化はそのコードが存在するとしても名前探索に特別のルールが適応される。暗黙的特殊化とはいったい何なのだろうか。

本項目はテンプレートの実体化と特殊化についてサイト作成者の理解を整理する。特に関数テンプレートの暗黙的特殊化に関する問題をODRを絡めて議論する。最後にテンプレートをpimplイディオムへ応用する。本項目で議論するテンプレートは、全て明示的特殊化を持たないものとする。

予備知識

必要となる予備知識を補う。

テンプレート化エンティティ

テンプレート化エンティティ(templated entity)(N4659 17/p6)はテンプレート定義が含むテンプレート仮引数に依存するエンティティを総称する。クラス/関数テンプレートもテンプレート化エンティティに含む。一部のテンプレート化エンティティに全てのテンプレート実引数リストを与えればクラス/関数を特殊化として得るが、そのルールはクラス/関数テンプレートに準ずる。本項目は断らない限り"クラス/関数テンプレート特殊化"は"クラス/関数を特殊化とするテンプレート化エンティティの特殊化"を参照する。

// テンプレート化エンティティを型仮引数リストから特殊化を生成する一種のメタ関数として整理する、F(型仮引数,...)->特殊化
template<typename T> struct A // クラステンプレート、F(T)->クラス
{
void f() {} // クラステンプレートメンバ関数、F(T)->関数
template<typename T1> void g(T1) {} // クラステンプレートメンバ関数テンプレート、F(T,T1)->関数
struct BB // クラステンプレートメンバクラス、F(T)->クラス
{
void f() {} // クラステンプレートメンバクラスメンバ関数、F(T)->関数
template<typename T1> void g(T1) {} // クラステンプレートメンバクラスメンバ関数テンプレート、F(T,T1)->関数
};
template<typename T0> struct AA // クラステンプレートメンバクラステンプレート、F(T,T0)->クラス
{
void f() {} // クラステンプレートメンバクラステンプレートメンバ関数、F(T,T0)->関数
template<typename T1> void g(T1) {} // クラステンプレートメンバクラステンプレートメンバ関数テンプレート、F(T,T0,T1)->関数
};
};
struct B // クラス、テンプレート化エンティティではない
{
void f() {} // クラスメンバ関数、テンプレート化エンティティではない
template<typename T1> void g(T1) {} // クラスメンバ関数テンプレート、F(T1)->関数
struct BB // クラスメンバクラス、テンプレート化エンティティではない
{
void f() {} // クラスメンバクラスメンバ関数、テンプレート化エンティティではない
template<typename T1> void g(T1) {} // クラスメンバクラスメンバ関数テンプレート、F(T1)->関数
};
template<typename T0> struct AA // クラスメンバクラステンプレート、F(T0)->クラス
{
void f() {} // クラスメンバクラステンプレートメンバ関数、F(T0)->関数
template<typename T1> void g(T1) {} // クラスメンバクラステンプレートメンバ関数テンプレート、F(T0,T1)->関数
};
};
void f() {} // 関数、テンプレート化エンティティではない
template<typename T1> void g(T1) {} // 関数テンプレート、F(T1)->関数

二段階名前探索

テンプレート中の関数コールにおける識別子(名前)がテンプレート仮引数(例えばT)に依存してかつ修飾されていない場合(17.6.2/p1)、識別子は二段階名前探索(two phase lookup)と呼ぶ特別なルールで探索する。規格はそういった識別子を"dependent name"と呼ぶが、その訳語である"依存名"を本サイトはより一般的な意味合いで不用意に使ってきたため、あえて"dependent name"のままで参照する。なお関数識別子がT依存という事はT依存した実引数を持つか、識別子がT依存のテンプレートIDであるかのどちらかである。二段階名前探索を示す(17.6.4.2/p1)。

  1. 修飾されていない識別子としての名前探索(6.4.1)はテンプレート定義位置で行う
  2. 実引数依存名前探索(ADL)(6.4.2)はテンプレート定義位置とPOIで行う

サンプルコードでこれを確認する。クラス自体は関数をコールできないのでスコープなしメンバ列挙型の定義で代用する。スコープなしメンバ列挙型を用いるのはクラステンプレートとPOIが一致するからで、他メンバのほとんどは一致するとは限らない。それぞれの暗黙的特殊化におけるPOIは後に検討する。なお本項目のサンプルコードは全て/* ... */形式のコメント行でPOIを示す。テンプレート実体化に係わる規格は難解でmingw-w64も完全に準拠できてないようで、そのような箇所はコメント末に[?]を付記する。

#include<iostream>
// g()オーバーロード関数の定義位置を意図的に違えてPOIで返り値が異なるようにする
// mingw-w64は関数テンプレートの明示的実体化しても暗黙的実体化が抑止されない [?]
// https://gcc.gnu.org/bugzilla/show_bug.cgi?id=92413
constexpr char g(...) {return '?';} // 最低優先順位のオーバーロード
struct A{}; // X<A>とf<A>は暗黙的実体化する
struct B{}; // X<B>とf<B>は明示的実体化する
template<typename T> struct X // クラステンプレート
{
enum{ // g()の用法、名前探索
E1=::g(A{}), // T依存しない修飾された識別子、テンプレート定義位置で修飾された識別子による名前探索(N4659 6.4.3)
E2=::g(T{}), // T依存する修飾された識別子、実体化時(17.6/p9)にテンプレート定義位置で修飾された識別子による名前探索
E3=g(A{}), // T依存しない修飾されていない識別子、テンプレート定義位置で修飾されていない識別子による名前探索(6.4.1、6.4.2)
E4=g(T{})}; // T依存する修飾されていない識別子すなわち"dependent name"、実体化時に二段階名前探索(17.6/p9) -> POIのADLを名前探索に含む
};
template<typename T> void f(T t) // 関数テンプレート
{
A a{}; // g()の用法、名前探索
std::cout <<::g(a) // T依存しない修飾された識別子、テンプレート定義位置で修飾された識別子による名前探索
<<::g(t) // T依存する修飾された識別子、実体化時にテンプレート定義位置で修飾された識別子による名前探索
<<g(a) // T依存しない修飾されていない識別子、テンプレート定義位置で修飾されていない識別子による名前探索
<<g(t) // T依存する修飾されていない識別子すなわち"dependent name"、実体化時に二段階名前探索 -> POIのADLを名前探索に含む
<<std::endl;
}
template class X<B>; /* X<B>のPOI(明示的実体化)(17.6.4/p6) */
template void f(B); /* f<B>のPOI(明示的実体化) */
constexpr char g(A) {return 'A';} // g(A)オーバーロード、X<A>とf<A>のPOIより前
constexpr char g(B) {return 'B';} // g(B)オーバーロード、X<B>とf<B>のPOIより後
/* X<A>のPOI(暗黙的実体化) */
int main()
{
std::cout<<"\n** Class template specialization **\n"<<std::endl;
std::cout<<(char)X<A>::E1<<(char)X<A>::E2<<(char)X<A>::E3<<(char)X<A>::E4<<std::endl; // "???A"
std::cout<<(char)X<B>::E1<<(char)X<B>::E2<<(char)X<B>::E3<<(char)X<B>::E4<<std::endl; // "????"
std::cout<<"\n** Function template specialization **\n"<<std::endl;
f(A{}); // "???A"
f(B{}); // "????"のはずだが"???B" [?]
return 0;
}
/* f<A>のPOI(暗黙的実体化) */

暗黙的特殊化

ほとんどの教科書はクラステンプレートと関数テンプレートを統一的に語り、部分特殊化などの差異を追加的に説明する。規格もテンプレート(N4659 17)をクラス(12)や関数(11.4)と同列のエンティティとして(6/p3)、クラス/関数テンプレートの差異は詳細とする。しかしそれぞれの特殊化であるクラスと関数は異なる特質を持ち、前者は(メンバを別議論とすれば)コンパイルされたオブジェクトファイルに何も残さないが、後者は(インラインされる場合を除けば)シンボルテーブルに登録されオブジェクトファイルにバイナリイメージを残す。リンカはそういったバイナリを実行形式ファイルに組み込むが、それが複数存在すればODR違反によるエラーとなる。言い換えれば前者のODR範囲は翻訳単位で後者はプログラムである。ところでテンプレートのODR範囲はどちらも翻訳単位であり、つまり関数テンプレートとその特殊化である関数のODR範囲が一致しない。明示的特殊化なら関数テンプレートの宣言のみを参照して矛盾しないが(17.7.3/p2)、暗黙的特殊化は複雑な問題をもたらす事になる。

クラステンプレートの暗黙的特殊化

明示的実体化が指示されない場合、クラステンプレートは暗黙的実体化する(17.7.1/p1)。POIは翻訳単位で最大一つ(17.6.4.1/p8)、特殊化を最初に参照する宣言/定義と同じ名前空間の直前とする(p4)。プログラム中の全POIにおける特殊化はODRの下に厳密に一致しなればならない(p8)。クラステンプレートの暗黙的実体化はメンバ定義を例外を除き実体化しない。例外となるメンバはスコープのない列挙型と無名共用体で、他のメンバ定義は独立して実体化する(17.7.1/p2)。

template<typename T> struct X{};
/* X<int>のPOI */
void f() {X<int> x;}
/* X<int>のPOIではないが、X<double>のPOI */
void g() {X<int> x;X<double> y;}

クラステンプレート特殊化(クラス)は翻訳単位をODR範囲として元のクラステンプレートと一致し、POIも翻訳単位でただ一つしかない。従って本項目冒頭の"テンプレート仮引数を実引数に置き換えて各翻訳単位のPOIにコード挿入する"という認識でほぼほぼ矛盾しない。ただしこの解釈は厳密でなくその理由は関数テンプレート特殊化と合わせて後述する。なお明示的実体化の場合はメンバ全ての実体化を伴い(17.7.2/p8)、そのメンバ関数は当然ながら関数テンプレート特殊化の問題を持つ。

クラステンプレートメンバの暗黙的特殊化

メンバクラスとメンバ関数は"クラス/関数を特殊化とするテンプレート化エンティティ"で、クラス/関数テンプレートのルールで暗黙的/明示的実体化する。それ以外のメンバはクラス/関数テンプレートのどちらかと同じルールで暗黙的実体化する。なおC++17ではスタティックメンバ変数も明示的実体化できるようになったはずだ。クラスメンバーのPOIを調査するテストコードを例示する。一部に後述のODR制約を冒すが、規格準拠しなかったりデバッグ/リリースビルドで挙動が変わったりするようだ。

#include <iostream>
// X<A>の関数テンプレートルールで実体化するメンバがPOI(1)とPOI(2)でODR制約を冒す(N4659 17.6.4.1/p8)が試験目的として行う
template<typename T> struct X // クラステンプレート
{
// スコープのないメンバ列挙型だけがクラステンプレートと共に定義が実体化し、それ以外は独立して実体化される(17.7.1/p2)
enum {E=g(T{})}; // スコープのないメンバ列挙型、クラステンプレートのルールで実体化(17.6.4.1/p4)
enum class ScopedEnum:char {E=g(T{})}; // スコープ付きメンバ列挙型、クラステンプレートのルール
struct NestedClass {enum {E=g(T{})};}; // メンバクラス、クラステンプレートのルール
static constexpr char memVal=g(T{}); // スタティックメンバ変数、関数テンプレートのルールで実体化(17.6.4.1/p1)
static constexpr char memFunc1() {return g(T{});} // メンバ関数、関数テンプレートのルール
static constexpr char memFunc2(T val=T{}) {return g(val);} // デフォルト実引数も関数テンプレートのルールに従う
};
struct A{}; // X<A>はg(A)オーバーロード定義より前でクラスと全メンバを参照する
struct B{}; // X<B>はg(B)オーバーロード定義より後でクラスと全メンバを参照する
struct C{}; // X<C>はg(C)オーバーロード定義より前でクラスを参照し、後で全メンバを参照する
// POIを調査するマクロ関数、テンプレート関数はPOIに影響するので使えない
// クラステンプレートはローカルインスタンスで暗黙的実体化する、g()コールはクラステンプレートPOIではなくマクロ展開位置
#define X_CLASS(T) \
{ \
X<T> x __attribute__((unused)); \
std::cout<<"X<"<<#T<<"> instantiated where g("<<#T<<")="<<g(T{})<<std::endl; \
}
// メンバは全て各POIでg()をコールする、例えばg(A)が'?'を返せばPOIはg(A)オーバーロードより前、'A'を返せば後
#define X_MEMBERS(T) \
{ \
std::cout<<"X<"<<#T<<">::"<<std::endl; \
std::cout<<" E = "<<(char)X<T>::E<<std::endl; \
std::cout<<" ScopedEnum::E = "<<(char)X<T>::ScopedEnum::E<<std::endl; \
std::cout<<" NestedClass::E = "<<(char)X<T>::NestedClass::E<<std::endl; \
std::cout<<" memVal = "<<X<T>::memVal<<std::endl; \
std::cout<<" memFunc1() = "<<X<T>::memFunc1()<<std::endl; \
std::cout<<" memfunc2() = "<<X<T>::memFunc2()<<std::endl; \
}
constexpr char g(...) {return '?';} // 最低優先順位のオーバーロード
/* X<A>クラス、X<C>クラスの(スコープのないメンバ列挙型を含めた)POI */
/* X<A>の(スコープのないメンバ列挙型を除いた)クラステンプレートルールで実体化するメンバのPOI */
void f1()
{
// デバッグビルドでX<A>::memValとX<A>::memFunc1/memfunc2のPOIが一致しない [?]
// X<A>::memFunc1/memFunc2のPOIがデバッグビルドとリリースビルドで一致しない [?]
std::cout<<"\n** Before g() overloads are defined **\n"<<std::endl;
X_CLASS(A);
X_CLASS(C);
X_MEMBERS(A);
}
/* X<A>の関数テンプレートルールで実体化するメンバのPOI(1) */
constexpr char g(A) {return 'A';} // g(A)オーバーロード
constexpr char g(B) {return 'B';} // g(B)オーバーロード
constexpr char g(C) {return 'C';} // g(C)オーバーロード
/* X<B>クラスの(スコープのないメンバ列挙型を含めた)POI */
/* X<B>、X<C>の(スコープのないメンバ列挙型を除いた)クラステンプレートルールで実体化するメンバのPOI */
void f2()
{
// X<C>::ScopedEnum::Eが参照されなくてもクラステンプレート実体化で実体化されている [?]
std::cout<<"\n** After g() overloads are defined **\n"<<std::endl;
X_CLASS(B);
X_MEMBERS(B);
X_MEMBERS(C);
}
/* X<B>、X<C>の関数テンプレートルールで実体化するメンバのPOI(1) */
int main()
{
f1();
f2();
return 0;
}
/* X<A>、X<B>、X<C>の関数テンプレートルールで実体化するメンバのPOI(2) */

関数テンプレートの暗黙的特殊化

明示的実体化が指示されない場合、関数テンプレートは暗黙的実体化する(17.7.1/p4)。POIは特殊化を参照する宣言/定義と同じ名前空間の直後とする(17.6.4.1/p1)。POIは複数の翻訳単位内にそれぞれ複数存在して良く、さらに翻訳単位の末尾であっても良い(p8)。プログラム中の全POIにおける特殊化はODRの下に厳密に一致しなればならない(p8)。以降、暗黙的実体化POIに要求されるODRの厳密な一致をODR制約と呼ぶ。

template<typename T> void f(T) {}
void g1() {f(0);}
/* f<int>のPOI(1) */
void g2() {f(0);f(0.0);}
/* f<int>のPOI(2)、f<double>のPOI(1) */
void g3() {f(0.0);}
/* f<double>のPOI(2)、f<int>のPOI(3) */

関数テンプレート特殊化(関数)は(インライン修飾されてなければ)ODRでプログラムを通してただ一つしか存在できない。これと複数の翻訳単位に存在する複数のPOIをどのように解釈すれば良いのだろうか。暗黙的実体化のPOIは全てODR制約されるわけだから、実質的にはどれでも良い(どうでも良い)と言う事だろうか。

関数テンプレート特殊化は関数であるからインライン展開されない限り実行形式ファイルにバイナリイメージとして存在するはずだ。そういったバイナリをどのPOIが生成するかサンプルコード(function_templates_main.cpp)で調査する。はたしてPOI(1)とPOI(2)のどちらがf(X<4>)バイナリを生成するだろうか。調査手段としてf(X<4>)のコールするオーバーロード関数g(X<0>)とg(X<1>)のPOIに対する位置を違えるが、これはODR制約を冒してあくまでも試験目的のコーディングである。結果はデバッグ/リリースビルド共にPOI(2)を選択して、つまり翻訳単位末尾で得るバイナリを使用している。

// function_templates_main.cpp
// 本来ならインクルードファイルで供給する共通コード ---------------------------------
#include <iostream>
template<int I> struct X:X<I-1> // 関数オーバーロードテスト用のクラス
{
void Check() const {this->DoCheck(I);}
};
template<> struct X<0>
{
void Check() const {DoCheck(0);}
void DoCheck(int i) const {std::cout<<"X<"<<i<<">::Check()"<<std::endl;}
};
template<typename T> void f(T val) {g(val);} // g()をコールする関数テンプレート
//-----------------------------------------------------------------------------------
void g(X<0> val) {val.Check();} // オーバーロード関数g(X<0>)
void h0() {f(X<4>{});} // g()の選択優先順位はX<4>, X<3>, ... , X<0>
/* h0()からの参照によるf(X<4>)のPOI(1)、X<0>::Check()をコールする */
int main()
{
h0();
return 0;
}
void g(X<1> val) {val.Check();} // オーバーロード関数g(X<1>)
/* 翻訳単位末尾によるf(X<4>)のPOI(2)、X<1>::Check()をコールする */

function_templates_main.cppを修正し別の翻訳単位(function_templates_sub.cpp)をリンクしてPOI(3)とPOI(4)を追加する。デバッグビルドでfunction_templates_main.oを最初にリンクするとPOI(2)、function_templates_sub.oを最初にリンクするとPOI(4)を選択する。各オブジェクトファイルはそれぞれの翻訳単位末尾のバイナリを持ち、リンカが最初に見つけた方を実行形式に組み込む。驚くべきはリリースビルドで、リンク順によらずh0()コールはPOI(2)、h2()コールはPOI(4)となる。最適化でリンク以前に翻訳単位末尾のPOIがインライン展開されてしまったみたいだ。

// function_templates_main.cpp
...
extern void h2(); // 別翻訳単位に定義される関数h2()の宣言
int main()
{
h0();
h2(); // 別翻訳単位に定義される関数h2()のコール
return 0;
}
...
// function_templates_sub.cpp
// 本来ならインクルードファイルで供給する共通コード ---------------------------------
#include <iostream>
template<int I> struct X:X<I-1> // 関数オーバーロードテスト用のクラス
{
void Check() const {this->DoCheck(I);}
};
template<> struct X<0>
{
void Check() const {DoCheck(0);}
void DoCheck(int i) const {std::cout<<"X<"<<i<<">::Check()"<<std::endl;}
};
template<typename T> void f(T val) {g(val);} // g()をコールする関数テンプレート
//-----------------------------------------------------------------------------------
void g(X<2> val) {val.Check();} // オーバーロード関数g(X<2>)
void h2() {f(X<4>{});}
/* h2()からの参照によるf(X<4>)のPOI(3)、X<2>::Check()をコールする */
void g(X<3> val) {val.Check();} // オーバーロード関数g(X<3>)
/* 翻訳単位末尾によるf(X<4>)のPOI(4)、X<3>::Check()をコールする */

さらに明示的実体化を試みる。mingw-w64はデバッグ/リリースビルド共にリンク順に関係なくPOI(2)を選択するが、規格は明示的実体化によるPOI(5)とする(17.6.4.1/p6)。明示的実体化宣言がPOI(4)によるバイナリを抑止するのは確認される。明示的実体化宣言をPOI(3)より後置すればそのバイナリが生成されると考えたが、そのような事は起きなかった。規格は明示的実体化宣言の位置について何も規定せず、前後の暗黙的POI全てを抑止するとも解釈できる。

// function_templates_main.cpp
...
void g(X<0> val) {val.Check();} // オーバーロード関数g(X<0>)
void h0() {f(X<4>{});} // g()の選択優先順位はX<4>, X<3>, ... , X<0>
/* h0()からの参照によるf(X<4>)のPOI(1)、X<0>::Check()をコールする */
template void f(X<4>); // f(X<4>)の明示的実体化定義、POIは翻訳単位末尾のまま [?]
/* 明示的実体化によるf(X<4>)のPOI(5)、X<0>::Check()をコールするはずだが [?] */
extern void h2(); // 別翻訳単位に定義される関数h2()の宣言
int main()
{
h0();
h2(); // 別翻訳単位に定義される関数h2()のコール
return 0;
}
void g(X<1> val) {val.Check();} // オーバーロード関数g(X<1>)
/* 翻訳単位末尾によるf(X<4>)のPOI(2)、X<1>::Check()をコールする */
// function_templates_sub.cpp
...
extern template void f(X<4>); // f(X<4>)の明示的実体化宣言、翻訳単位内の暗黙的POIを抑止する
void g(X<2> val) {val.Check();} // オーバーロード関数g(X<2>)
void h2() {f(X<4>{});}
/* h2()からの参照によるf(X<4>)のPOI(3)、X<2>::Check()をコールする */
//extern template void f(X<4>); // 明示的実体化宣言は暗黙的POIより後でも良いのか(規格は不言及)
void g(X<3> val) {val.Check();} // オーバーロード関数g(X<3>)
/* 翻訳単位末尾によるf(X<4>)のPOI(4)、X<3>::Check()をコールする */

これらの結果からmingw-w64の実装を想像する。例外を除き暗黙的実体化のODR制約を守れば正当なコードを生成する。例外は明示的実体化のPOIで、ODR制約にとらわれないはずだから、そのPOIを無視するのは規格に沿わず意図と異なるコード生成の可能性がある。

  • 関数テンプレート特殊化のバイナリは必ず翻訳単位末尾で生成される
  • 関数テンプレート特殊化を明示的実体化定義してもそのPOIは無視される
  • 関数テンプレート特殊化を明示的実体化宣言すれば翻訳単位末尾のバイナリ生成を抑止する
  • リンカは複数のオブジェクトファイルに同じ関数テンプレート特殊化の(異なるかもしれない)バイナリを発見してもエラーとしない
  • リンカは最初に発見する関数テンプレート特殊化のバイナリを実行形式ファイルに組み込む
  • 最適化レベルを上げると関数テンプレート特殊化は翻訳単位末尾のPOIがインライン展開される場合がある

暗黙的特殊化とは何か

プログラマの書く明示的特殊化はソースコード上に定義として実在する。コンパイラ生成の暗黙的特殊化もそのような定義を自動的に挿入するのだろうか。ISOが規格化する前(C++98より前)はクラス/関数テンプレートは与えられた型で"テンプレートクラス/関数"を"定義"するとして(Margaret A. Ellis et al., The Annotated C++ Reference Manual, Readings, Addison-Wesley, 1990; Readings, Addison-Wesley, 1997, p.343,p.345)、そういったものであるかの印象を与えていた。この"定義"は通常エンティティの定義とは別物で、関数テンプレートとODRの関係からも適当な用語と思えない。"クラス/関数テンプレート"から"テンプレートクラス/関数"を生成するという表現もいかがなものか。そして"定義"は"実体化(instantiation)"、"テンプレートクラス/関数"はそれぞれ"特殊化(specialization)"に置き換わる。

実体化された暗黙的特殊化とはテンプレートからクラス/関数のコードを生成するための抽象物と思われ、明示的特殊化のような具体物として説明できない。クラステンプレートのそれはクラス定義のPOIへの挿入としてほぼほぼ矛盾しない。しかしその解釈で明示的実体化を行うと、実体化されない翻訳単位で定義の挿入が行われずクラスODR(翻訳単位で必ず定義)違反となってしまう。クラステンプレートは常に暗黙的実体化し、クラステンプレート明示的実体化は全クラスメンバを明示的実体化すると理解する方がよほど妥当だ。

関数テンプレートはさらに難しい。POIへの定義の挿入と理解するのが大きな誤りである事は既に説明した。ところで最終的には実行形式ファイルに組み込まれるバイナリとなるのは間違いない。これを実体化された暗黙的特殊化と呼ぶのは抵抗あるが、そこへ至る過程に何があるかはコンパイラ(狭義のコンパイラとリンカ)のみぞ知る。mingw-w64実装においてオブジェクトファイルに組み込まれる関数テンプレート暗黙的特殊化のバイナリは、常に翻訳単位末尾をPOIとするものと理解してそれ以上は議論しない(できない)。明示的実体化してもそのPOIを無視して翻訳単位末尾をPOIとするバイナリが生成されるが、この点でmingw-w64は規格に沿わない。そしてリンカは最初に発見したバイナリを実行形式ファイルに組み込む。

ソース構成モデル

ソース構成モデル(source code organization model)(Herb Sutter et al., Why We Can't Afford Export, 2003, JTC1/SC22/WG21 N1426, p.7)は関数テンプレートを記述する方法だが、現実は本サイト含めて"インクルードモデル(inclusion model)"(David Vandevoorde et al., C++ Templates, Boston, Addison-Wesley, 2003, p.149、Why We Can't Afford Export, p7)のみを用いる。C++03以前は規格上"セパレートモデル(separation model)"(C++ Templates, p.149)が存在したが実用されなかった。"セパレートモデル"は"エクスポートモデル(export model)"(Why We Can't Afford Export, p.7)とも呼ばれた。

インクルードモデル

インライン関数f(int)とインラインでない関数テンプレートg(T)で説明する。両方ともインクルードファイルX.hが定義し、つまりg(T)はインラインであるかのように定義する。関数テンプレートの定義はインクルードファイルに晒され、その実装は隠蔽できない。g(T)をどう扱うかはコンパイラ実装に依存するがmingw-w64は"貪欲的実体化(greedy instantiation)"と呼ばれる手法で、詳細は既に説明した。これはインライン修飾された関数がインライン展開されなかった場合の扱いと同様と理解できる。

// X.h (definition, header only)
#include <iostream>
inline void f(int val) {std::cout<<"f(X.h):Given value is "<<val<<std::endl;}
template<typename T> void g(T val) {std::cout<<"f(X.h):Given value is "<<val<<std::endl;}
// main.cpp (client code)
#include "X.h"
int main()
{
f(0);
g(0);
g(1.0/3.0);
return 0;
}

セパレートモデル

セパレートモデルはキーワード"export"でテンプレートを修飾する。これはC++11で廃止された(N4659 C.2.7 17.1)がキーワード自体はC++20でテンプレートと関係ない別構文(N4861 10.2)で復活している。インラインでない関数f(int)とインラインでない関数テンプレートg(T)で説明する。インクルードファイルX.hが(定義でない)宣言してソースコードファイルX.cppが定義する。関数テンプレートの定義はインクルードファイルに晒されず、実装隠蔽されているように見える。

// X.h (declaration)
void f(int);
export template<typename T> void g(T);
// X.cpp (definition)
#include "X.h"
#include <iostream>
void f(int val) {std::cout<<"f(X.cpp):Given value is "<<val<<std::endl;}
template<typename T> void g(T val) {std::cout<<"g(X.cpp):Given value is "<<val<<std::endl;}

これが実用とならなかった顛末はハーブ・サッターがまとめている。EDGが唯一実装に成功するが3人年を要して、これは彼らがJavaの全実装に要した2人年を凌駕する。しかし期待されたビルド時間短縮や実装隠蔽などが原理的に達成できなかった。C++最大の黒歴史だったのだと思う。

疑似的なセパレートモデル

任意型での利用を想定する関数テンプレートの実装隠蔽はセパレートモデルであったとしても不可能だ。テンプレート実引数が確定するのはPOIで、そこでバイナリ生成するには関数テンプレートの定義(あるいは相当する何か)が必要になる。EDG実装は、セパレートモデルが単にそれを見えなくしているだけであることを示した。つまりインクルードモデルであろうがセパレートモデルであろうがテンプレートライブラリの供給にソースコード(あるいは相当する何か)を必要とする。しかし我々は一般に公開するライブラリを書いているわけではない。コーディング省力化にテンプレートを大いに活用するが、テンプレートに与える型をあなたは完全に把握している。そうであるなら関数テンプレートの実装隠蔽は通常の関数と同じレベルでできる。自分が書いた実装を自分から隠蔽する必要があるかと問われれば、pimplイディオムをもって回答する。

サンプルコードのg(T)に与えるテンプレート実引数がintとdoubleだけとして、他は未定義リンクエラーとする。最初に関数テンプレートの明示特殊化で定義を隠蔽する。インクルードファイル(X.h)のg(T)を(定義でない)宣言に留め、各型の明示的特殊化(定義でない)宣言を加える。ソースコードファイル(X.cpp)は各型の明示的特殊化定義する。同じコードを繰り返して少しも省力化にならないが、全ての型に対して実装が異なればそれなりに有効かもしれない。

// X.h (g(T) explicit specialization declarations for all possible types)
void f(int val);
template<typename T> void g(T);
template<> void g(int);
template<> void g(double);
// X.cpp (g(T) explicit specialization definitions for all possible types)
#include "X.h"
#include <iostream>
void f(int val) {std::cout<<"f(X.cpp):Given value is "<<val<<std::endl;}
template<> void g(int val) {std::cout<<"g(X.cpp):Given value is "<<val<<std::endl;}
template<> void g(double val) {std::cout<<"g(X.cpp):Given value is "<<val<<std::endl;}

次に関数テンプレートの明示的実体化で定義を隠蔽する。X.hのg(T)を(定義でない)宣言に留める。X.cppでg(T)を定義し、各型の明示的実体化定義を加える。明示的特殊化による定義隠蔽よりシンプルでこちらが本筋だろう。ある型で明示的特殊化が必要であればそれを明示的実体化の代わりに定義すれば良い。

// X.h (g(T) explicit instantiation declarations are not required)
void f(int);
template<typename T> void g(T);
// X.cpp (g(T) explicit instantiation definitions for all possible types)
#include "X.h"
#include <iostream>
void f(int val) {std::cout<<"f(X.cpp):Given value is "<<val<<std::endl;}
template<typename T> void g(T val) {std::cout<<"g(X.cpp):Given value is "<<val<<std::endl;}
template void g(int);
template void g(double);

なお関数テンプレート型推定のルール(N4659 17.8.2.1)には注意を払う。

// main.cpp (template function argument deduction does not take implicit conversions into account)
int main()
{
unsigned int x=0;
f(x); // intへ暗黙の型変換してコール(N4659 7.8/p3)
//g(x); // g<unsigned int>特殊化が存在しないので未定義リンクエラー(17.8.2.1/p2)
g<int>(x); // intへ暗黙の型変換してg<int>をコール
g((int)x); // g<int>をコール
return 0;
}

テンプレートとpimplイディオム

疑似的なセパレートモデルでテンプレートをpimplイディオムに応用する。本筋の関数テンプレート明示的実体化を用いる。繰り返すがテンプレート実引数を未定とする場合に応用できないが、あなたのプロジェクトなのだからテンプレート実引数に与える全ての型をあなたは知っているはずだ。コーディングは通常のpimplイディオムと大きな差異は無く、実装するソースコードファイルにテンプレート実引数となる型の全てで明示的実体化定義を加える。さすがに明示的実体化定義を何十個も加えれば手間だが、サイト作成者の経験範囲で10個を上回ることは無かった。

メンバ関数テンプレート

最初にクラスがメンバ関数テンプレートを持つ場合を検討する。メンバ関数テンプレートは"関数を特殊化とするテンプレート化エンティティ"であるから、関数テンプレート特殊化として実装隠蔽できる。TMyTypeCheck1クラスはCheckメンバ関数テンプレートを持ち、これをpimplイディオムに沿って実装隠蔽する。Checkメンバ関数のテンプレート実引数に与える型はint、double、const char*として、それぞれをソースコードファイルで明示的実体化する。なおユーザー定義型はインクルードファイルなどで定義を供給する必要がある。

TMyTypeCheck1.h

#ifndef TMYTYPECHECK1_H
#define TMYTYPECHECK1_H
#include <memory>
class TMyTypeCheck1 final
{
private:
class Impl;
std::unique_ptr<Impl> pimpl_;
public:
TMyTypeCheck1();
~TMyTypeCheck1();
template<typename T> void Check(T val);
};
#endif // TMYTYPECHECK1_H

TMyTypeCheck1.cpp

#include "TMyTypeCheck1.h"
#include <iostream>
#include <boost/core/demangle.hpp>
struct TMyTypeCheck1::Impl
{
template<typename T> void Check(T val)
{
std::cout<<"Given value is "<<val
<<" of which type is "<<boost::core::demangle(typeid(T).name())
<<"."<<std::endl;
}
};
TMyTypeCheck1::TMyTypeCheck1():pimpl_{new Impl} {}
TMyTypeCheck1::~TMyTypeCheck1() {}
template<typename T> void TMyTypeCheck1::Check(T val) {pimpl_->Check(val);}
// TMyTypeCheck1::Check explicit instantiations.
template void TMyTypeCheck1::Check(int);
template void TMyTypeCheck1::Check(double);
template void TMyTypeCheck1::Check(const char*);

クラステンプレート

次にクラステンプレートの場合を検討する。クラステンプレート自体は隠蔽と無関係で、しかし全てのメンバ関数を実装隠蔽する。クラステンプレートメンバ関数は"関数を特殊化とするテンプレート化エンティティ"であるから、関数テンプレート特殊化として実装隠蔽できる。全てのメンバ関数をソースコードファイルで明示的実体化する必要があるが、これはクラステンプレート明示的実体化で一括できる(N4659 17.7.2/p8)。TMyTypeCheck2クラステンプレートのテンプレート実引数に与える型はint、double、const char*として、それぞれをソースコードファイルで明示的実体化する。なおユーザー定義型はインクルードファイルなどで定義を供給する必要がある。

TMyTypeCheck2.h

#ifndef TMYTYPECHECK2_H
#define TMYTYPECHECK2_H
#include <memory>
template<typename T> class TMyTypeCheck2 final
{
private:
class Impl;
std::unique_ptr<Impl> pimpl_;
public:
TMyTypeCheck2();
~TMyTypeCheck2();
void Check(T val);
};
#endif // TMYTYPECHECK2_H

TMyTypeCheck2.cpp

#include "TMyTypeCheck2.h"
#include <iostream>
#include <boost/core/demangle.hpp>
template<typename T> struct TMyTypeCheck2<T>::Impl
{
void Check(T val)
{
std::cout<<"Given value is "<<val
<<" if it is interpreted as "<<boost::core::demangle(typeid(T).name())
<<"."<<std::endl;
}
};
template<typename T> TMyTypeCheck2<T>::TMyTypeCheck2():pimpl_{new Impl} {}
template<typename T> TMyTypeCheck2<T>::~TMyTypeCheck2() {}
template<typename T> void TMyTypeCheck2<T>::Check(T val) {pimpl_->Check(val);}
// TMyTypeCheck2 explicit instantiations.
template class TMyTypeCheck2<int>;
template class TMyTypeCheck2<double>;
template class TMyTypeCheck2<const char*>;

明示的実体化メタプログラミング

疑似的なセパレートモデルのテンプレート明示的実体化定義をメタプログラミングを用いて型リストから自動化してみる。詳細はソースコードに譲るが、規格上不明確な点が多くmingw-w64以外の実装に互換できる確証もない。メタプログラミングの常としてパズル的な面白さはあるけれど、それぞれの明示的実体化定義を愚直に書き連ねる方が可読性やメンテナンス性からも良いと思う。