こんにちは。田原です。
前回、前々回とテンプレートは通常は暗黙的に実体化される話をしました。マクロでクラス・テンプレートっぽいことをする場合は「明示的実体化のようなもの」が必要ですが、テンプレートでは省略できます。しかし、明示的に実体化することもできますので、その必要性と使い方について解説します。実際に使うことは少ないですが、テンプレートを理解するために「実体化」についてきちんと把握しておくことは重要です。
1.一般の関数やクラスの場合
1-1.グローバル関数の場合
グローバル関数はヘッダ・ファイルには宣言だけを書き、関数の中身をcppファイルで定義することができます。(普通の書き方ですね。)
また、inline関数にすればヘッダ・ファイルで関数の中身を定義することができます。inlineを付けないとインクルードされる度に、その関数のマシン語コードが生成されるため、リンク時に多重定義(multiple definition)エラーになります。
1-2.クラスのメンバ関数の場合
クラスの場合も同様です。かなり前に解説しましたように、クラスのメンバ関数の中身はクラス内でも定義できますし、クラス外でも定義できます。(クラス外定義をヘッダ・ファイルに書く場合は1-1と同じ理由でinline指定が必要です。しかし、クラス内定義の場合は、わざわざinlineを付ける必要はありません。暗黙的にinlineになります。)
ところで、以前の解説で「メンバ関数の実体を定義」と表現してます。明示的実体化の「実体」とはちょっと違う意味で使ってますので説明します。
「メンバ関数の実体の定義」は「中身の定義」の意味です。プログラマが関数の中身を定義することです。
これに対して「明示的実体化」の「実体化」はコンパイラが行います。コンパイラが関数の中身のマシン語コードを生成することです。
さて、私自身は、メンバ関数の本質的に同じ宣言を2回書かないようにするため、問題ない時はクラス内で定義することが多いです。しかし、逆にヘッダ・ファイルでクラスを宣言し、メンバ関数の実体をcppファイルで定義する人も多いです。むしろ、こちらの方が多いかも知れません。必要が有るわけでもない時にこのようにするのはC言語時代からの習慣ではないかと思います。
クラス外で宣言した方が良い時はありますし、クラス外で宣言せざる得ない時もあります。
上記でリンクした第28回目でも述べたように、下記の3つが大きいと思います。
- 相互参照したい場合
相互参照が必要な時はクラス外定義する以外の策はありません。 - 多数のコンパイル単位でインクルードされる時
なるべくコンパイル時に処理する量を減らした方がコンパイルが速くなります。プログラムの生産性にも効いてきますので意外に重要です。例えば、そのほとんどをヘッダで定義しているメジャーなライブラリにboostがあります。インクルードすると多くの場合コンパイル時間が劇的に長くなります。これはこれで可能な時は避けたいものです。 - 商用ライブラリ等でなるべくユーザから実装を隠したい時
boostのようなオープン・ソースではありえませんが、商用ライブラリでは結構死活問題になります。
inlineについて補足
因みにinline関数は使う度にその場に展開されるのが「建前」です。実際、小さな関数は大抵そうなるようです。しかし、大きな関数は使ったその場に展開されず、普通にサブルーチンとして生成されるそうです。ですので、inlineを使ったからと言ってプログラムのオブジェクト・コードが巨大になる心配はないようです。
2.テンプレートの場合
上記の1., 2., 3.の条件は、テンプレートの場合でも当然成り立つ場合があります。
1.についてはクラス外定義部をヘッダ・ファイルとcppファイルのどちらでも宣言することができますが、2.と3.についてはクラス外定義部分をcppファイルで定義しないと意味がないです。
しかし、テンプレートの場合は一般のクラスや関数と違い、安易にcppファイルで関数の中身を定義することができません。
2-1.何故できないのか?
一言で言うと「暗黙的実体化」ができなくなるからです。
テンプレートは型が決まって初めて実体化できます。型が決まる前は関数の中身を具体的なマシン語に落とそうとしても、取り扱う変数のバイト数さえ決まっていないのでとてもできません。
例えば、前回出てきた下記の関数テンプレートで考えてみます。
template<typename tType> tType max(tType a, tType b) { return (a >= b)?a:b; }
2-1-1.関数の中身をヘッダ・ファイルで定義した場合
上記のようにヘッダ・ファイルで定義した場合の話です。
この関数テンプレートがmax(10, 20)
と呼ばれると10や20はint型なので型推論によりtTypeはint型と判断されa, bはint型なので、(a >= b)は(多くのPCでは)4バイトのレジスタにロードして大小比較するマシン語コードが生成されます。
そして、max(10.1, 20.2)
と呼ばれると10.1や20.2はdoubleなので型推論によりtTypeはdouble型と判断され,a, bはdouble型なので、(a >= b)は(多くのPCでは)浮動小数点処理用のレジスタにロードして大小比較するマシン語コードが生成されます。
2-1-2.関数の中身をcppファイルで定義した場合
例えば、下記のようにヘッダとcppに分けて記述した時の話です。
template<typename tType> tType max(tType a, tType b);
template<typename tType> tType max(tType a, tType b) { return (a >= b)?a:b; }
#include <iostream> #include "max.h" int main() { std:cout << max(10, 20) << "\n"; }
max(10, 20)
やmax(10.1, 20.2)
などの文とreturn (a >= b)?a:b;
の文は同じコンパイル単位に存在する必要があります。コンパイル単位毎にコンパイラは処理しますから、他のコンパイル単位に書かれた記述をコンパイラが知ることができないからです。
従って、このように別のコンパイル単位だった場合、max(10, 20)
を処理している時(main.cppをコンパイルしている時)、コンパイラはmax(a, b)の中身がreturn (a >= b)?a:b;
であることを知りません。ですので、「4バイトのレジスタにロードして大小比較するマシン語コード」を生成することができないのです。そこで、コンパイラは、max(10, 20)
をmax<int>(a, b)
のようなmax関数のtType=intバージョンへの呼び出しに変換します。
逆に、return (a >= b)?a:b;
を処理している時(max.cppをコンパイルしている時)、コンパイラはmax(10, 20)
として呼ばれることを把握できません。なので、aやbがint型として呼ばれていることが分からないため、具体的なマシン語コードを生成できないのです。
ならば、max.cppをコンパイルしている時、使用される可能性のある「全てのtType」について生成することができれば、max<int>(a, b)
も生成されるため呼び出しに成功しますね。
しかし、残念ながら、それは無理な相談です。tTypeは組み込み型だけでなくユーザ定義のクラスを指定することも可能です。operator>=
が定義されていれば処理できますから。そして、ユーザがどんなクラスを定義するかなんてコンパイラに分かる筈もありませんので「全ての型について」生成することは不可能なのです。
Wandboxで見てみる 未定義(undefined reference)エラーになります。
つまり、暗黙的実体化のためには、そのテンプレートを使う時の型が全て決まっており、かつ、その中身の定義も同じコンパイル単位に存在している必要があります。コンパイル単位を分けてしまうと「暗黙的実体化」に必要な条件が分離されるため、同時に満たさず「暗黙的実体化」できないということなのです。
3.明示的実体化の出番です
先程述べたように、max.cppをコンパイルしている時、使用される可能性のある「全てのtType」について生成すれば良いです。
しかし、それをコンパイラは知りませんでした。しかし、プログラマは知っている場合があります。
例えば、「max()はintとdoubleのみサポートします。Foo等のユーザ定義クラスを含め他の型はサポートしません。」などと事前に決めることができるような場合です。
このような時は、プログラマが型を指定して実体化するよう指示する(=明示する)事により関数の中身をcppファイルへ分離できます。
しかし、事前に決められないこともありますね。その時はすっぱりあきらめてヘッダ・ファイルで関数の中身を定義して下さい。残念ながら他に手はありません。本当に残念なのですが如何ともしがたいです。標準規格制定時にもかなり揉めたそうです。
明示的実体化は`template’に続けて型を指定して宣言を書きます。
3-1.関数テンプレートの明示的実体化
template int max<int>(int a, int b); template double max<double>(double a, double b);
明示的実体化は「関数の中身の定義」があるコンパイル単位に書きます。でないと関数の中身が分からないのでコンパイラが実体化できないからですね。
Wandboxで確認する
7行目のコメントアウトを外してみて下さい。これはlong型のmax(a, b)を呼び出します。long型のmax(a, b)は明示的実体化していないため、未定義エラーになります。
3-2.クラス・テンプレートのメンバ関数のクラス外定義
まず、先程のFooクラスをクラス・テンプレートにして、メンバ関数をクラス外定義してみました。
#include <iostream> template<typename tType> class Foo { tType mData; public: Foo(tType iData); }; template<typename tType> Foo<tType>::Foo(tType iData) : mData(iData) { std::cout << "Foo::mData=" << mData << "\n"; } int main() { Foo<int> foo0(10); Foo<double> foo1(123.4); Foo<double> foo2(100l); }
見ての通りクラス・テンプレートのメンバ関数のクラス外定義、面倒ですね。
通常のクラスのメンバ関数をクラス外定義する時は、関数名の前に「クラス名::」を書けば良かったのですが、クラス・テンプレートの場合は、「クラス名::」の部分を以下のように記述します。
- まずテンプレート・パラメータを書く
上記の例ではtemplate<typename tType>
の部分です。
テンプレート・パラメータが複数ある時はカンマで区切ります。
template<typename T, typename U, typename V>
- 次に「クラス名::」の代わりに、「クラス名<テンプレート・パラメタのリスト>::」と書く
上記の例ではFoo<tType>::
の部分です。
テンプレート・パラメータが複数ある時はBar<T, U, V>
のようにカンマで区切って書きます。
Wandboxで確認する
まずはクラス外定義をヘッダ内に書いてます。全てのコンパイル単位に「関数の中身の定義」があるので、上述したように明示的実体化は不要です。
通常のクラスではこのような使い方の場合、inline指定が必要でした。しかし、クラス・テンプレートの場合はODRの例外になるため、inline指定不要です。ODR(One Definition Rule)の例外についてはまた後日解説します。
3-3.クラス・テンプレートの明示的実体化
template int max<int>(int a, int b); template double max<double>(double a, double b);
Wandboxで確認する
今回はクラス外定義をcppファイル(foo.cpp)内に書いてます。main()関数のあるコンパイル単位には「関数の中身の定義」がないので、上述したように明示的実体化が必要になります。関数テンプレート同様、明示的実体化は「関数の中身の定義」があるfoo.cppに書きます。
4.まとめ
今回は、関数テンプレートとクラス・テンプレートの明示的実体化について解説しました。思ったより大掛かりになりました。あまり使わない機能なのでどうかとも思いましたが、テンプレートの意味合いを把握するために重要ですので頑張ってみました。
さて、次回からだんだん難しくなってきます。次回は「明示的特殊化」を解説します。なんとなく「明示的特殊化」があるなら「暗黙的特殊化」もありそうに感じますが、「暗黙的特殊化」はありません。このあたりは正直 命名ミスと感じます。その辺の話も含めて何回かに分けて解説したいと思います。お楽しみに。