テンプレートの特殊化
テンプレートは、それ自体は関数やクラスではなく文字通り関数やクラスのテンプレート(ひな形)です。
テンプレートから実際に使用できる関数やクラスを生成することをテンプレートの特殊化といいます。
テンプレートの実体化
テンプレートは、呼び出し側からテンプレート仮引数に具体的なデータ型(型推論を含む)を与えることで関数やクラスの実体が生成され、その実体が実際に動作するプログラムの部品となります。
これをテンプレートの実体化といいます。
実体の生成は各データ型毎に行われ、例えばint
型を指定した場合とchar
型を指定した場合とでは異なる実体を生成します。
(同じデータ型で生成された実体がすでに存在する場合はそれが使用され、再度生成はされません)
テンプレートの実体化には暗黙的実体化(暗黙的インスタンス化)と明示的実体化(明示的インスタンス化)があります。
ここで言う「実体の生成」とはクラスのインスタンスの生成ことではありません。
クラステンプレートのインスタンス化は、テンプレートをコンパイルしてクラスそのものを作ることです。
クラスのインスタンス化は、クラスを実際に使用するためにメモリ上にデータを展開することです。
暗黙的実体化
暗黙的実体化は今まで普通に行ってきたもので、テンプレートの使用時に具体的なデータ型を与える方法です。
//関数テンプレート
template<typename T>
void func(T a){ /*省略*/ }
//クラステンプレート
template<typename T>
class C { /*省略*/ };
int main()
{
func<int>(123);
func("abc"); //型推論
C<int> a;
C<const char*> b;
}
多くの場合はこの方法で問題はありません。
しかしプログラムの規模がある程度大きくなってくると、ソースコードを分割して開発することになります。
そのときに様々なソースコード(.cppファイル)から使用されるテンプレートを定義しようとすると問題が起こる場合があります。
ファイルの分割と暗黙的実体化
例えば以下のような関数テンプレートを定義したとします。
template<typename T>
T max(T a, T b)
{
return a >= b ? a : b;
}
int main()
{
std::cout << max(1, 2);
}
これを複数のソースコードから利用できるようにヘッダーファイルとソースコードに分離してみます。
//max.h
template<typename T>
T max(T a, T b);
//max.cpp
template<typename T>
T max(T a, T b)
{
return a >= b ? a : b;
}
//main.cpp
#include <iostream>
#include "max.h"
int main()
{
std::cout << max(1, 2);
}
一見問題なさそうに思えますが、このコードは動きません。
コンパイルは通るのですが、リンク時にmax
という関数が見つからないというエラーが発生し、ビルド(実行ファイルの生成)ができません。
(リンク=複数のオブジェクトファイルを結合して実行ファイルを生成すること)
コンパイル処理はソースコード毎に行われます。
「main.cpp」と「max.cpp」とは別々にコンパイルされるのですが、このとき自分以外のファイルの内容を知ることはできません。
つまり、「max.cpp」がコンパイルされるとき、関数テンプレートmax
は外部からどのように呼びされているかを知ることはできません。
int型を指定して呼び出されているかもしれませんし、ユーザー定義クラスなどの組み込み以外の型が指定されているかもしれません。
あるいはまったく使用されていないかもしれません。
どの型で実体化すればいいのかを決めることができないので、「max.cpp」のコンパイル時には関数テンプレートは実体化されません。
「main.cpp」はヘッダファイル(max.h)をインクルードしているので関数テンプレートmax
の宣言は知ることができます。
しかしその定義(実装)を知ることができないため、「main.cpp」のコンパイル時にもやはり実体化することができません。
結果として関数テンプレートmax
は最後まで実体化されることがなく、実体の存在しない関数は実行できないのでビルドができないというわけです。
この問題は明示的実体化をすることである程度は解決できます。
明示的実体化
暗黙的実体化は、関数テンプレートを使用する時に呼び出し側からデータ型を指定(型推論を含む)して実体を生成します。
明示的実体化は、実際に呼び出される前にあらかじめ任意の型の実体を生成しておく方法です。
//max.cpp
template<typename T>
T max(T a, T b)
{
return a >= b ? a : b;
}
//明示的実体化
template int max<int>(int a, int b); //型の明示的指定
template double max(double a, double b); //型推論
template
キーワードに続いて、型引数を実際のデータ型に置き換えた形式の関数プロトタイプを記述します。
テンプレートの実体化はその実装を知る必要があるので、テンプレートの定義を記述したソースファイルと同じファイルに記述します。
上の例ではint
型版とdouble
型版のふたつの実体を生成しているので、「main.cpp」ファイルからはint
型版とdouble
型版のmax関数を使用することができるようになります。
他に必要なデータ型がある場合は同じようにして明示的実体化を追加します。
明示的実体化の欠点はあらかじめ実体化しておかないと使用できないという点です。
上述の通り、明示的実体化はテンプレートを定義したソースファイル内で行う必要があります。
つまり他のソースファイルから任意の型を指定して使用することができません。
特定のデータ型に依存しないというテンプレートのメリットがやや薄れることになります。
テンプレートをライブラリ化する場合、定義(実装)をソースコード(.cpp)ではなくヘッダファイル(.h/.hpp)に全て記述してしまう方法があります。
これならば他のソースファイルからも定義が見えるので、暗黙的実体化が可能です。
(多重定義を回避するためにinline化しておく必要があります)
ただしこの場合、そのヘッダファイルをインクルードしているソースコードをコンパイルする度にヘッダファイルの実装もコンパイルされます。
実装が大きくなるとコンパイルの時間も増えるので、関係のない箇所の修正でもコンパイル時間が長くなるというデメリットがあります。
また、外部から定義が見えてしまうので実装を隠蔽したい場合には使えません。
テンプレートの明示的(完全)特殊化
テンプレートはあらゆるデータ型に対して同一の処理を行います。
しかし特定のデータ型に対しては異なる動作をさせたい場合があります。
その場合はテンプレートの明示的特殊化(完全特殊化)という機能が使用できます。
例えば、数値の比較は単純な比較演算子(==
等)で可能ですが、C言語形式の文字列の比較は専用の関数を使用する必要があります。
これに対応するクラスは以下のように記述できます。
#include <iostream>
#include <cstring> //C言語のstring.hに相当するヘッダファイル
//クラステンプレート
template<typename T>
class TestClass
{
public:
T x;
TestClass(T x = T())
: x(x) {}
//==演算子による比較
bool operator==(const T& r) const
{
return x == r;
}
};
//テンプレートの明示的特殊化
//const char*型が指定された場合に使用されるクラス
template<>
class TestClass<const char*>
{
public:
const char* x;
TestClass(const char* x = nullptr)
: x(x) {}
//文字列比較関数による比較
bool operator==(const char* r) const
{
if (!x || !r)
return false;
return std::strcmp(x, r) == 0;
}
};
int main()
{
bool b;
TestClass<int> tc_int(1);
b = tc_int == 1; //true
b = tc_int == 2; //false
const char str1[] = "abc";
const char str2[] = "def";
TestClass<const char*> tc_str("abc");
b = tc_str == str1; //true
b = tc_str == str2; //false
}
テンプレートの明示的特殊化において、元となるテンプレートは一次テンプレート(プライマリテンプレート)といいます。
一次テンプレートの定義後に、template<>
を先頭に記述した新しいテンプレートを定義します。
<>
の中身は空で、これがテンプレートの明示的特殊化を表します。
これに続きテンプレート名<データ型>
の形式で特殊化したいデータ型を指定します。
(名前は一次テンプレートと同じものにします)
後はそのデータ型を用いたクラスの定義を行います。
ここではテンプレート仮引数(例ではT
型)は使用できませんが、そもそも一次テンプレートのテンプレート仮引数を具体的なデータ型に置き換えるのがテンプレートの明示的特殊化なので、テンプレート仮引数は必要ありません。
テンプレートの明示的特殊化は、関数のオーバーロードのように名前が同じの別のクラスや関数を定義するようなものです。
クラスの継承のように一次テンプレートの機能を引き継ぐようなことはありません。
一次テンプレートとは全く違う機能にすることもできますし、クラステンプレートの場合は一次テンプレートに存在しないメンバを追加したり、メンバを削除したりすることもできます。
しかし出来る限り一次テンプレートと同じ機能にすべきでしょう。
テンプレートの明示的特殊化は、実際に使用可能なクラスや関数を作るので実体化されます。
メンバ関数だけ明示的特殊化する
クラステンプレートの一部のメンバ関数だけを明示的特殊化することもできます。
例えば上記サンプルコードの等価演算子のオーバーロード処理だけを変更してみます。
#include <iostream>
#include <cstring>
template<typename T>
class TestClass
{
public:
T x;
TestClass(T x = T())
: x(x) {}
bool operator==(const T& r) const
{
return x == r;
}
};
//TestClassでconst char*型を指定した場合
//等価演算子のオーバーロードだけを明示的特殊化
template<>
inline bool TestClass<const char*>::operator==(const char* const& r) const
{
if (!x || !r)
return false;
return std::strcmp(x, r) == 0;
}
int main()
{
//省略
}
同じコードを二回書かなくて済むため、こちらの方が保守性が高くなります。
なお、明示的特殊化する際の関数宣言(引数等)は一次テンプレートと同じにする必要があります。
上記の場合、一次テンプレートの引数にconst
が指定されているので、明示的特殊化した関数でもconst
が必要になります。
const T&
型は「値を変更できないTの参照型」ですが、「値」とは参照元の値のことです。
T
にconst char*
型を指定する場合、「参照元の値」はポインタのことで、ポインタの先の値のことではありません。
const char*
型のconstはポインタの先の値に掛かっていますが、ポインタ自体は変更可能です。
(ポインタ演算が可能)
なので、一次テンプレートと同じにするにはconst char*&
型ではなくconst char* const&
型を指定します。
一次テンプレートがconstのないT&
型の場合はconst char*&
型で良いです。
関数テンプレートの明示的特殊化
関数テンプレートに対しても明示的特殊化が可能です。
#include <iostream>
#include <cstring>
//関数テンプレート
template<typename T>
T max(const T x, const T y)
{
return x > y ? x: y;
}
//char*版
template<>
char* max<char*>(char* x, char* y)
{
if (!x || !y)
return nullptr;
return std::strcmp(x, y) > 0 ? x : y;
}
//const char*版
template<>
const char* max<const char*>(const char* x, const char* y)
{
if (!x || !y)
return nullptr;
return std::strcmp(x, y) > 0 ? x : y;
}
int main()
{
char s1[] = "abc";
char s2[] = "def";
const char s3[] = "abc";
const char s4[] = "def";
const char* const s5 = "abc";
const char* const s6 = "def";
std::cout << max(1, 2) << std::endl; //通常版
std::cout << max("abc", "def") << std::endl; //const char*版
std::cout << max(s1, s2) << std::endl; //char*版
std::cout << max(s3, s4) << std::endl; //const char*版
std::cout << max(s5, s6) << std::endl; //const char*版
}
関数テンプレートの場合は引数の型からの型推論が可能なので、明示的にデータ型を指定せずに関数を呼び出すことができます。
(クラステンプレートではC++17から型推論が可能です)
上記コードを少し捕捉すると、const char* const
型を型推論により実行した場合、関数テンプレート側でもconst char* const
型の明示的特殊化の定義が必要になるように思われるかもしれません。
しかし型推論は値を修飾するconstを削除するため、上記コードはconst char*
型の定義が実行されます。
(→型推論#constや参照の削除)
std名前空間のクラステンプレートの特殊化
明示的特殊化は一次テンプレートに具体的なデータ型を与えた場合の動作を全て自分で定義するので、一次テンプレートの定義(実装)を知っている必要はありません。
そのため、実装が隠されている外部のライブラリが提供するクラステンプレートも特殊化が可能です。
例としてstd::hash
というC++標準ライブラリのクラステンプレートを特殊化してみます。
これはハッシュ値という値を計算するクラステンプレートです。
(ハッシュ値に関しては今は関係ないので気にしなくて良いです)
#include <iostream>
#include <functional> //std::hash用
class MyClass
{
public:
int x;
int y;
};
//std名前空間内で定義
namespace std {
//std::hashクラステンプレートの
//MyClassに対する明示的特殊化
template <>
struct hash<MyClass>
{
size_t operator ()(const MyClass& key) const
{
//ハッシュ値の計算
//ここは特殊化には関係ない処理なのであまり気にしなくて良い
std::hash<int> h;
return h(key.x) ^ h(key.y);
}
};
}
int main()
{
//int型を渡す
std::cout << std::hash<int>()(1) << std::endl;
//明示的特殊化を利用してMyClass型を渡す
std::cout << std::hash<MyClass>()({ 1, 2 }) << std::endl;
//int型のハッシュ値は処理系により異なる
}
4218009092 277333299
//C++11以降は以下でもOK
template <>
struct std::hash<MyClass>
{
size_t operator ()(const MyClass& key) const
{
std::hash<int> h;
return h(key.x) ^ h(key.y);
}
};
std::hash
は、int型やポインタ型などの組み込み型に対してはあらかじめ特殊化が提供されていて、何もしなくても使用できます。
ユーザー定義のクラスに対する処理はないので自分で実装するのですが、逆に言えば標準ライブラリ内の実装を知らなくても(実装が見える箇所に記述しなくても)良いということです。
なお、std::hash
に具体的な型を与えて得られるのは関数オブジェクトです。
なので、
std::cout << std::hash<int>()(1) << std::endl;
上記はまずstd::hash<int>()
でクラステンプレートをint型で特殊化した実体(関数オブジェクト)を取得し、次の丸括弧(1)
で関数オブジェクトに引数を指定して実行し、結果を出力する、という意味になります。
std::hash
はstd
名前空間にあるので、明示的特殊化も同じstd
名前空間の中で行います。
std
名前空間に何かを追加することは本来はできないのですが(未定義の動作)、std
名前空間内のクラステンプレートの特殊化を追加することは認められています。
テンプレートの部分特殊化
テンプレートの明示的特殊化は、テンプレートに特定のデータ型を与えた時の動作を定義するものです。
これは一次テンプレートと同じ名前の、(具体的なデータ型を使用した)別のクラスや関数を定義するのと同じといえます。
一次テンプレートの抽象的な箇所の全てを具体的に定義するので完全特殊化とも呼ばれます。
(定義にあいまいな箇所が無くなるので実体化ができる)
これに対して、テンプレートの部分特殊化というものもあります。
これは一次テンプレートの一部だけを具体的に定義します。
例えば、テンプレート引数T
型のポインタ型T*
を指定した場合や、テンプレート仮引数が複数ある場合にその一部だけに具体的なデータ型を当てはめた場合の処理を定義することです。
部分特殊化は一次テンプレートの一部だけを具体化します。
抽象的な(データ型が指定されない)部分が残るため、テンプレートの実体化はできず、部分特殊化はクラステンプレートから新しいクラステンプレートを作ることと言えます。
部分特殊化されたクラステンプレートを使用する段階で全てに具体的な型が与えられ、実体化されます。
そのため、部分「特殊化」という名前ではありますが、実際に使用できるクラスを生成するわけではないので正確にはテンプレートの特殊化には含められません。
なお、「明示的特殊化」はありますが「暗黙的特殊化」はありません。
テンプレートの部分特殊化はクラステンプレートに対してのみ可能で、関数テンプレートには適用できません。
任意の型のポインタ型の部分特殊化
任意の型T
に対して、T*
型で部分特殊化する例です。
#include <iostream>
//一次テンプレート
template<typename T>
class TestClass
{
T x;
public:
TestClass(T x = T())
: x(x) {}
void PrintValue() const
{
std::cout << x << std::endl;
}
};
//部分特殊化
template<typename T>
class TestClass<T*> //「T」のポインタ型を与えた場合のテンプレート
{
T* x;
public:
TestClass(T* x = nullptr)
: x(x) {}
void PrintValue() const
{
if(!x)
std::cout << "(null pointer)" << std::endl;
else
std::cout << *x << std::endl;
}
};
int main()
{
int n = 123;
TestClass<int> tc1(n);
TestClass<int*> tc2(&n);
TestClass<int*> tc3;
tc1.PrintValue(); //123
tc2.PrintValue(); //123
tc3.PrintValue(); //(null pointer)
}
テンプレート仮引数(template<typename T>
)と、クラス名に続いて<データ型>
を指定すると、そのクラスは部分特殊化されたクラスとなります。
上記コードは、ポインタ型が指定された場合はNULLチェックを行い、ポインタが参照する値を表示するように部分特殊化しています。
テンプレート仮引数の一部を部分特殊化
複数のテンプレート仮引数がある場合に、その一部だけを具体的な型で部分特殊化する例です。
#include <iostream>
#include <string>
//一次テンプレート
template<typename T, typename U>
class TestClass
{
T x;
U y;
public:
TestClass(T x, U y)
: x(x), y(y) {}
void PrintValue() const
{
std::cout << x << " : ";
std::cout << y << std::endl;
}
};
//部分特殊化
template<typename U>
class TestClass<std::string, U> //「T」にstd::stringを与えた場合のクラス
{
std::string x;
U y;
public:
TestClass(std::string x, U y)
: x(x), y(y) {}
void PrintValue() const
{
if (x.empty())
std::cout << "(empty)";
else
std::cout << x;
std::cout << " : ";
std::cout << y << std::endl;
}
};
int main()
{
TestClass<int, int> tc1(0, 1);
TestClass<std::string, int> tc2("abc", 1);
TestClass<std::string, int> tc3("", 1);
tc1.PrintValue(); //0 : 1
tc2.PrintValue(); //abc : 1
tc3.PrintValue(); //(empty) : 1
}
テンプレート仮引数を増やす部分特殊化
一次テンプレートからテンプレート仮引数を増やす部分特殊化の例です。
#include <iostream>
//一次テンプレート
//複数の要素を動的配列(ポインタ)で管理する
template<typename T>
class TestClass
{
T* x;
const size_t SIZE;
public:
TestClass(size_t size)
: x(new T[size]), SIZE(size) {}
~TestClass() {
delete[] x;
}
const T& operator[](size_t index) const
{
return x[index];
}
T& operator[](size_t index)
{
return x[index];
}
void PrintValue() const
{
for (size_t n = 0; n < SIZE; n++) {
std::cout << x[n] << ", ";
}
std::cout << std::endl;
}
};
//部分特殊化
//複数の要素を通常の(静的)配列で管理する
template<typename T, size_t SIZE>
class TestClass<T[SIZE]>
{
T x[SIZE];
public:
const T& operator[](size_t index) const
{
return x[index];
}
T& operator[](size_t index)
{
return x[index];
}
void PrintValue() const
{
for (size_t n = 0; n < SIZE; n++) {
std::cout << x[n] << ", ";
}
std::cout << std::endl;
}
};
int main()
{
const size_t SIZE = 5;
TestClass<int> tc1(SIZE);
TestClass<int[SIZE]> tc2;
for (size_t n = 0; n < SIZE; n++) {
tc1[n] = n + 1;
tc2[n] = n + 1;
}
tc1.PrintValue(); //1, 2, 3, 4, 5,
tc2.PrintValue(); //1, 2, 3, 4, 5,
}
一次テンプレートの方は要素T
を動的配列で管理しています。
テンプレートの部分特殊化の方は、要素T
を通常の配列で管理しています。
あらかじめ要素数が分かっている場合は高速な処理が期待できます。