C++編【言語解説】 第21章 テンプレートのインスタンス化

先頭へ戻る

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

この章の概要

この章の概要です。

テンプレートのインスタンス化

テンプレートパラメータに具体的な型を当てはめて、 普通のクラスや関数の定義を作成する過程を、テンプレートのインスタンス化と言います。
テンプレートとは関係無く、クラスのオブジェクトを作り出すこともインスタンス化と呼ばれるので紛らわしいですが、両者は別物です。

例えば第20章で、「Stack<int> iStack;」のように変数を定義しました。 この場合、Stackクラステンプレートのテンプレートパラメータ T に int を当てはめてテンプレートのインスタンス化が行われ、 Stack<int>型のクラスが作成されます。 そして、Stack<int>型のオブジェクトを iStack という名前でインスタンス化している訳です。
クラステンプレート自体が型なのではなく、クラステンプレートをインスタンス化した結果、型が得られるのだと考えて下さい。

このような形で行われるテンプレートのインスタンス化を、テンプレートの暗黙的なインスタンス化(テンプレートの非明示的なインスタンス化)と呼びます。
ただしこのとき、メンバの定義はインスタンス化されていません。 メンバの定義は、そのメンバが実際に使用されるときにまで先送りされることになっています。 これは重要な仕様で、このおかげで次のような使い方が許されます。

#include <iostream>
#include <string>

template <typename T>
class SizeOperator {
public:
	explicit SizeOperator(T& t) :
	    mTarget(t)
	{}

	inline typename T::size_type Get() const
	{
		return mTarget.size();
	}

	inline void Set(typename T::size_type size)
	{
		mTarget.resize(size);
	}

private:
	T& mTarget;
};

class DataStoreArray {
public:
	typedef std::size_t size_type;

	explicit DataStoreArray(size_type size) :
		mValueArray(new int[size]),
		mSize(size)
	{}
	
	~DataStoreArray()
	{
		delete [] mValueArray;
	}

	inline size_type size() const
	{
		return mSize;
	}

private:
	int*        mValueArray;
	size_type   mSize;
};

int main()
{
	std::string str = "abcde";
	DataStoreArray ds(10);

	SizeOperator<std::string> op1(str);
	SizeOperator<DataStoreArray> op2(ds);

	op1.Set(50);
	std::cout << op1.Get() << std::endl;

//	op2.Set(50);  // DataStoreArray は resize() を持っていない
	std::cout << op2.Get() << std::endl;
}

実行結果:

50
10

SizeOperatorクラステンプレートは、複数のデータを管理するクラスをテンプレートパラメータに指定し、 Get、Set というメンバ関数を経由して、そのデータの要素数を取得・設定します。 あまり意味の無いテンプレートではありますが、これを例に取って説明していきます。

main関数のところを見ると、SizeOperator を std::string と DataStoreArray を使ってインスタンス化しています。
std::string を使った op1 の方では、Setメンバ関数と Getメンバ関数を呼び出しています。 これらはそれぞれ最終的に、std::string::resizeメンバ関数(【標準ライブラリ】第2章)と、 std::string::sizeメンバ関数を呼び出すことになります。 サイズを表現する型 T::size_type については、std::string::size_type が存在しています。 結果として、この過程の中に何も問題はありません。

一方、DataStoreArrayクラスを使った op2 の方では、「op2.Set(50);」のところをコメントアウトしていますが、 このコメントを外すと、コンパイルエラーになります。 これは、SizeOperator<T>::Setメンバ関数のところで、DataStoreArray::resize を必要としますが、 そのようなメンバ関数は存在していないためです。
ここで注目すべきことは、「op2.Set(50);」をコメントアウトして、Setメンバ関数を呼び出さないようにしていれば、 プログラム全体としては問題が無いということです。

つまり、クラステンプレートのメンバの定義は、実際に使用しなければ、インスタンス化を行わないので、 テンプレートパラメータに具体的な型を当てはめた結果、不正な部分があったとしても問題ありません
ただし、メンバの宣言については、クラステンプレート自身がインスタンス化されるタイミングで、インスタンス化されます。 そのため、次のプログラムのように、DataStoreArrayクラスが size_type を typedef していなければ、 Getメンバ関数の戻り値のところに登場する T::size_type を解決できないため、コンパイルエラーになります。

#include <iostream>
#include <string>

template <typename T>
class SizeOperator {
public:
	explicit SizeOperator(T& t) :
	    mTarget(t)
	{}

	inline typename T::size_type Get() const
	{
		return mTarget.size();
	}

	inline void Set(typename T::size_type size)
	{
		mTarget.resize(size);
	}

private:
	T& mTarget;
};

class DataStoreArray {
public:
	explicit DataStoreArray(std::size_t size) :
		mValueArray(new int[size]),
		mSize(size)
	{}
	
	~DataStoreArray()
	{
		delete [] mValueArray;
	}

	inline std::size_t size() const
	{
		return mSize;
	}

private:
	int*        mValueArray;
	std::size_t mSize;
};

int main()
{
	std::string str = "abcde";
	DataStoreArray ds(10);

	SizeOperator<std::string> op1(str);
	SizeOperator<DataStoreArray> op2(ds);  // DataStoreArray は size_type を持っていない

	op1.Set(50);
	std::cout << op1.Get() << std::endl;

	op2.Set(50);
	std::cout << op2.Get() << std::endl;
}

このように、クラステンプレートは、宣言の部分に登場するテンプレートパラメータを置き換えた結果が不正であるとエラーになりますが、 定義の中に不正な形が登場してしまっても、実際に使おうとしなければエラーになりません

