C++編【言語解説】 第23章 テンプレートの特殊化

先頭へ戻る

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

この章の概要

この章の概要です。

特殊化

テンプレートでは、テンプレートパラメータに当てはめる具体的な型に応じて、普通のクラスや関数をインスタンス化します。 こうして生成されるクラスや関数のことを、特殊化と呼びます。

ところで、あるクラステンプレートの設計を考えたとき、そのテンプレートパラメータに当てはめられる型の種類によっては、 実装上、都合が悪いケースがあります。 よくあるのは、次のような処理です。

template <typename T>
class DataStore {
pubilc:
	
	// 無関係のメンバは省略
	
	inline bool operator==(const DataStore& rhs) const
	{
		return mValue == rhs.mValue;
	}

private:
	T  mValue;
};

DataStore<>::operator== は、メンバ変数 mValue の値を ==演算子を使って比較します。 基本的に間違っていませんが、もし、テンプレートパラメータ T の具体的な型が const char*型のように、C言語の文字列表現だったらどうでしょうか? この場合、std::strcmp関数によって比較される方が適切かも知れません。

この例のように、テンプレートパラメータの具体的な型によって、処理内容を変更したいことがあります。 これを可能にするための仕組みとして、完全特殊化(明示的特殊化)部分特殊化が使えます。
完全特殊化や部分特殊化は、テンプレートパラメータに特定の型が当てはめられたときにだけ使われる、特別版を定義する機能です

完全特殊化

それではまず、完全特殊化の例を見ていきましょう。 クラステンプレートの場合と、関数テンプレートの場合とがあるので、それぞれ個別に解説します。

クラステンプレートの完全特殊化

先ほどの DataStoreクラステンプレートの例を解決してみます。

#include <iostream>
#include <cstring>

template <typename T>
class DataStore {
public:
	explicit DataStore(const T& value) :
		mValue(value)
	{}

	inline bool operator==(const DataStore& rhs) const
	{
		return mValue == rhs.mValue;
	}
	
private:
	T    mValue;
};

template <>
class DataStore<const char*> {
public:
	explicit DataStore(const char* value) :
		mValue(value)
	{}

	inline bool operator==(const DataStore& rhs) const
	{
		return std::strcmp(mValue, rhs.mValue) == 0;
	}
	
private:
	const char*    mValue;
};


int main()
{
	const char s1[] = "abc";
	const char s2[] = "abc";

	DataStore<const char*> ds1(s1);
	DataStore<const char*> ds2(s2);

	std::cout << std::boolalpha
	          << (ds1 == ds2)
	          << std::endl;
}

実行結果:

true

完全特殊化を行う際には、まず、通常のテンプレートの定義を用意しておきます。 これはこれまでの章とまったく同じように作ればいいです。 こうして用意したテンプレートのことを、1次テンプレート(プライマリテンプレート)と呼びます。

完全特殊化のためのクラス定義を別途用意します。 これは、1次テンプレートが見える位置に書きます

1次テンプレートと同じ名前のクラスを定義しますが、このとき冒頭に「template <>」を付けることで、 完全特殊化を行っていることを表す必要があります。 また、クラス名の後ろに、テンプレートパラメータに当てはめられる具体的な型名を指定します。 これを記述することで、この型がテンプレートパラメータに当てはめられたときにだけ、この完全特殊化されたクラスが使用されるようになります。

あとは、1次テンプレートと同じ意味合いの内容になるように、メンバを書いていきます。 感覚として意識して欲しいのは、1次テンプレート側と違い、完全特殊化のためのクラスは、具体的な型を使って記述されるという点です。 テンプレートパラメータに当てはめられる型は、すでに決定しているのですから、 T のようなテンプレートパラメータは登場しません。


テンプレートを使用する側は、特に何も意識する必要がありません。 テンプレート引数に const char*型を指定した場合にだけ、完全特殊化された DataStoreクラスが使用され、 それ以外の型を指定した場合には、DataStoreクラステンプレートをその型を使ってインスタンス化したクラスが使用されます。

実行結果を見ると分かるように、内容は同一だがアドレスが異なる2つの文字列の比較で、true という結果になっています。 これは、==演算子ではなく std::strcmp関数が使用されているからです。 試しに、完全特殊化のコードだけをコメントアウトして試すと、結果は false に変わります。

関数テンプレートの完全特殊化

