C++編【言語解説】 第20章 クラステンプレート

先頭へ戻る

このエントリーをはてなブックマークに追加

この章の概要

この章の概要です。

クラステンプレート

第9章で、関数をテンプレート化できることを解説しましたが、 本章では、クラスをテンプレート化してみます。
クラスの場合は、クラステンプレートと呼ばれます。 また、class と struct は概念としては同等なので(第12章)、 構造体をテンプレート化することもできます。

クラステンプレートでなく、テンプレートクラスと表現することもあります。 同じ意味で使われていますが、稀に、クラステンプレートに具体的な型を当てはめて作り出される具体的なクラスのことを指して、 テンプレートクラスと呼んでいることがあります。
紛らわしいので、クラステンプレートと表現する方が良いと思われます。

クラステンプレートを使うと、メンバ変数やメンバ関数の引数・戻り値の型を任意に指定できるようになります。 そのため、例えば、int型でも double型でも std::string型でも、 更にはユーザが任意で作ったクラス型でも扱えるようなスタッククラスを作り出せます。

template <typename T>
class Stack {
public:
	explicit Stack(std::size_t capacity);
	~Stack();

	void Push(const T& data);
	void Pop();
	inline const T Top() const
	{
		return mData[mSP - 1];
	}

	inline std::size_t GetSize() const
	{
		return mSP;
	}
	inline std::size_t GetCapacity() const
	{
		return mCapacity;
	}

private:
	const std::size_t      mCapacity;
	T*                     mData;
	int                    mSP;
};

template <typename T>
Stack<T>::Stack(std::size_t capacity) :
	mCapacity(capacity),
	mData(new T[capacity]),
	mSP(0)
{
}

template <typename T>
Stack<T>::~Stack()
{
	delete [] mData;
}

template <typename T>
void Stack<T>::Push(const T& data)
{
	assert(static_cast<std::size_t>(mSP) < mCapacity);
	mData[mSP] = data;
	mSP++;
}

template <typename T>
void Stack<T>::Pop()
{
	assert(mSP > 0);
	mSP--;
}

クラステンプレートを定義するには、通常のクラス定義の先頭に「template <typename T>」のような表記を付けます。 これは、関数テンプレートのときと同じなので、typename の代わりに classキーワードを使っても構いません。 また、テンプレートパラメータの T が慣習的な名前であり、任意に付けて構わない点も同様です。

テンプレートパラメータとして宣言された名前を、クラス定義内で型名の代わりに使用できます。 勿論、これらの名前は、テンプレート使用時に具体的な型名に置き換わります。

メンバ関数の定義を書く際にも、「template <typename T>」のような表記が必要です。 コード例を見ると分かるように、非常に面倒臭いですが仕方がありません。 勿論、クラス定義内に直接、メンバ関数の定義を書くことはできますが、 その場合、インライン化する指示を与えていることになる点に注意して下さい(第12章参照)。
また、「Stack<T>::Pop()」のような感じで、 クラステンプレート名の直後にもテンプレートパラメータの記述が必要なので、忘れないようにして下さい。 ここは少し表記法が違い、テンプレートパラメータの名前だけを書き並べます。

Stackクラステンプレートを使用する例は、次のようになります。

#include <iostream>
#include "Stack.h"

int main()
{
	Stack<int>         iStack(10);
	Stack<std::string> sStack(10);

	const std::size_t capacity = iStack.GetCapacity();
	for (std::size_t i = 0; i < capacity; ++i) {
		iStack.Push(static_cast<int>(i));
	}

	for (std::size_t i = 0; i < capacity; ++i) {
		std::cout << iStack.Top() << std::endl;
		iStack.Pop();
	}


	sStack.Push("aaa");
	sStack.Push("bbb");
	sStack.Push("ccc");

	const std::size_t size = sStack.GetSize();
	for (std::size_t i = 0; i < size; ++i) {
		std::cout << sStack.Top() << std::endl;
		sStack.Pop();
	}
}

実行結果:

