クラステンプレートの使い方 - C++ プログラミング
PROGRAM
クラステンプレートを使う
C++ では、任意の型を扱えるように汎用化したクラスを実装できる "クラステンプレート" という機能があります。
これを使用することで、ひとつの型にとらわれない "テンプレートクラス" を定義して使うことができます。
テンプレートクラスで有名なのが、たとえばスタックを実現するクラスです。
スタックというのは、複数の値を格納できるデータ構造で、最後に格納したデータから順に取り出せるという特徴があります。具体的には、push という機能で値を最後尾に入れ、pop という機能を使って値を最後尾から取り出します。
スタックはそういった仕組みのことを言うのであって、データ型には言及していません。
この、スタックを使ったデータ構造を表現するクラスを作りたいとき、通常のクラス定義であれば、次のどれかの方法を使って実装して行くのが基本的な考え方でした。
- あらかじめ、扱う型を 1 つに限定したスタッククラスを実装する。
- ほとんど同じ実装を持ったスタッククラスを、必要な型の数だけ実装する。
- 汎用的な void* ポインタを扱うスタッククラスを実装して、使う側が適切な型にキャストする。
たとえば void* 型を扱うスタッククラスであれば、プロトタイプ宣言は次のような雰囲気になります。
// void* 型を扱うスタッククラスのプロトタイプ宣言
class CEzStackVoid
{
private:
std::unique_ptr<void*[]> m_values;
size_t m_index;
public:
CEzStackVoid();
CEzStackVoid(const CEzStackVoid& stack);
CEzStackVoid(CEzStackVoid&& stack);
void push(void* value);
void* pop();
}
ただしこのような void* 型だと、間違った型の値を使って操作していないかをコンパイラにチェックをしてもらえなくなるので、より信頼性の高いプログラムを組むには、void* 型ではなく、厳密な型を定義することが不可欠です。
ただしそのような場合、スタッククラスの実装内容はほとんど同じでも、型毎にスタッククラスを定義しないといけないとなると、実装の手間も然ることながら、バグが見つかったり機能追加したりしたいときに各型用のスタッククラス全てを保守しなければいけなくて、効率的とはとても言えません。
そんなときにクラステンプレートを使うと、複数の型に対応したクラスを 1 つの実装を用意するだけで実現できます。
なお、クラスではなく関数について似たいようなことをしたい場合は 関数テンプレート という機能を使います。
テンプレートクラスを定義する
テンプレートクラスのプロトタイプ宣言:型パラメータを使った一般的なもの
クラステンプレートでは、冒頭で template キーワードを使ってクラスを定義します。
template キーワードでは <> を使って、その中で class キーワードに続けて、関数内で登場するデータ型を、任意の名前で指定します。複数のデータ型を扱いたい場合は、class キーワードとデータ型で表す型パラメータを必要な数だけカンマ区切りで指定します。
// クラステンプレートを使うと、扱うデータ型だけが違うクラスを 1 つ定義するだけで済みます。
template<class T>
class CEzStack
{
private:
std::unique_ptr<T[]> m_values;
size_t m_index;
public:
CEzStack();
CEzStack(const CEzStack& stack);
CEzStack(CEzStack&& stack);
void push(const T& value);
T& pop();
};
後は template キーワードで始まる行に、後は続けて普段通りのクラスを実装するだけで、template キーワードのところで指定した型名(ここでは T という名前)を使ったクラスを定義することができます。
class キーワードで定義した名前は、変数名ではなくデータ型名なので、この名前はデータ型のところに登場するのが、普段の関数定義とは感覚が違ってくるところです。
テンプレートクラスのプロトタイプ宣言:非型パラメータを合わせて使う場合
クラステンプレートでは、データ型を示す型パラメータだけでなく、任意のデータ型の値を扱う非型パラメータも指定できます。
非型パラメータは、class キーワードの代わりに具体的なデータ型を明示して、それに付ける名前を指定します。それを型パラメータと同じように template キーワードの <> の中に、必要な数だけ宣言します。
たとえば、スタッククラスで登録できる値の個数を制限したいとします。
その時は、扱いたい型を class キーワードで型パラメータとして宣言するのに加えて、最大数を指定するための非型パラメータをここでは size_t 型で宣言しています。
// テンプレートクラスには、型パラメータだけでなく、性質を表す値を非型パラメータとして定義することもできます。
template<class T, size_t maxValues>
class CEzStackLimited
{
private:
T m_values[maxValues];
size_t m_index;
public:
CEzStackLimited();
CEzStackLimited(const CEzStackLimited& stack);
CEzStackLimited(CEzStackLimited&& stack);
void push(const T& value);
T& pop();
};
こうすると、テンプレートクラスの定義で、型パラメータ "T" と合わせて非型パラメータ "maxValues" が使えるようになるので、これらを組み合わせて目的の機能を持ったクラスを実装して行きます。
ちなみにこの非型パラメータでは、関数の引数のように既定値を指定することもできます。
// テンプレートクラスでは、非型パラメータに既定値を定義することもできます。
template<class T, size_t maxValues = 10>
class CEzStackLimited
{
テンプレートクラスの実装
テンプレートクラスの実装方法は、通常のクラスのときとほとんど変わりません。
ただし、プロトタイプ宣言と同じように各メンバ関数の定義のところでも冒頭に必ず template キーワードが必要なことと、スコープ解決演算子 (::) の左に記載するクラス名では "CEzStack<T>" というようにパラメータの名称を付けないといけないところに注意します。
たとえば、先ほどの例で挙げた CEzStack テンプレートクラスの push 関数を実装する場合は、次のような記載になります。
// テンプレートクラスのメンバ関数の実装時にも、ひとつひとつに template キーワード指定が必要です。
template<class T>
void CEzStack<T>::push(const T& value)
{
// ここで必要な実装を行います。
};
非型パラメータを取るテンプレートクラスの実装についても同様で、たとえば先ほどの例で挙げた CEzStackLimited テンプレートクラスの push は次のような記載になります。
// テンプレートクラスのメンバ関数の実装時にも、ひとつひとつに template キーワード指定が必要です。
template<class T, size_t maxValues>
void CEzStackLimited<T, maxValues>::push(const T& value)
{
// ここで必要な実装を行います。
};
もちろん、非型パラメータである "maxValues" を実装内で値として使えるので、それを踏まえたプログラムを組むことができます。
テンプレートクラスを使う
定義したテンプレートクラスは、各パラメータを <> 内で指定することで、それをデータ型として使用できます。
たとえば CEzStack テンプレートクラスを int 型の値を扱うクラスとして使いたいときは、CEzStack の第一引数の型パラメータを int にした "CEzStack<int>" がそのデータ型になります。
それだけ注意すれば、後は通常のクラスと同じようにインスタンス化して使用できます。
// テンプレートクラスは、宣言時に型パラメータを指定してクラスの型を完成させます。
CEzStack<int> intStack;
// クラス型を定めてインスタンスを作成したら、後は通常通りの使い方でクラスを操作できます。
intStack.push(100);
int value = intStack.pop();
このとき、同様にしてたとえば CEzStack<float> というテンプレートクラスも使うことができますが、これと CEzStack<int> とは別のクラスとして扱われるので、キャストなどでこれらを相互に変換することはできません。
非型パラメータを取るクラスについても同様です。
// テンプレートクラスは、宣言時に型パラメータを指定してクラスの型を完成させます。
CEzStackLimited<double, 10> doubleStack;
// クラス型を定めてインスタンスを作成したら、後は通常通りの使い方でクラスを操作できます。
doubleStack.push(100.0);
double value = doubleStack.pop();
このようにして決定したテンプレートクラスの型は、そのインスタンスを別の変数で扱うときには、同じパラメータを使って受ける必要があります。
たとえば int 型を扱う CEzStack を引数に取る関数を宣言する場合は、次のように、型パラメータまで含めた型を引数に取るようにします。
// テンプレートクラスを変数に受ける場合は、パラメータで指定する内容も合致している必要があります。
void function(const CEzStack<int>& stack);
このようにすることで、CEzStack<int> 型のインスタンスを引数に取ることができます。
ここにもし CEzStack<double> 型のインスタンスを渡そうとすると、型が一致しないということで、コンパイルエラーが発生します。
テンプレートクラス実装に関する細かな制御
メンバ関数テンプレートを定義する
テンプレートクラスでは、メンバ関数毎にも型パラメータを定義できるようになっています。
これを "メンバ関数テンプレート" といいます。
たとえば、テンプレートクラス内で任意の型を引数に取って計算するメンバ関数 popAndDividingBy 関数を用意したいとします。
このときプロトタイプ宣言では、目的の関数を宣言するところで、テンプレートクラスで使用している型パラメータとは違う名前のパラメータを template キーワードを使って指定します。
// テンプレートクラスでは、任意の引数を取るメンバ関数を定義することもできます。
template<class T>
class CEzStack
{
public:
template<class U>
U popAndDividingBy(U value);
};
このように template キーワードを入れ子のように使って定義できます。
このとき、メンバ関数テンプレートは、引数に渡された値に応じて適用される型パラメータの型を判断するので、引数を取らないメンバ関数テンプレートを使うことはできません。
メンバ関数テンプレートの実装は、関数定義の冒頭で、テンプレートクラスの template 宣言とメンバ関数テンプレートの template 宣言を連ねて記載します。
// 関数テンプレートを使うと、扱うデータ型だけが違うクラスを 1 つだけ定義するだけで済みます。
template<class T> template<U>
U CEzStack<T>::popAndDividingBy(U value)
{
return this->pop() / value;
};
このようにすることで、引数に指定された値の型が、型パラメータ U に適用されます。
typename キーワードを使う
クラステンプレートには "typename" というキーワードがあります。
これを使うと、型パラメータで表現されたデータ型がさらにデータ型を入れ子で持っているとき、それをプログラム内で使うときに、それがデータ型であることを明示することができます。
たとえば、型パラメータ T で表現したクラスが iterator というローカル定義のデータ型を持っていたとします。
そのとき、通常のプログラムであれば次のような感じで使うことになります。
// たとえば、ポインタ型の変数宣言
T::iterator* p;
// たとえば、ローカル定義のデータ型を返す関数の定義
T::iterator begin();
ただ、テンプレートクラスの場合、ここの T で表現された型パラメータがどんな型かは、具体的な値が指定されるまでは判らないので、その判らない T が持っている iterator といっても、それが型なのか、静的メンバ関数なのか、定数なのか、何なのか判りません。
そこで、T::iterator がデータ型を示すものであるということを明示するために、この typename キーワードを使います。
// たとえば、ポインタ型の変数宣言
typename T::iterator* p;
// たとえば、ローカル定義のデータ型を返す関数の定義
typename T::iterator begin();
このようにすることで、T が何者か判らなくても、T::iterator がデータ型であることが明記されるので、コンパイラが適切にそれを扱うことができるようになります。
ちなみに、この typename キーワードは、template キーワードの <> の中でも使用できます。
この場合は、上記のときとは意味が変わって、単純に class キーワードとまったく同じ意味合いになります。
テンプレートクラスを特殊化する
特定の型を別実装にする
基本的にはテンプレートクラスの通りの実装とするけれど、ある特定のデータ型については特別な実装をしたい場合があります。
そういうときには、型パラメータをある型に限定することで、その型専用の実装を別に定義することができます。
限定するのに使う型は何でもよくて、int 型でも double* 型でも const char* 型でも、果てには void 型でもよかったりします。
型を限定すれば、後の実装はプログラマ任せになるので、戻り値の型をわざわざ限定した型に合わせる必要もなければ、限定していないテンプレートクラスの方で定義してあるメソッドを全て実装する義務もありません。
そのため、たとえば CEzStack テンプレート関数を void 限定で指定して、全ての値を飲み込むスタックなんていうものも出来たりします。
型で特殊化する場合のプロトタイプ宣言
型限定のテンプレートクラスのプロトタイプ宣言は、template キーワードを <> というように型パラメータなしで宣言して、次のクラスの定義のところで <> に限定する型を直接指定します。
// 例えば void に特化した、全てを飲み込むテンプレートクラスを定義してみます。
template<>
class CEzStack<void>
{
public:
CEzStack();
CEzStack(const CEzStack<void>& stack);
template<class U> void push(U value);
void* pop();
};
ちなみに余談になりますけど、ここで push 関数をメンバ関数テンプレートとして定義しているのは、どんな型でも受け入れられるようにするためです。
型で特殊化する場合の実装
このようにして定義した特殊化されたテンプレートクラスは、実装のときには template キーワードを付けずに、指定した型を <> に直接書いて指定します。
今回の例であれば、自分自身のクラス名を CEzStack<void> というように記載します。
CEzStack<void>::CEzStack()
{
}
CEzStack<void>::CEzStack(const CEzStack<void>& stack)
{
}
template<class U>
void CEzStack<void>::push(U value)
{
}
void* CEzStack<void>::pop()
{
return nullptr;
}
こうすることで void に特化したテンプレートクラスが出来上がりました。
これで、CEzStack<void> として宣言した変数ではこの実装が、それ以外のたとえば CEzStack<int> として宣言した変数では通常のテンプレートクラスの実装が、コンパイラが自動的に使い分けてくれるようになります。
ちなみに、特殊化したテンプレートクラスのメンバ関数テンプレートの実装では、template キーワードをメンバ関数テンプレートを宣言したときに明示した 1 つだけを記載すれば大丈夫です。
普通のテンプレートクラスと違って、特殊化したテンプレートクラスでは、クラス自身の template キーワード指定が不要なため、メンバ巻子テンプレートでもちょうどその分が記載されない形になります。
型で特殊化したテンプレートクラスを使う
型で特殊化したテンプレートクラスも、通常のテンプレートクラスと同じ方法で宣言して使うことができます。
// 通常のテンプレートクラスは、宣言時に型パラメータを指定してクラスの型を完成させます。
CEzStack<int> intStack;
// 型で特殊化したテンプレートクラスも同様です。指定した型が特別に定義されている場合はそれが自動で採用されます。
CEzStack<void>.voidStack;
このように同じ書き方で宣言をしても、コンパイラによって適切なクラスが選択されます。
もちろん定義によっては内部実装や定義されている関数が違ってくる場合があるため、その場合は、宣言の仕方は同じでもその後の使い方は違ってきます。
また、これらのクラスは別物なので CEzStack<int> と CEzStack<void> を相互に変換することはできません。
特殊化する型の、継承関係は無関係
テンプレートクラスを型で特殊化する場合、それがクラス型やクラスポインタ型であっても、指定された型以外は考慮されません。
つまり、たとえば指定したクラス CMyClass を継承した派生クラス COutClass があったとして、テンプレートで CEzStack<CMyClass*> が特殊化されていたとしても、CEzStack<COurClass*> を使ったときに、特殊化された CEzStack<CMyClass*> は採用されません。
CEzStack<COurClass> が別途定義されていない場合は、あくまでも標準の CEzStack<T> が採用されます。
もっとも、COurClass が CMyClass から派生されているのであれば、基底クラスのポインタで特殊化された CEzStack<CMyClass*> で変数宣言して、そこに COurClass のポインタを CMyClass として格納させることはできます。
クラス型を使ってテンプレートクラスを特殊化する場合は、このように派生クラスをどう扱いたいかを考慮しながら、実装していく必要があります。
値型とポインタ型を別実装にする
普段の変数でも、値と参照とではずいぶんと扱い方が違ってきます。
クラステンプレートでも部分特殊化の機能を使えば、その違いを考慮して、それぞれで別の実装をする仕組みが用意されています。
たとえば、次のようなテンプレートクラスが定義されているとします。
// 通常のテンプレートクラスです。
template<class T>
class CEzStack
{
private:
std::unique_ptr<T[]> m_values;
public:
CEzStack();
CEzStack(const CEzStack& stack);
void push(const T& value);
T& pop();
};
このとき、ポインタが渡されたときに別の実装を適用したい場合には、基本的な書き方は変えずにクラス名を CEzStack<T*> というように、型パラメータをポインタで取ることを明示して定義します。
部分特殊化する場合のプロトタイプ宣言
// ポインタに限定したテンプレートクラスです。
template<class T>
class CEzStack<T*>
{
private:
std::unique_ptr<T*[]> m_values;
public:
CEzStack();
CEzStack(const CEzStack<T*>& stack);
void push(T* value);
T* pop();
};
このように、クラス名を CEzStack<T*> にすることで、型パラメータがポインタ型だった場合のテンプレートクラスを簡単に実装することができます。コピーコンストラクタのところも、自分自身を表現するのに CEzStack<T*> を使っています。
もちろん、他の CEzStack<> テンプレートクラスとは独立して存在するので、実装する関数の名前や戻り値などを合わせる必要はありません。
ここで注意したいのは、このとき型パラメータ T には、たとえば CEzStack<int*> なら int が設定されるので、ポインタを返したい場合は "T*" とする必要があります。逆に値を受け取りたいところでは "T" にします。
これまでのテンプレートクラスの場合は、int でも int* でも、指定された型が型パラメータに設定されていたので、混乱しやすいところです。
部分特殊化する場合の実装
実装のときも考え方はまったく同じで、スコープ解決演算子 (::) で指定するクラス名を CEzStack<T*> というようにポインタ指定で記載します。
template<class T>
CEzStack<T*>::CEzStack()
{
}
template<class T>
CEzStack<T*>::CEzStack(const CEzStack<T*>& stack)
{
}
template<class T>
void CEzStack<T*>::push(T* value)
{
}
T* CEzStack<T*>::pop()
{
}
注意点としては、template キーワードでは "<class T>" というように、ここはポインタ型にはしません。
自分自身のクラス名を指定したり、引数や戻り値の型としてポインタを使いたいときには "T*" というようにポインタを示す記号を付ける必要があるので、ここを混同しないように注意する必要があります。
部分特殊化したテンプレートクラスを使う
部分特殊化したテンプレートクラスも、通常のテンプレートクラスと同じ方法で宣言して使うことができます。
// 通常のテンプレートクラスは、宣言時に型パラメータを指定してクラスの型を完成させます。
CEzStack<int> intStack;
// 部分特殊化したテンプレートクラスも同様です。指定した型の扱いが特別に定義されている場合はそれが自動で採用されます。
CEzStack<int*>.pIntStack;
このように同じ書き方で宣言をしても、コンパイラによって適切なクラスが選択されます。
もちろん定義によっては内部実装や定義されている関数が違ってくる場合があるため、その場合は、宣言の仕方は同じでもその後の使い方は違ってきます。
また、これらのクラスは別物なので CEzStack<int> と CEzStack<int*> を相互に変換することはできません。
部分特殊化できるパターン
テンプレートクラスの部分特殊化は、ポインタ型だけでなく、参照や右辺値参照、読み取り専用についても行えます。
- template<T>
class CEzStack<T*> - template<T>
class CEzStack<T**> - template<T>
class CEzStack<T&> - template<T>
class CEzStack<T&&> - template<T>
class CEzStack<const T*> - template<T>
class CEzStack<const T&>
このようにさまざまなものが区別して定義できるので、幅広いケースにも対応できます。
ちなみに template<T> class CEzStack<T> という記載はできないようでした。そのため、基本となるテンプレートクラスは値型として template<T> class CEzStack で書いて、それ以外のポインタや参照について特殊化するという流れになるようです。
もどる ]