今度は、関数テンプレートの完全特殊化の例を挙げます。

#include <iostream>
#include <string>
#include <cstring>

template <typename T>
inline std::size_t Length(const T* str)
{
	return str->length();
}

template <>
inline std::size_t Length<char>(const char* str)
{
	return std::strlen(str);
}


int main()
{
	const std::string s1("aaa");
	const char s2[] = "xxxx";

	std::cout << Length(&s1) << "\n"
	          << Length(s2) << std::endl;
}

実行結果:

3
4

このサンプルプログラムは、文字列の長さを調べる Length関数テンプレートを定義しています。 実引数に指定したポインタ経由で、lengthメンバ関数を呼び出して結果を得ようとしていますが、 const char*型で表現された文字列の場合には、当然ながらメンバ関数がありません。 そこで、完全特殊化を利用して、const char*型の場合にだけ、std::strlen関数を使わせようとしています。

関数テンプレートの場合の完全特殊化は、クラステンプレートの場合とほぼ同様で、 1次テンプレートとなる関数テンプレートを定義し、これが見える位置に、完全特殊化のための関数を定義します関数名を同じにすることや、冒頭に「template <>」が必要である点も、クラステンプレートの場合と同様です

完全特殊化のための定義の方には、関数名の後ろに、テンプレートパラメータに当てはめる具体的な型名を指定します。 これも、クラステンプレートの場合と同様ではありますが、関数テンプレートの場合は、実引数から型を推測しますから(第9章)、 それによってコンパイラが判断可能であれば省略できます。 省略する場合は、次のようになります。

template <>
inline std::size_t Length(const char* str)
{
	return std::strlen(str);
}

省略しなかった場合の指定は「<const char*>」ではなくて「<char>」であることに注目して下さい。
1次テンプレートの方の仮引数の型は「const T*」ですが、ここに const char*型の実引数を与えると、テンプレートパラメータ T は「char」と判断されます。 「const」も「*」もすでに T の外側に付いていますから、T に当てはめられる型はあくまで「char」なのです。 従って、完全特殊化の側のテンプレート引数は「char」が正解です。

ところで、Length関数テンプレートの仮引数がポインタなのが少し気にならないでしょうか? 恐らく、参照の方がシンプルで安全だと思われます。 しかし、仮に以下のように参照に直すと、コンパイルが通らなくなります(呼び出し側も修正したとしても)。

template <typename T>
inline std::size_t Length(const T& str)
{
	return str.length();
}

template <>
inline std::size_t Length<char>(const char* str)
{
	return std::strlen(str);
}

これは、完全特殊化の方ではポインタが使われているため、引数の型が一致しないためです。 完全特殊化はあくまでも、テンプレートパラメータの部分を具体化するだけですから、それ以外の部分が変わってはいけません

このケースでは、完全特殊化ではなくて、単に関数をオーバーロードすればいいでしょう。

template <typename T>
inline std::size_t Length(const T& str)
{
	return str.length();
}

inline std::size_t Length(const char* str)
{
	return std::strlen(str);
}

これなら問題ありません。 関数テンプレートと通常の関数が存在していて、どちらにも適合するときには、通常の関数の方が優先される第9章)ので、 きちんと動作します。

部分特殊化

少し分かりづらいのですが、部分特殊化には幾つかのパターンがありますが、 どれも一言で言えば、「テンプレートパラメータの一部だけを特殊化する」ということです。
テンプレートパラメータのうちの一部分だけしか特殊化されないので、 言い換えると、特殊化されていないテンプレートパラメータが残っているということです。 そのため、完全特殊化する場合と違い、部分特殊化によって作られるものはクラスではなく、クラステンプレートのままです

1つ目のパターンは、テンプレートパラメータの型の具体性を高めるような特殊化で、 例えば、T型のテンプレートパラメータに対して、T*型で特殊化するというものです。 この場合、指定されたテンプレート引数がポインタだった場合にだけ、特殊化されるということになります。

2つ目のパターンは、2個以上あるテンプレートパラメータのうちの1個以上に関して、具体的な型を想定するもので、 例えば、T型と U型のテンプレートパラメータに対して、T は何でも構わないが、U が double のときには特殊化するというものです。

3つ目のパターンは、テンプレートパラメータの個数自体を変えてしまうもので、 例えば、T型のテンプレートパラメータに対して、T[N]型で特殊化するとき、 N を新たなノンタイプテンプレートパラメータ(第22章)に追加できます。