9
8
7
6
5
4
3
2
1
0
ccc
bbb
aaa

「Stack<int>」のような表記で、クラステンプレートに具体的な型を当てはめることができます。 こうして初めて、このクラステンプレートを、実体のあるものとして使用できるようになります。
このサンプルプログラムの場合、変数iStack の型は Stack<int>型であり、変数sStack の型は Stack<std::string>型です。 Stack型だとか、Stack<T>型だとは表現しません。こういった表現では、T の部分が不明確であり、まだ型として使える状態ではありません。

なお、関数テンプレートのときもそうでしたが(第9章)、 クラステンプレートのメンバ関数の実体もヘッダファイル側に書く必要があります。 ただし、第21章で説明するように、 テンプレートの明示的なインスタンス化を利用することで、この制約を回避することができます。

第9章のコラムでも書いたように、コンパイラによっては、このような制約が無いこともあります。

デフォルトテンプレート引数

クラステンプレートでは、テンプレートパラメータにデフォルト値を指定することができます

関数テンプレートの場合は、C++03 の時点ではデフォルト値を与えることはできませんが、 C++11 になって可能になりました(第9章)。

例えば、Stackクラステンプレートが扱う型を、デフォルトで int型とするには、次のように書きます。

template <typename T = int>
class Stack {
	// 省略
};

通常の関数に与えるデフォルト引数と同様に、 デフォルト値を与えられるのは、後続のデフォルトパラメータだけです第8章)。

この使い方は、Stack の要素が int型であることに必然性が無いので、あまり良い使い方ではありませんが、 一応、この例のように、テンプレートパラメータのところにデフォルト値(デフォルトの型)を指定すれば、 デフォルト引数を指定したことになります。
実際に使用する際には、次のように書きます。

Stack<> iStack(10);  // Stack<int>型

この例のように、テンプレートパラメータが1つだけしかなく、 かつデフォルト値を持っている場合でも、使用する際には「<>」の部分を省くことはできません

連続する山括弧の解釈

テンプレート引数に、テンプレートを指定する場合などで、<> がネストするような形になることがあります。 次の例は、Stack<int> を要素とする Stack を作ろうとしています。

Stack<Stack<int>> isStack;  // エラー

このとき、>> の部分が、シフト演算子であると解釈されてしまうため、コンパイルエラーになります。 そのため、正しく認識させるために >> の間にスペースを入れる必要があります。

Stack<Stack<int> > isStack;  // OK

C++11

C++11 では、このような連続する山括弧を、テンプレートのための > であると正しく認識するようになりました

VisualC++、Xcode のいずれでも、この場面での >> を正しく認識できます。

C++11 (可変個テンプレートパラメータ)

C++11

C++11 では、テンプレートパラメータの個数を可変にできるようになりました。 この機能については、第22章で改めて取り上げます。

C++11 (エイリアステンプレート)

C++11

C++11 では、クラステンプレートを使って、型の別名を定義することができます。 この機能は、エイリアステンプレートと呼ばれます。

#include <iostream>
#include <utility>

template <typename SECOND_TYPE>
using Pair = std::pair<int, SECOND_TYPE>;

int main()
{
	Pair<const char*> pair(10, "abc");

	std::cout << pair.first << ", " << pair.second << std::endl;
}

実行結果:

10, abc

標準ライブラリの std::pair(【標準ライブラリ】第3章)は、2つのテンプレートパラメータを持つクラステンプレートです。 ここでは、第1テンプレートパラメータには int型を当てはめ、第2パラメータだけが未確定な新たなクラステンプレート Pair を定義しています。 Pair を使う際には、残りのテンプレートパラメータにだけ具体的な型を当てはめれば、使用できます。

エイリアステンプレートを定義するために、usingキーワードを使用していますが、 このキーワードは、次のように、テンプレートとは無関係に使用できます。

using 新しい型名 = 既存の型名;

これは、従来の typedef による新しい型の定義と同等の意味を持ちますが、 typedef では、エイリアステンプレートを実現することはできません。
using を使った表記の場合、例えば関数ポインタ型の定義などが、比較的読みやすくなる利点もあります。 次の2行は、同じ意味になります。

