|
本サイトは移転しました。旧アドレスからのリダイレクトは2025年03月31日(月)まで有効です。
|
🛈 | ✖ |
テンプレートは明示的特殊化を除き、クラスあるいは関数をテンプレート実引数リストを用いて実体化し、このように実体化されたものを暗黙的特殊化と呼ぶ。これはコンパイラ(狭義のコンパイラとリンカ)によるコンパイルリンクの内部で行われる。規格は実体化位置(Point Of Instantiation、POI)で特殊化が実体化すると言うが(JTC1/SC22/WG21 N4659 17.6.4.1)、多くはこれを以下のように解釈する。
これが間違いなのは明白で、なぜなら関数テンプレートのPOIは翻訳単位あるいはプログラムで複数ありうるが(17.6.4.1/p8)、その全てにコード挿入すればODR違反ではないか。またプログラマの書く明示的特殊化は通常のクラス/関数コードと何ら変わらないが、コンパイラの生成する暗黙的特殊化はそのコードが存在するとしても名前探索に特別のルールが適応される。暗黙的特殊化とはいったい何なのだろうか。
本記事はテンプレートの実体化と特殊化についてサイト作成者の理解を整理する。特に関数テンプレートの暗黙的特殊化に関する問題をODRを絡めて議論する。最後にテンプレートをpimplイディオムへ応用する。本記事で議論するテンプレートは、全て明示的特殊化を持たないものとする。
必要となる予備知識を補う。
テンプレート化エンティティ(templated entity)(N4659 17/p6)はテンプレート定義が含むテンプレート仮引数に依存するエンティティを総称する。クラス/関数テンプレートもテンプレート化エンティティに含む。一部のテンプレート化エンティティに全てのテンプレート実引数リストを与えればクラス/関数を特殊化として得るが、そのルールはクラス/関数テンプレートに準ずる。本記事は断らない限り"クラス/関数テンプレート特殊化"は"クラス/関数を特殊化とするテンプレート化エンティティの特殊化"を参照する。
テンプレート中の関数コールにおける識別子(名前)がテンプレート仮引数(例えばT)に依存してかつ修飾されていない場合(17.6.2/p1)、識別子は二段階名前探索(two phase lookup)と呼ぶ特別なルールで探索する。規格はそういった識別子を"dependent name"と呼ぶが、その訳語である"依存名"を本サイトはより一般的な意味合いで不用意に使ってきたため、あえて"dependent name"のままで参照する。なお関数識別子がT依存という事はT依存した実引数を持つか、識別子がT依存のテンプレートIDであるかのどちらかである。二段階名前探索を示す(17.6.4.2/p1)。
サンプルコードでこれを確認する。クラス自体は関数をコールできないのでスコープなしメンバ列挙型の定義で代用する。スコープなしメンバ列挙型を用いるのはクラステンプレートとPOIが一致するからで、他メンバのほとんどは一致するとは限らない。それぞれの暗黙的特殊化におけるPOIは後に検討する。なお本記事のサンプルコードは全て/* ... */形式のコメント行でPOIを示す。テンプレート実体化に係わる規格は難解でmingw-w64も完全に準拠できてないようで、そのような箇所はコメント末に[?]を付記する。
ほとんどの教科書はクラステンプレートと関数テンプレートを統一的に語り、部分特殊化などの差異を追加的に説明する。規格もテンプレート(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)。
クラステンプレート特殊化(クラス)は翻訳単位をODR範囲として元のクラステンプレートと一致し、POIも翻訳単位でただ一つしかない。従って本記事冒頭の"テンプレート仮引数を実引数に置き換えて各翻訳単位のPOIにコード挿入する"という認識でほぼほぼ矛盾しない。ただしこの解釈は厳密でなくその理由は関数テンプレート特殊化と合わせて後述する。なお明示的実体化の場合はメンバ全ての実体化を伴い(17.7.2/p8)、そのメンバ関数は当然ながら関数テンプレート特殊化の問題を持つ。
メンバクラスとメンバ関数は"クラス/関数を特殊化とするテンプレート化エンティティ"で、クラス/関数テンプレートのルールで暗黙的/明示的実体化する。それ以外のメンバはクラス/関数テンプレートのどちらかと同じルールで暗黙的実体化する。なおC++17ではスタティックメンバ変数も明示的実体化できるようになったはずだ。クラスメンバのPOIを調査するテストコードを例示する。一部に後述のODR制約を冒すが、規格準拠しなかったりデバッグ/リリースビルドで挙動が変わったりするようだ。
明示的実体化が指示されない場合、関数テンプレートは暗黙的実体化する(17.7.1/p4)。POIは特殊化を参照する宣言/定義と同じ名前空間の直後とする(17.6.4.1/p1)。POIは複数の翻訳単位内にそれぞれ複数存在して良く、さらに翻訳単位の末尾であっても良い(p8)。プログラム中の全POIにおける特殊化はODRの下に厳密に一致しなればならない(p8)。以降、暗黙的実体化POIに要求されるODRの厳密な一致をODR制約と呼ぶ。
関数テンプレート特殊化(関数)は(インライン修飾されてなければ)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を修正し別の翻訳単位(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がインライン展開されてしまったみたいだ。
さらに明示的実体化を試みる。mingw-w64はデバッグ/リリースビルド共にリンク順に関係なくPOI(2)を選択するが、規格は明示的実体化によるPOI(5)とする(17.6.4.1/p6)。明示的実体化宣言がPOI(4)によるバイナリを抑止するのは確認される。明示的実体化宣言をPOI(3)より後置すればそのバイナリが生成されると考えたが、そのような事は起きなかった。規格は明示的実体化宣言の位置について何も規定せず、前後の暗黙的POI全てを抑止するとも解釈できる。
これらの結果からmingw-w64の実装を想像する。例外を除き暗黙的実体化のODR制約を守れば正当なコードを生成する。例外は明示的実体化のPOIで、ODR制約にとらわれないはずだから、そのPOIを無視するのは規格に沿わず意図と異なるコード生成の可能性がある。
プログラマの書く明示的特殊化はソースコード上に定義として実在する。コンパイラ生成の暗黙的特殊化もそのような定義を自動的に挿入するのだろうか。ISOが規格化する前(C++98より前)はクラス/関数テンプレートは与えられた型で"テンプレートクラス/関数"を"定義"するとして(Margaret A. Ellis et al., The Annotated C++ Reference Manual, Reading, Addison-Wesley, 1990; Reading, 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, p.7)のみを用いる。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)"と呼ばれる手法で、詳細は既に説明した。これはインライン修飾された関数がインライン展開されなかった場合の扱いと同様と理解できる。
セパレートモデルはキーワード"export"でテンプレートを修飾する。これはC++11で廃止された(N4659 C.2.7 17.1)がキーワード自体はC++20でテンプレートと関係ない別構文(N4849 10.2)で復活している。インラインでない関数f(int)とインラインでない関数テンプレートg(T)で説明する。インクルードファイルX.hが(定義でない)宣言してソースコードファイルX.cppが定義する。関数テンプレートの定義はインクルードファイルに晒されず、実装隠蔽されているように見える。
これが実用とならなかった顛末はハーブ・サッターがまとめている。EDGが唯一実装に成功するが3人年を要して、これは彼らがJavaの全実装に要した2人年を凌駕する。しかし期待されたビルド時間短縮や実装隠蔽などが原理的に達成できなかった。C++最大の黒歴史だったのだと思う。
任意型での利用を想定する関数テンプレートの実装隠蔽はセパレートモデルであったとしても不可能だ。テンプレート実引数が確定するのはPOIで、そこでバイナリ生成するには関数テンプレートの定義(あるいは相当する何か)が必要になる。EDG実装は、セパレートモデルが単にそれを見えなくしているだけである事を示した。つまりインクルードモデルであろうがセパレートモデルであろうがテンプレートライブラリの供給にソースコード(あるいは相当する何か)を必要とする。しかし我々は一般に公開するライブラリを書いているわけではない。コーディング省力化にテンプレートを大いに活用するが、テンプレートに与える型をあなたは完全に把握している。そうであるなら関数テンプレートの実装隠蔽は通常の関数と同じレベルでできる。自分が書いた実装を自分から隠蔽する必要があるかと問われれば、pimplイディオムをもって回答する。
サンプルコードのg(T)に与えるテンプレート実引数がintとdoubleだけとして、他は未定義リンクエラーとする。最初に関数テンプレートの明示的特殊化で定義を隠蔽する。インクルードファイル(X.h)のg(T)を(定義でない)宣言に留め、各型の明示的特殊化(定義でない)宣言を加える。ソースコードファイル(X.cpp)は各型の明示的特殊化定義する。同じコードを繰り返して少しも省力化にならないが、全ての型に対して実装が異なればそれなりに有効かもしれない。
次に関数テンプレートの明示的実体化で定義を隠蔽する。X.hのg(T)を(定義でない)宣言に留める。X.cppでg(T)を定義し、各型の明示的実体化定義を加える。明示的特殊化による定義隠蔽よりシンプルでこちらが本筋だろう。ある型で明示的特殊化が必要であればそれを明示的実体化の代わりに定義すれば良い。
なお関数テンプレート型推定のルール(N4659 17.8.2.1)には注意を払う。
疑似的なセパレートモデルでテンプレートをpimplイディオムに応用する。本筋の関数テンプレート明示的実体化を用いる。繰り返すがテンプレート実引数を未定とする場合に応用できないが、あなたのプロジェクトなのだからテンプレート実引数に与える全ての型をあなたは知っているはずだ。コーディングは通常のpimplイディオムと大きな差異は無く、実装するソースコードファイルにテンプレート実引数となる型の全てで明示的実体化定義を加える。さすがに明示的実体化定義を何十個も加えれば手間だが、サイト作成者の経験範囲で10個を上回ることは無かった。
最初にクラスがメンバ関数テンプレートを持つ場合を検討する。メンバ関数テンプレートは"関数を特殊化とするテンプレート化エンティティ"であるから、関数テンプレート特殊化として実装隠蔽できる。TMyTypeCheck1クラスはCheckメンバ関数テンプレートを持ち、これをpimplイディオムに沿って実装隠蔽する。Checkメンバ関数のテンプレート実引数に与える型はint、double、const char*として、それぞれをソースコードファイルで明示的実体化する。なおユーザー定義型はインクルードファイルなどで定義を供給する必要がある。
次にクラステンプレートの場合を検討する。クラステンプレート自体は隠蔽と無関係で、しかし全てのメンバ関数を実装隠蔽する。クラステンプレートメンバ関数は"関数を特殊化とするテンプレート化エンティティ"であるから、関数テンプレート特殊化として実装隠蔽できる。全てのメンバ関数をソースコードファイルで明示的実体化する必要があるが、これはクラステンプレート明示的実体化で一括できる(N4659 17.7.2/p8)。TMyTypeCheck2クラステンプレートのテンプレート実引数に与える型はint、double、const char*として、それぞれをソースコードファイルで明示的実体化する。なおユーザー定義型はインクルードファイルなどで定義を供給する必要がある。
疑似的なセパレートモデルのテンプレート明示的実体化定義をメタプログラミングを用いて型リストから自動化してみる。詳細はソースコードに譲るが、規格上不明確な点が多くmingw-w64以外の実装に互換できる確証もない。メタプログラミングの常としてパズル的な面白さはあるけれど、それぞれの明示的実体化定義を愚直に書き連ねる方が可読性やメンテナンス性からも良いと思う。