なお、部分特殊化は、クラステンプレートにのみ可能であり、関数テンプレートに対しては行えません。 関数テンプレートの場合の一部は、オーバーロードを使うことで代替できます。

関数の戻り値の型を指定するために、テンプレートパラメータを使うような関数テンプレートの場合、 オーバーロードでは引数の型や個数でしか区別を付けられないので、対応できません。 この場合は、クラステンプレートを用意して、そちらで部分特殊化し、そのメンバ関数を呼ぶように実装すれば対応可能です。


それでは、部分特殊化の例を見ていきましょう。

T型を T*型で部分特殊化する

まず、1つ目のパターンの例として、T型を T*型で部分特殊化します。

#include <iostream>

// 1次テンプレート
template <typename T>
class DataStore {
public:
	explicit DataStore(const T& value) :
		mValue(value)
	{}

	void Print() const;
	
private:
	T    mValue;
};

template <typename T>
void DataStore<T>::Print() const
{
	std::cout << mValue << std::endl;
}


// 部分特殊化
template <typename T>
class DataStore<T*> {
public:
	explicit DataStore(T* value) :
		mValue(value)
	{}

	void Print() const;
	
private:
	T*    mValue;
};

template <typename T>
void DataStore<T*>::Print() const
{
	std::cout << *mValue << std::endl;
}



int main()
{
	int num = 20;

	DataStore<int> ds1(10);
	DataStore<int*> ds2(&num);

	ds1.Print();
	ds2.Print();
}

実行結果:

10
20

DataStore<>::Printメンバ関数は、保持している値を出力しますが、 ポインタ型を保持しているときには、指し示す先の値を出力します。

前述した通り、部分特殊化が作り出すのはクラステンプレートなので、classキーワードの手前の「template <typename T>」はそのまま必要です。 「template <>」だと、完全特殊化を意味するので注意して下さい。

複数個あるテンプレートパラメータの一部を特殊化する

次の例は、2つあるテンプレートパラメータのうちの1つだけを特殊化するものです。

#include <iostream>
#include <cassert>


// 1次テンプレート
template <typename T, std::size_t CAPACITY>
class Stack {
public:
	static const std::size_t CAPACITY = CAPACITY;

public:
	Stack();
	~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;
	}
	
private:
	T                      mData[CAPACITY];
	int                    mSP;
};


template <typename T, std::size_t CAPACITY>
Stack<T, CAPACITY>::Stack() :
	mSP(0)
{
}

template <typename T, std::size_t CAPACITY>
Stack<T, CAPACITY>::~Stack()
{
}

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

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



// 部分特殊化
template <std::size_t CAPACITY>
class Stack<bool, CAPACITY> {
public:
	static const std::size_t CAPACITY = CAPACITY;

public:
	Stack();
	~Stack();
	
	void Push(const bool& data);
	void Pop();
	inline const bool Top() const
	{
		return (mData[GetDataIndex(mSP - 1)] & (1 << GetDataBit(mSP - 1))) != 0;
	}
	
	inline std::size_t GetSize() const
	{
		return mSP;
	}

private:
	typedef unsigned int data_t;

	static const unsigned int DATA_BITS = sizeof(data_t) * 8;
	static const std::size_t DATA_ARRAY_SIZE = (CAPACITY / DATA_BITS) + 1;

	inline std::size_t GetDataIndex(int sp) const
	{
		return sp / DATA_BITS;
	}
	inline std::size_t GetDataBit(int sp) const
	{
		return sp % DATA_BITS;
	}

private:
	data_t                 mData[DATA_ARRAY_SIZE];
	int                    mSP;
};


template <std::size_t CAPACITY>
Stack<bool, CAPACITY>::Stack() :
	mSP(0)
{
}

template <std::size_t CAPACITY>
Stack<bool, CAPACITY>::~Stack()
{
}

template <std::size_t CAPACITY>
void Stack<bool, CAPACITY>::Push(const bool& data)
{
	assert(static_cast<std::size_t>(mSP) < CAPACITY);

	if (data) {
		mData[GetDataIndex(mSP)] |= (1 << GetDataBit(mSP));
	}
	else {
		mData[GetDataIndex(mSP)] &= ~(1 << GetDataBit(mSP));
	}
	mSP++;
}