typedef const char* (*getter)(int);
using getter = const char*(*)(int);

この機能は、VisualC++、Xcode のいずれでも使用できます。

typedefの活用

本章の最初に取り上げた Stackクラステンプレートを使った次のコードを見て下さい。

Stack<int> s(10);

// s へ要素をプッシュ

int value = s.Top();

プッシュしている部分は省略していますが、幾つかの要素を Pushメンバ関数で追加していると考えて下さい。

問題なのは最後の Topメンバ関数の部分です。 戻り値を変数 value で受け取っていますが、この変数の型 int はどうやって決めたのでしょうか?
勿論、s を定義したとき、テンプレート引数を int型にしているのですから、int型で受け取ることは正しいのですが、 こういう書き方をしていると、後から型を変更しづらくなります。 例えば、次の通り。

Stack<long long int> s(10);  // int から変更

// s へ要素をプッシュ

int value = s.Top();  // 切りつめられる。情報を失う危険がある

改めて Topメンバ関数の宣言を確認してみると、次のように、戻り値の型は T になっています。

inline const T Top() const
{
	return mData[mSP - 1];
}

それならば、次のように書けば、テンプレート引数の型の変更に自動的に対応できるでしょうか?

Stack<int> s(10);

// s へ要素をプッシュ

T value = s.Top();  // ?

これはコンパイルエラーになります。 流石に、このように唐突に T と書くのはダメだろうと思いますが、次のように修飾してみてもやはりダメです。

Stack<int> s(10);

// s へ要素をプッシュ

Stack<int>::T value = s.Top();  // ?

残念ながら、テンプレートパラメータの名前を、そのクラステンプレートの定義内以外から使うことはできません。 そこで、クラステンプレート側で、後から使えるような名前を用意してやります。

template <typename T>
class Stack {
public:
	typedef T value_type;

	inline const value_type Top() const
	{
		return mData[mSP - 1];
	}

	// 今の話題に関係ないメンバは省略
};

追加された value_type という typedef名があれば、先ほどのプログラムコード片を次のように書き直せます。

Stack<int> s(10);

// s へ要素をプッシュ

Stack<int>::value_type value = s.Top();

これならばコンパイルが通ります。 しかしこれだけだと、テンプレート引数の型を変えたときに、2か所の書き換えが必要であることに変わりありません。 「Stack<int>」が2回登場してしまっていることが問題です。 そこで、今度は使用者側で次のように工夫を加えます。

typedef Stack<int> NumberStack;

NumberStack s(10);

// s へ要素をプッシュ

NumberStack::value_type value = s.Top();

ここでも typedef を使っています。 これで、具体的な型名が typedef のところにしか登場しなくなりますから、int の部分を long long int とか、 double とかに変えたとしても、既存のコードが影響を受けなくなります。
このように、C言語から存在している typedef は、非常に重宝する存在です。 特に、記述が長くなりがちな上、各所で繰り返し記述することが多いテンプレートの型は、 基本的に typedef で別名を付けてから使うようにするのがお勧めです

C++11 では using を使うのがより良いでしょう。

C++11 (代替策: auto を使う)

この項のような問題であれば、C++11 の auto(第2章)を使うと簡単に済みます。

Stack<int> s(10);

// s へ要素をプッシュ

auto value = s.Top();

この場合、Stackクラステンプレート側で value_type を用意する必要もありませんが、 それでも、value_type のような名前は用意しておいた方が、使用者に優しいと言えます。

typenameキーワード

前の項で、Stackクラステンプレートに value_type という型の別名を導入しました。 基本的な考え方としてはこれで問題ないのですが、実際に使っていると、突然コンパイルエラーに見舞われることがあります。 ちょっと無理やりな例ですが、次のプログラムを見て下さい。

class MyClass {
public:
	typedef int DataType;

	// 省略
};

template <typename T>
void func(T t)
{
	T::DataType* p;

	// 省略
}