テンプレートの明示的なインスタンス化

次のような方法で、テンプレートをインスタンス化することもできます。

template class Stack<int>;

この記述によって、Stackクラステンプレートのテンプレートパラメータ T に int型を当てはめて、インスタンス化できます。 この方法を、テンプレートの明示的なインスタンス化と呼びます。
この方法を使えば、クラステンプレートのメンバ関数の実装をソースファイル側へ隠すことができます。

// Stack.h

#ifndef STACK_H
#define STACK_H

#include <cstddef>

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

	explicit Stack(std::size_t capacity);
	~Stack();
	
	void Push(const T& data);
	void Pop();
	inline const value_type 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;
};

#endif
// Stack.cpp

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

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 class Stack<int>;
template class Stack<std::string>;

Stack.cpp の末尾で、明示的なインスタンス化を行っています。 この位置に置くことで、Stack.cpp がコンパイルされたときに、int型版と std::string型版のインスタンスが生成されます。
このように明示的なインスタンス化を行っておけば、プログラム内のどこかで Stack<int> や Stack<std::string> を使う際に、 明示的に作られた実体が利用されるようになるため、ヘッダファイルに実装が無くても問題なくリンクできるようになります。

ただし問題もあります。
そもそも、明示的なインスタンス化を行うには、 クラステンプレートの利用者がどんな型をテンプレートパラメータに当てはめるのかを理解している必要があります。 今回の場合、int型と std::string型を使いましたが、double型を使わないという保証が取れるでしょうか? もし利用者が、明示的にインスタンス化されていない Stack<double>型でインスタンス化を行ったら、 Stack<double>型版のメンバ関数の実装が見つかりませんから、結局、リンクエラーになります


また、テンプレートのインスタンスは、翻訳単位ごとに行われるため、Stack.h を #include した2つのファイル (main.cpp と sub.cpp) のそれぞれで、 int型を当てはめて暗黙的なインスタンス化を行ったとすると、Stack<int> の実体は、main.cpp をコンパイルしたオブジェクトファイルと、 sub.cpp をコンパイルしたオブジェクトファイルのそれぞれに含まれることになります。 これは、プログラムサイズの増大につながります。
明示的なインスタンス化によって、Stack<int> の実体が、Stack.cpp をコンパイルしたオブジェクトファイルだけに作られるようになりますから、 プログラムサイズの増大を軽減させる効果もあります

なお、もしも同じ明示的なインスタンス化が、プログラム全体の中で2回以上登場した場合、リンクエラーになります。 巨大なプロジェクトの場合、明示的なインスタンス化を書く場所をきちんと管理しないと、この問題に引っかかることがあります。


なお、明示的なインスタンス化は、メンバ関数単位で行うこともできます。 例えば、テンプレートパラメータ T を int型として、Pushメンバ関数だけを選択的にインスタンス化させるには、次のように記述します。

template void Stack<int>::Push(const int &);

クラス全体でないので classキーワードが無くなります。 また、テンプレートパラメータ T の部分はすべて、具体的な型に置き換えて記述する必要があります。 この例の場合だと、引数は const T& ではなく、const int& にしなくてはいけません。

C++11 (テンプレートのインスタンス化の抑止)

C++11

C++11 では、ある翻訳単位内ではテンプレートのインスタンス化を行わないように抑制する機能が追加されました。 これは、extern template と呼ばれます。

extern template class Stack<int>;

このように、明示的なインスタンス化の構文の先頭に extern を付けます。 この記述がある翻訳単位内では、この形でのインスタンス化が行われなくなります。

main.cpp と sub.spp の2か所で Stack<int>型を使用しているとしましょう。 結局のところ、Stack<int> の実体がどこかに1つあれば問題無いのですが、明示的なインスタンス化をしていなければ、 翻訳単位ごとに実体が作られているので、無駄な重複が生まれます。
extern template を使うことで、使い方の難しい明示的なインスタンス化を避けつつも、無駄な重複を排除できるようになります。 例えば、sub.cpp の方に、extern template を記述しておくことで、sub.cpp から作られるオブジェクトファイルには実体が生成されなくなります。 当然、どこかには実体が必要が必要になるので、main.cpp には extern template を記述してはいけません。

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

exportキーワード

テンプレート関数の実体をヘッダ側に置かなければならない場合、 実装を変更するたびに、そのヘッダを #include しているすべてのソースが影響を受け、再コンパイルを必要とします。 これは巨大なプログラムでは、開発効率に影響を与えます。

そこで、テンプレートの宣言時に exportキーワードを付加することによって、 定義を別の場所に書けるようにする機能が、C++03 の標準規格に存在しています。

しかし、exportキーワードを実装したコンパイラは非常に限られており、事実上、使用できる環境はほとんど存在しません。 これは、コンパイラを正しく実装することが技術的に非常に困難であったためです。 この事実を受けて、次の標準規格となった C++11 では廃止されているので、このキーワードについての解説は行いません。

C++11 (exportキーワードの廃止)

実装上、大きな困難を抱えており、現実に実装したコンパイラが非常に少ないことを踏まえ、 C++11 では、exportキーワードは廃止されました。 一応、キーワードとしては残されており、将来復活する可能性を残してはいます。


練習問題

問題@ クラステンプレートのメンバ関数の定義が、ヘッダ側に書かれていないと、その関数を使用できないことを確認して下さい。 その後、明示的なインスタンス化を行うことで、この問題を解決できることを確認して下さい。


解答ページはこちら

参考リンク

更新履歴

'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/11/1 新規作成。



前の章へ

次の章へ

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

Programming Place Plus のトップページへ