こんにちは。田原です。
前回は明示的特殊化を解説しました。明示的特殊化(完全特殊化)は型を確定してプライマリー・テンプレートとは異なる振る舞いを定義する機能でした。部分的特殊化は対応する型を完全に決めるのではなく、ある程度の範囲の型に対してプライマリー・テンプレートとは異なる振る舞いを定義する機能です。関数テンプレートには部分的特殊化がありませんが、関数テンプレート自体を複数オーバーロード定義することで似たようなことができます。今回はこれらの基本的な使い方を解説します。
1.クラス・テンプレートの部分的特殊化
冒頭に述べたように部分的特殊化は、型を完全に限定せずある程度の範囲に対してプライマリー・テンプレートとは異なるクラスを定義します。例えば次の例は「ポインタ型全て」について別途定義しています。
(typename.hは第2回で紹介した便利ツールです。)
#include <iostream> #include "typename.h" // プライマリー・テンプレート template<typename tType> struct Foo { Foo() {std::cout << "primary template : Foo<" << TYPENAME(tType) << ">\n";} }; // ポインタ型用の部分特殊化テンプレート template<typename tType> struct Foo<tType*> { Foo() {std::cout << "partial specialization : Foo<" << TYPENAME(tType) << "*>\n";} }; int main() { Foo<int> foo0; Foo<int*> foo1; Foo<short> foo2; Foo<short*> foo3; }
primary template : Foo<int> partial specialization : Foo<int*> primary template : Foo<short> partial specialization : Foo<short*>
template<typename tType>
の部分はプライマリ・テンプレートと全く同じように記述されていることがが多いので、つい「同じように書くべき」と思い込んでしまう人もいます(実は私のことです)が、全く違います。
まず、クラス・テンプレートを使う時に与えるべきテンプレート・パラメータの種類(*1)
や数は、プライマリ・テンプレートのtemplate<>
で定義します。上記の例では、Fooを使う時には型を1つ指定する必要があることをプライマリ・テンプレートのtemplate<typename T>
で定義しています。
(*1)テンプレート・パラメータの種類
テンプレート・パラメータには型パラメータ(typenameやclassを指定する)だけでなく非型パラメータ(int等の具体的な型を指定する)もあります。非型パラメータについては後日解説しますので、現在はテンプレート・パラメータには「型」でないものを指定できる「非型パラメータ」というパラメータもあると思って頂ければ十分です。
次に、部分特殊化のtemplate<>
のパラメータ・リストは、部分特殊化で使いたい仮の引数を記述します。こちらはプライマリ・テンプレートの仮引数とは無関係に定義できます。プライマリ・テンプレートより数が少ないことが多いですが、必要な場合はプライマリ・テンプレートより多くのテンプレート・パラメータを定義することもあります。
そして、そのままテンプレート名に続けて再度(?)パラメータ・リストを指定します。こちらはtemplate<>
のリストとは異なり、プライマリー・テンプレートで定義したテンプレート・パラメータの種類と順番通りに記述します。クラス・テンプレートを使う時の指定と部分特殊化で定義したものをコンパイラが見比べ、適合する部分特殊化を選択するのです。
言葉で書いても分かりにくいと思いますので、早速サンプルです。これはプライマリ・テンプレートにて2つの型パラメータを受け取るテンプレートを定義し、その指定した2つの型が同じだった場合と、指定した2つの型が同じポインタ型だった場合のそれぞれを部分特殊化しています。
#include <iostream> #include "typename.h" // プライマリー・テンプレート template<typename tType, typename tType2> struct Bar { Bar() { std::cout << "primary template : Bar<" << TYPENAME(tType) << ", " << TYPENAME(tType2) << ">\n"; } }; // 部分特殊化テンプレート(同じ型を2つ指定) template<typename tType> struct Bar<tType, tType> { Bar() { std::cout << "partial specialization1 : Bar<" << TYPENAME(tType) << ", " << TYPENAME(tType) << ">\n"; } }; // 部分特殊化テンプレート(同じポインタ型を2つ指定) template<typename tType> struct Bar<tType*, tType*> { Bar() { std::cout << "partial specialization2 : Bar<" << TYPENAME(tType) << "*, " << TYPENAME(tType) << "*>\n"; } }; int main() { Bar<int, short> bar0; Bar<int*, int> bar1; Bar<int, int> bar2; Bar<int*, int*> bar3; }
primary template : Bar<int, short> primary template : Bar<int*, int> partial specialization1 : Bar<int, int> partial specialization2 : Bar<int*, int*>
2.関数テンプレートのオーバーロード(その2)
第2回目で関数テンプレートをオーバーロードしました。前回、関数テンプレートの明示的特殊化を説明しました。この両者はほぼ同じことができますが、明示的特殊化は混乱しやすいのでオーバーロードの方が使い方が難しくないようです。
さて、これらの「オーバーロード」はテンプレートでない通常の関数でオーバーロードしてました。
そして、実にややこしいのですが、関数テンプレート自体もオーバーロード定義できます。
max()テンプレートを例にとってやってみます。
第2回目でmax(“abcde”, “fgh”)
の結果をそれぞれのアドレスが大きい方ではなく辞書順比較にて大きい方を返却させるために、max(char const* a, char const* b)
のオーバーロードを定義しました。
同様に、max関数でポインタの比較の際、ポインタの指すアドレスの大きい方ではなく、ポインタの指す先のデータの値が大きい方を返却するようにしてみましょう。
ただし、ポインタが指すのは配列ではなく1つのデータのみとします。
char型へのポインタ
C言語文字列はchar型配列でNULL終端するお約束があります。そのため、char型ポインタはchar型配列を指し、最後はNULL終端していると仕様で定めることは多いです。もちろん、単にchar型1つを指しているという決め方も可能です。
文字型以外へのポインタ
しかし、char型等の文字型(char, wchar_t, char16_t, char32_t)以外へのポインタにはそのような習慣も約束も存在しません。そして、配列の場合はその要素数を判定する仕組みが必要ですが、文字列以外はNULL終端的なお約束も存在しません。従って、int型等の文字型以外のポインタの場合、ポインタが指すのは配列ではなく要素1つだけと仕様を定めることもよく有ります。
まとめ
このようにポインタが配列を指すのか、要素1つを指すのかは、そのポインタを受け取る関数の仕様で決めます。文字型の場合はNULL終端する配列、それ以外の場合は要素1つを指すという決め方は良くある使い方に過ぎません。
#include <iostream> #include "typename.h" // 1つめのオーバーロードした関数テンプレート template<typename tType> tType max(tType a, tType b) { std::cout << "[0] " << TYPENAME(tType) << " max(" << TYPENAME(tType) << " " << a << ", " << TYPENAME(tType) << " " << b << ") -> "; return (a >= b)?a:b; } // 2つめのオーバーロードした関数テンプレート template<typename tType> tType* max(tType* a, tType* b) { std::cout << "[1] " << TYPENAME(tType) << "* max(" << TYPENAME(tType) << "* a, " << TYPENAME(tType) << "* b) -> "; return (*a >= *b)?a:b; } int main() { std::cout << max(11, 22) << "\n"; int x=123; int y=456; std::cout << *max(&x, &y) << "\n"; std::cout << max("aaa", "abcde") << "\n"; }
[0] int max(int 11, int 22) -> 22 [1] int* max(int* a, int* b) -> 456 [1] char* max(char* a, char* b) -> aaa
int型ポインタを渡した方は適切に動作しています。*max(&x, &y)
と頭に*
を付けているのはint型へのポインタが返却されるので、その中身を取り出すためです。
文字列を渡した方もうまく動作しているようにも見えますが、よく見ると変ですね。”aaa”と”abcde”は辞書順なら”abcde”の方が後ろになるので”abcde”の方を返却して欲しいです。
期待と逆になるのは、先頭の1つのみ比較していることが原因です。引数のaとbが等しい時はaを返却しているからこのようになります。
ですので、文字列は従来通りの比較が必要です。つまり、第2回目のオーバーロードか、第4回目の明示的特殊化が必要です。そこで、まずは安牌な通常関数でオーバーロードしてみます。
#include <iostream> #include "typename.h" // 1つめのオーバーロードした関数テンプレート template<typename tType> tType max(tType a, tType b) { std::cout << "[0] " << TYPENAME(tType) << " max(" << TYPENAME(tType) << " " << a << ", " << TYPENAME(tType) << " " << b << ") -> "; return (a >= b)?a:b; } // 2つめのオーバーロードした関数テンプレート template<typename tType> tType* max(tType* a, tType* b) { std::cout << "[1] " << TYPENAME(tType) << "* max(" << TYPENAME(tType) << "* a, " << TYPENAME(tType) << "* b) -> "; return (*a >= *b)?a:b; } // 通常関数でオーバーロード char const* max(char const* a, char const* b) { std::cout << "[2] " << "char const* max(char const* a, char const* b) -> "; return (std::string(a) >= std::string(b))?a:b; } int main() { std::cout << max(11, 22) << "\n"; int x=123; int y=456; std::cout << *max(&x, &y) << "\n"; std::cout << max("aaa", "abcde") << "\n"; }
[0] int max(int 11, int 22) -> 22 [1] int* max(int* a, int* b) -> 456 [2] char const* max(char const* a, char const* b) -> abcde
よっしゃ! 狙い通りうまくいきました。
ここで終わってもよいのですが、折角材料が揃っていますので、通常関数のオーバーロードではなく明示的特殊化でも対処してみました。前回の明示的特殊化をそのままコピペします。
#include <iostream> #include "typename.h" // 1つめのオーバーロードした関数テンプレート template<typename tType> tType max(tType a, tType b) { std::cout << "[0] " << TYPENAME(tType) << " max(" << TYPENAME(tType) << " " << a << ", " << TYPENAME(tType) << " " << b << ") -> "; return (a >= b)?a:b; } // 2つめのオーバーロードした関数テンプレート template<typename tType> tType* max(tType* a, tType* b) { std::cout << "[1] " << TYPENAME(tType) << "* max(" << TYPENAME(tType) << "* a, " << TYPENAME(tType) << "* b) -> "; return (*a >= *b)?a:b; } // 明示的特殊化 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; } int main() { std::cout << max(11, 22) << "\n"; int x=123; int y=456; std::cout << *max(&x, &y) << "\n"; std::cout << max("aaa", "abcde") << "\n"; }
[0] int max(int 11, int 22) -> 22 [1] int* max(int* a, int* b) -> 456 [1] char* max(char* a, char* b) -> aaa
あらら、明示的特殊化が呼ばれていません!!
コンパイラのバグ!? いいえ、残念ながら仕様です。
原則として、より限定されたテンプレートの方が優先的に選択されるのですが、その選択順序は、①まずプライマリ・テンプレート同士のオーバーロードのより適合する方が選択され、②次に選択されたプライマリ・テンプレートに明示的特殊化があればそれが試されます。
ところで、上記の明示的特殊化は、1つめのプライマリ・テンプレートの特殊化です。max<>()
の<>
の型と()
の型が一致していますので、T max<T>(T a, T b)
に一致するからです。
もし、template<> char const* max<
char const>(char const* a, char const* b)
と書けば(<>
の中がポインタでない)、T* max<T>(T* a, T* b)
に一致するので2つめのプライマリ・テンプレートの明示的特殊化となります。なかなか微妙ですね。
そして、max("aaa", "abcde")
の呼び出しはポインタ型を与えていますので、まずはより限定された2つめのプライマリ・テンプレートが選択されます。そして、こちらには明示的特殊化がないのでそのままプライマリ・テンプレートが採用されるのです。
Why Not Specialize Function Templates?に記載されている内容と同じです。
これに対して、通常関数でオーバーロードした場合は、通常関数と関数テンプレートでは先に通常関数の方をチェックします。従って、通常関数で適合したのであっさり選択されるのです。
とはいえ、実はchar*を与えると残念なことに1つめの関数テンプレートが選択されます。これは最初に通常関数のオーバーロードをチェックする時は全く同じ型に対してのみチェックし、それが適合しなければ関数テンプレートを探しに行くからなのです。実にこのあたりの選択アルゴリズムは複雑怪奇です。
Wandboxで見てみる。
“Why Not Specialize Function Templates?”を書いたHerb Sutter氏は、この記事の中で「Moral#2:複雑な関数テンプレートは書かないで、クラス・テンプレートのstatic関数を使った方が良い」と言ってます。
一度単純な関数テンプレートで受けてからクラス・テンプレートを呼び出せば型推論も使えますので、良い案かもしれません。そこで、max()関数テンプレートをクラス・テンプレートで書いてみました。
Wandboxで見てみる。
なお、残念ながら、この例は単にクラス・テンプレート化する例です。クラス・テンプレートにするとよりよく書ける例にはなっていません。関数テンプレートでも大差ない方法(通常関数のオーバーロード)で回避できます。難易度の高い話なので例を見つける難易度も高く思いつきませんでした。
3.まとめ
今回は、クラス・テンプレートの部分的特殊化と関数テンプレートのオーバーロードについて説明しました。
そして、関数テンプレートを特定の型について別定義したい時、オーバーロードと明示的特殊化ではオーバーロードの方が好ましい例を示すことができたと思います。(最初から狙っていたわけではなく、たまたまうまいことはまりました。)
ところで、そもそもオーバーロードは「同じ名前で別の関数を定義する」ことでした。テンプレートの部分特殊化や明示的特殊化も同様です。「同じ名前のまま別の定義をする」ことがその目的です。クラス・テンプレートはオーバーロードがないので話が単純ですが、関数テンプレートはオーバーロードできるのでオーバーロードと明示的特殊化のルールが絡み合い、更に型推論も絡むため非常に複雑です。ですので、関数テンプレートの複雑なオーバーロードを書いていると、「分岐地獄」を見るかも知れません。その地獄に入りかけたと感じたらクラス・テンプレートに載せ替えた方が却って速いかも知れませんね。
さて、次回からいよいよSFINAEを解説します。SFINAE(Substitution failure is not an error)は、テンプレートを選択する際に適合しなかったものはエラーにするのではなく単に無かったことにするだけの機能ですが、積極的に使うと型の範囲をもっと複雑で高度に指定することができます。例えば enum型とか、foo()というメンバ関数を持つクラスとか、更にそれらの組み合わせなどの複雑な指定ができます。
そのための定型的なパターンがありますので、その定形パターンを解説します。お楽しみに。