int main()
{
	MyClass mc;
	func(mc);
}

このプログラムは問題ありません。 しかし、「T::DataType* p;」の部分が、コンパイルエラーを起こす可能性があります。 MyClassクラスの DataType の定義を次のように変更してみます。

	static const int DataType = 100;

DataType が typedef で定義される型名であれば、「T::DataType* p;」は T::DataType のポインタ型 p の変数を宣言していることになります。 しかし、DataType が静的メンバ定数であると、「T::DataType* p;」は「100 * p;」という乗算をしているように見えてしまいます。 そのため、p が未定義であるというコンパイルエラーになります。
もし、p が別にあれば、コンパイルエラーにはならず、意図に反する結果を生んでしまいます。

int p = 3;  // これがあると…

template <typename T>
void func(T t)
{
	T::DataType* p;  // 100 * 3; は有効な文なのでエラーにならない

	// 省略
}

どこが問題のポイントなのかに注意して下さい。 「T::DataType* p;」という文は、ポインタ変数 p を宣言しているのであって、それ以外の意図は無いでしょうから、 誤ったテンプレートの解釈は、コンパイルエラーになるようにしたいのです。
この問題を解決するには、「T::DataType」が型名であることを明示的に指示します。 そのために typenameキーワードが使えます。

template <typename T>
void func(T t)
{
	typename T::DataType* p;

	// 省略
}

typenameキーワードには、その直後に続く塊が、型名であることを明示する効果があります
ちなみに、テンプレートパラメータの記述では、typename を class で代用できますが、 class には、後続を型名であると明示する効果はありません


同じ問題は、Topメンバ関数の定義を、クラステンプレートの定義の外で書こうとしたときにも起こります。

template <typename T>
const Stack<T>::value_type Stack<T>::Top() const
{
	return mData[mSP - 1];
}

戻り値が、クラス内に定義された型の場合には、クラス名による修飾が必要です。 そのため、ここでは「Stack<T>::」による修飾が必要ですが、これもコンパイルエラーになってしまいます。 そこでやはり、typenameキーワードを挟みます。

template <typename T>
const typename Stack<T>::value_type Stack<T>::Top() const
{
	return mData[mSP - 1];
}

std::string の正体

これまでそれほど気にせず使用してきた std::string (std::wstring も) ですが、この正体はクラステンプレートの typedef です。 次のように定義されています。

namespace std {
	typedef basic_string<char> string;
	typedef basic_string<wchar_t> wstring;
}

std::string と std::wstring は、std::basic_stringクラステンプレートのテンプレートパラメータに、 それぞれ char と wchar_t を当てはめて作られており、これに typedef を使って名前を付けたものです。

c_str関数や size関数、=演算子、[]演算子といったメンバ関数は、std::basic_stringクラステンプレートに定義されているので、 std::string でも std::wstring でも共通で使用できます。


練習問題

問題@ 標準ライブラリには、比較的単純なクラステンプレートとして std::pair があります。 この章で取り上げた Stackクラステンプレートを使い、スタックの要素が std::pair であるようなプログラムを作成して下さい。
(std::pair については、【標準ライブラリ】第3章で解説しています)。

問題A 任意の型の3つの値を管理できるクラステンプレートを作ってみて下さい。 値の管理に必要な最低限のメンバがあれば十分です。


解答ページはこちら

参考リンク

更新履歴

'2017/7/30 clang 3.7 (Xcode 7.3) を、Xcode 8.3.3 に置き換え。

'2017/3/25 VisualC++ 2017 に対応。

'2016/10/15 clang の対応バージョンを 3.7 に更新。

'2015/10/12 clang の対応バージョンを 3.4 に更新。

'2015/9/5 VisualC++ 2012 の対応終了。

'2015/8/18 VisualC++2010 の対応終了。

'2015/8/15 VisualC++ 2015 に対応。

'2014/10/25 新規作成。



前の章へ

次の章へ

C++編のトップページへ

Programming Place Plus のトップページへ