こんにちは。田原です。
前回は明示的実体化について解説しました。今回は明示的特殊化について解説します。ちょっと似た用語ですので混乱しないようにして下さい。「実体化」と「特殊化」の違いです。またC++規格書は「特殊化」という用語をあまり直感的ではない使い方をしています。「明示的特殊化」があれば「暗黙的特殊化」もあると予想する人が多いと思いますがありません。他にも一般的な用法とは異なる使い方をされているため混乱しがちです。その辺りについても解説したいと思います。
1.明示的特殊化
明示的特殊化(Explicit specialization)は、最初に定義したテンプレート(プライマリ・テンプレートと呼びます)について、特定の型について異なる定義をできる仕組みです。
クラス・テンプレートと関数テンプレートのどちらとも、明示的特殊化をすることができます。
1-1.クラス・テンプレートの場合
まず簡単な「座標」をしまうクラス・テンプレートPointを定義します。(説明の都合上ヘッダ・ファイルで定義します。)そして、ちょっと便利なToString()メンバ関数を実装しました。
#include <string> #include <sstream> template<typename tType> class Point { tType mX; tType mY; public: Point() : mX(0), mY(0) { } Point(tType iX, tType iY) : mX(iX), mY(iY) { } std::string ToString() { std::stringstream ss; ss << "(" << mX << ", " << mY << ")"; return ss.str(); } };
#include <iostream> #include "point.h" int main() { std::cout << Point<int>(1, 2).ToString() << "\n"; }
(1, 2)
std::stringstream
ToString()関数でメモリ・ストリームstd::stringstreamを使ってます。C言語のsprintf()関数に当たります。
sprintf()と異なり、使う時、std::cout等と全く同じように使うことでメモリ・ストリームに出力され、出力された文字列をstr()メンバ関数で取り出せます。かなり便利ですので使う機会も多いと思います。ヘッダsstreamをインクルードして使います。
1-1-1.クラス・テンプレート全体を明示的特殊化する
さて、本当に小さな座標平面でPointを多量に使う時など、メモリ節約のためint型ではなくchar型を使うことにしましたので、ちょっとテストしてみます。
#include <iostream> #include "point.h" int main() { std::cout << Point<int>(1, 2).ToString() << "\n"; std::cout << Point<char>(48, 49).ToString() << "\n"; }
(1, 2) (0, 1)
なぜでしょう? (48, 49)
と表示されることを期待していたのに(0, 1)
と出てしまいました。
std::cout
はstd::ostream
クラスのインスタンスですが、char型用のoperator<<
はオーバーロードさており、受け取った値を文字として出力します。48, 49が文字コードを表していると解釈され、文字’0
‘のASCIIコードは48、文字’1
‘のASCIIコードは49ですので、文字’0
‘と’1
‘が出力されてしまったという落ちです。(私はデバッグの時に時々やらかして一瞬パニックになってしまいます。)
そこで、char型だけは特別扱いしてうまいこと表示できるようにしたいです。そのような時に「明示的特殊化」が有効です。明示的特殊化は元のテンプレートの振る舞いを特定の型に対して変更する(異なる定義をする)時に使います。
まずは、Pointクラス全体をchar型用に明示的特殊化します。(なお、実際の対策はいくつかありますが分かりやすいstatic_cast<int>()
を使ってint型に変換しています。)
template<> class Point<char> { char mX; char mY; public: Point() : mX(0), mY(0) { } Point(char iX, char iY) : mX(iX), mY(iY) { } std::string ToString() { std::stringstream ss; ss << "(" << static_cast<int>(mX) << ", " << static_cast<int>(mY) << ")"; return ss.str(); } };
(1, 2) (48, 49)
期待通りの結果になりました。ホッと一安心です。
ところで、クラスごと「明示的特殊化」する時はクラスの中身をごっそり入れ替えることができます。例えば、プライマリ・テンプレートには存在するメンバ関数を削除するとか、元々ないメンバ関数やメンバ変数を追加したりもできます。要するに明示的特殊化する時はほとんどなんでも有りです。
1-1-2.クラス・テンプレートのメンバ関数だけを明示的特殊化する
しかし、振る舞いを変えたいのはToString()関数だけです。なのでメンバ変数定義やコンストラクタはプライマリ・テンプレートとほぼ同じものをもう一回書いてます。面倒ですし、メンテナンス性が劣化します。
実は、クラス・テンプレートはメンバ関数だけを明示的特殊化することができます。今回の場合、ToString()だけ振る舞いを変えたいのでToString()だけを明示的特殊化すればよいです。
template<> inline std::string Point<char>:: ToString() { std::stringstream ss; ss << "(" << static_cast<int>(mX) << ", " << static_cast<int>(mY) << ")"; return ss.str(); }
1-1-3.明示的特殊化すると実体化される
分割コンパイルするため関数の定義を別cppに書きたい場合、テンプレートの時は明示的実体化が必要になる話をしました。実体化することで初めてマシン語のコードが生成され、その関数やクラスを使えるようになりますが、そのためにはテンプレート・パラメータの「型」を指定する必要がありました。
さて、明示的特殊化はそのテンプレート・パラメータの「型」を全て指定しつつ、定義を与えます。ということは型が決まっているので実体化できます。つまり自動的に実体化されます。と言うより実はクラス・テンプレートを明示的特殊化するということは、普通のクラスを定義するのとほぼ同じなのです。違いはクラス名をプライマリー・テンプレートと同じにものにできることです。これによりクラス・テンプレートを使う人が型名に合わせてクラス名を指定する苦労から解放されるのです。ありがたい機能です。
もし、明示的特殊化が無かった場合、char型に対するPointクラスの振る舞いが気に入らないなら、PointCharのようなクラスを定義して使うしかありません。その時、もし、間違ってPoint
更に他のテンプレートの中でPointテンプレートを使う時、char型のみ振る舞いを変えるためにその他のテンプレートまで別定義しないといけません。地獄ですね。
template<typename tType> class Line { Point<tType> mStart; Point<tType> mEnd; public: Line(Point<tType> iStart, Point<tType> iEnd) : mStart(iStart), mEnd(iEnd) { } // その他必要な定義 };
(SFINAEを使えばもう少しどうにかなるかもしれませんが、明示的特殊化さえない処理系にはSFINAEは期待できないと思います。SFINAEについては後日解説します。)
そして、テンプレートでない普通のクラスはその定義をヘッダに書いて複数のcppでインクルードしても多重定義にはなりません。
しかし、メンバ関数のクラス外定義をヘッダに書いて複数のcppでインクルードすると多重定義になります。その対策はinline関数として定義することでした。先述したようにクラス・テンプレートの明示的特殊化は普通のクラスを定義するのとほぼ同じですので、そのメンバ関数をクラス外定義する時は普通のクラスのメンバ関数のクラス外定義と同じようにマシン語コードが生成されますので、ヘッダに書いて複数のcppでインクルードすると多重定義エラーになります。そこで、1-1-2のメンバ関数の明示的特殊化はinline指定しています。
1-1-4.明示的特殊化したメンバ関数は明示的実体化無しで分割コンパイルできる
1-1-3に書いたように明示的特殊化するとそれだけで実体化されますから、明示的に実体化する必要はありません。
ただし、クラス・テンプレートの場合は、明示的に特殊化したクラスがどのような内容なのか、それを使っているコードをコンパイルしている時にコンパイラが知っている必要があります。どんなメンバ関数/変数があるのか分からないとマシン語コードを生成できないからです。ですので、クラス・テンプレートの宣言はヘッダに記述して必要なcppからインクルードする必要があります。
しかし、関数は呼び出せさえすればよいので中身を知らなくても使う側のコードをコンパイルできますから、cpp側に明示的特殊化を記述して分割コンパイルできます。その場合でも明示的実体化は必要ありません。
また、明示的特殊化は普通の関数定義と同等ですのでcpp側で定義する場合inline指定不要です。といいますか下手にinline指定するとそのコンパイル単位内で使われてない時、コンパイラが破棄してしいます。
#include <string> #include <sstream> template<typename tType> class Point { tType mX; tType mY; public: Point() : mX(0), mY(0) { } Point(tType iX, tType iY) : mX(iX), mY(iY) { } std::string ToString() { std::stringstream ss; ss << "(" << mX << ", " << mY << ")"; return ss.str(); } };
#include <iostream> #include "point.h" template<> std::string Point<char>::ToString() { std::stringstream ss; ss << "(" << static_cast<int>(mX) << ", " << static_cast<int>(mY) << ")"; return ss.str(); }
#include <iostream> #include "point.h" int main() { std::cout << Point<int>(1, 2).ToString() << "\n"; std::cout << Point<char>(48, 49).ToString() << "\n"; }
(1, 2) (48, 49)
ここでちょっと着目下さい。point.hには、Point<char>
の気配が全く含まれていません。main.cppをコーディングしている人は、point.hとpoint.cppを含むライブラリ・ファイルを持っていればmain.cppをビルドできます。
char型をoperator<<
で出力したら文字コードの値ではなく文字が出力されることを知っている人にとっては謎ですね。
提供されたライブラリのヘッダ・ファイルの詳しいところまで見る人は少ないと思いますが、テンプレートはこのような不思議なことができますので、そのような時はご注意下さい。
1-1-5.クラス・テンプレートのprivate/protectedメンバにアクセスできる
1-1-4のメンバ関数の明示的特殊化の特性から、当該クラス・テンプレートを使うプログラマが、そのメンバ関数を明示的特殊化することができます。そして、それはメンバ関数ですからprivate/protectedメンバにアクセスできます。
#include <iostream> #include "point.h" template<> std::string Point<int>:: ToString() { mX=123; mY=456; std::stringstream ss; ss << "(" << static_cast<int>(mX) << ", " << static_cast<int>(mY) << ")"; return ss.str(); } int main() { std::cout << Point<int>(1, 2).ToString() << "\n"; std::cout << Point<char>(48, 49).ToString() << "\n"; }
当たり前ですが、mX, mYに123, 456を代入しているため、以下のような結果になります。
(123, 456) (48, 49)
元々C++のprivateやprotected、const等の各種安全機能は、ポカミス避け(フールプルーフ)であってセキュリティ対策ではありません。悪意を持つプログラマからプログラムを防御することを目指しているものではありませんから、このように比較的簡単に回避できます。しかし、このような明示的実体化を「ポカミス」でやらかす人はいないでしょう。十分にフールプルーフとしての役割を果たします。
ですので、できるからと言って、フールプルーフを回避しprivateメンバにアクセスし、その結果ポカミスする「愚かな」プログラマにはならないようにしましょう。
1-2.関数テンプレートの場合
関数テンプレートもクラス・テンプレートと同じく特定の型についてプライマリ・テンプレートと異なる振る舞いをさせたい時に明示的特殊化をすることができます。
第2回目で使った関数テンプレートmaxで説明します。まずは明示的特殊化していないバージョンです。
#include <iostream> template<typename tType> tType max(tType a, tType b) { return (a >= b)?a:b; } int main() { std::cout << max(11, 22) << "\n"; std::cout << max(123.4, 567.8) << "\n"; std::cout << max("abcde", "fgh") << "\n"; }
22 567.8 abcde
“abcde”は”fgh”より、辞書順では「前」にありますから”abcde”の方が大きいと言うのはあまり一般的ではないですね。また、第2回目で解説しているようにこれは、単なる偶然であり処理系によっては振る舞いが異なりますし、同じ処理系でもソースをちょっと修正するだけで結果が変わる可能性もあります。第2回目はこの問題をオーバーロード関数で対処しました。char const*
を引数として受け取るmax関数を定義し、その中で辞書順で大小比較するようにしました。
今度は明示的特殊化で対処してみましょう。
オーバーロード関数の代わりに次の明示的特殊化を追加するだけです。
template<> char const* max<char const*>(char const* a, char const* b) { std::cout << "[2] " << "char const* max<char const*>(char const* a, char const* b) -> "; return (std::string(a) >= std::string(b))?a:b; }
微妙に宣言方法が異なるだけでオーバーロードとほとんど同じですね。1-1-3で述べたように明示的特殊化すると実体化され、それは普通にクラスを定義するのとほぼ同じことになります。関数の場合も同様なので、ほぼオーバーロードと同じなのです。
オーバーロードと明示的特殊化の両方が有る場合は「曖昧」にはならないでオーバーロードの方が採用されるとか、型が微妙に違った時にオーバーロードの方が呼ばれやすい等の微細な差はありますが大差はないようです。
私の知らない事情があるかもしれませんので、今teratailで質問してます。何か新しいことが判ったら、ここに追記します。
2017年10月24日追記
yohhoyさんからの回答で納得しました。回答で参照されているHerb Sutter氏の”Why Not Specialize Function Templates?“に説得力を感じました。まず、文脈的に「関数テンプレートの明示的特殊化」と「通常関数のオーバーロード」の機能に大差はないようです。そして、タイトル通り「関数テンプレートの明示的特殊化は使わない方がよい」ようです。最大の理由はその関数テンプレートを呼び出した時、明示的特殊化はオーバーロードより選択されにくいため、思わぬ振る舞いをすることがあるからです。
次回からの「クラス・テンプレートの部分特殊化と関数テンプレートのオーバーロード」の解説後に詳しいことを解説したいと思います。
2.テンプレート用語のややこしさ
「明示的特殊化」について解説しました。他に「部分的特殊化」があります。「明示的特殊化」と同様、特定の型に対してプライマリ・テンプレートとは異なる振る舞いをさせるため別途定義する機能ですが、「明示的特殊化」とは異なり型の指定が範囲を持ちます。例えば、ポインタ型全てに対して纏めて別途定義することができます。(詳しくは次回以降で解説します。)
これを踏まえて用語についての混乱を防ぐため少し説明します。
C++の標準規格では「特殊化」という用語を使っていますが、これは下記の2点でややこしいです。
- 「明示的特殊化」はあるけど「暗黙的特殊化」はありません。
-
字面より、「特殊化」は「明示的特殊化」と「部分的特殊化」のことを指すと思う人が大半ではないでしょうか? しかし、規格書では「特殊化」は「実体化」と「明示的特殊化」のことを指し、「部分的特殊化」を含みません。
1-1-3で説明したように「明示的特殊化」により「実体化」されますから、広義の「実体化」の意味で「特殊化」という用語を使っているようです。本当に混乱しやすいですのでご注意下さい。
図にすると以下の関係になります。
3.まとめ
今回は明示的特殊化について解説しました。テンプレートはたいへん便利です。そして、例外的に異なる振る舞いをして欲しい時に明示的特殊化ができるので非常に有用です。もし、異なる振る舞いを定義できなければテンプレートのありがたみが半減すると思います。
そのように大変有用なのですが、テンプレートに関しては何故か用語の使い方が正直可笑しいと思いますので、その辺も整理してみました。
「特殊化」を間違って「明示的特殊化」と「部分的特殊化」の意味で使っているサイトも少なくはないようです。また、私自身時々間違って「特殊化」をそのように使ってしまうこともあります。
そこで、当講座では混乱しやすい「特殊化」という用語を使わないよう気をつけたいと思います。もし、「特殊化」と使っていたら概ね「間違った」使い方の方をしていると思って頂けると助かります。すいません。
また、「明示的特殊化」もあまり的確な用語ではないと思います。”完全特殊化/Full Specialization”とも呼ぶ人もいます。私も必要に応じて「明示的特殊化」のことを「完全特殊化」と呼ぶと思います。
さて、次回から何回かに分けて「部分的特殊化」と「関数テンプレートのオーバーロード」について解説します。クラス・テンプレートは「部分的特殊化」できますが、関数テンプレートは「部分的特殊化」できません。その代わり「関数テンプレートのオーバーロード」が可能です。両者は似た役割を果たします。お楽しみに。