template <std::size_t CAPACITY>
void Stack<bool, CAPACITY>::Pop()
{
	assert(mSP > 0);
	mSP--;
}



int main()
{
	static const int SIZE = 5;

	typedef Stack<int, SIZE> IntStack;
	typedef Stack<bool, SIZE> BoolStack;

	IntStack iStack;
	BoolStack bStack;

	for (std::size_t i = 0; i < SIZE; ++i) {
		iStack.Push(static_cast<int>(i));
		bStack.Push((i & 1) ? true : false);
	}

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

実行結果:

4
false
3
true
2
false
1
true
0
false

要素数の上限が固定化された Stackクラステンプレートです。

クラステンプレートは2つあり、1つは要素の型、もう1つは要素数の上限値です。 ここで、要素の型を表すテンプレートパラメータを bool型と指定した場合に、 内部配列の管理を工夫して、1ビットに 1要素の情報を持つように特殊化しています。

標準ライブラリの vector が、vector<bool> の場合にしていることと、考え方は同じです(【標準ライブラリ】第5章)。

テンプレートパラメータの個数が増えるパターン

次の例は、テンプレートパラメータの個数が変わるパターンです。

#include <iostream>


// 1次テンプレート
template <typename T>
class DataStoreArray {
public:
	explicit DataStoreArray(std::size_t size) :
		mValueArray(new T[size])
	{}
	
	~DataStoreArray()
	{
		delete [] mValueArray;
	}
	
	inline T operator[](std::size_t index) const
	{
		return mValueArray[index];
	}
	
	inline T& operator[](std::size_t index)
	{
		return mValueArray[index];
	}

private:
	T*    mValueArray;
};


// 部分特殊化
template <typename T, std::size_t SIZE>
class DataStoreArray<T[SIZE]> {
public:
	DataStoreArray()
	{}
	
	~DataStoreArray()
	{}
	
	inline T operator[](std::size_t index) const
	{
		return mValueArray[index];
	}
	
	inline T& operator[](std::size_t index)
	{
		return mValueArray[index];
	}

private:
	T    mValueArray[SIZE];
};




int main()
{
	static const int SIZE = 5;

	DataStoreArray<int> iStoreArray(SIZE);
	DataStoreArray<int[SIZE]> iStoreArray2;

	for (int i = 0; i < SIZE; ++i) {
		iStoreArray[i] = i * 10;
		iStoreArray2[i] = i * 10;
	}

	for (int i = 0; i < SIZE; ++i) {
		std::cout << iStoreArray[i] << " " << iStoreArray2[i] << "\n";
	}
	std::cout << std::endl;
}

実行結果:

0 0
10 10
20 20
30 30
40 40

複数の要素を配列管理するクラステンプレートです。 通常は、指定された型の要素を動的配列を使って管理しますが、 配列型を指定した場合には、動的な管理を行わない実装を使用するようになります。

コンテナ

標準ライブラリには、STL (標準テンプレートライブラリ) と呼ばれる、テンプレートを活用した機能群があります。 この章までに、関数テンプレートやクラステンプレートに関する基本的な知識を得られたので、 STL を少しずつ理解していける段階に来たはずです。

まずは、STLコンテナについて学んでみましょう。 【標準ライブラリ】第4章に進み、概要を知ったら、 第5章第13章まで進めて下さい。
また、続けて、第14章に進み、イテレータという概念について理解して下さい。

なお、【標準ライブラリ】編では、ほぼ全機能を網羅するリファレンスのようなページ作りをしているので、 すべてを理解するのではなく、ざっくりとした概要を学ぶ程度で良いでしょう。 特に、【言語解説】編の本章までに解説されていない概念が登場することもあるので、その場合は、単に読み飛ばして良いです。


練習問題

問題@ 部分特殊化の1つ目のパターン「T型を T*型で部分特殊化する」において、 ポインタであるかそうでないかによって、printメンバ関数の実装を変えられることを示しました。 同様のことを、関数テンプレートで行うとすれば、どう実装しますか?

問題A 標準ライブラリに含まれる STLコンテナ、「vector」「list」「deque」「set」「map」は、それぞれどんなデータ構造を提供しますか?


解答ページはこちら

参考リンク

更新履歴

'2015/12/20 新規作成。



前の章へ

次の章へ

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

Programming Place Plus のトップページへ