テンプレートの目的は、プログラマが一度コードを書くだけで、そのコードが型の形式に準拠して広範囲の型に適用できるようにすることです。この章では関数テンプレートに関連したテンプレートの概念と用語を紹介し、より複雑な (そして、より強力な) クラステンプレートと、テンプレートの使用方法について説明しています。また、テンプレートのインスタンス化、デフォルトのテンプレートパラメータ、およびテンプレートの特殊化についても説明しています。この章の最後には、テンプレートの潜在的な問題が挙げられています。
関数テンプレートは、引数または戻り値の型だけが異なった、関連する複数の関数を記述したものです。
C++ 5.0 では、関数テンプレートにテンプレートの型名でないパラメータを使用することはできません 。
テンプレートは使用する前に宣言しなければなりません。次の例に見られるように、 「宣言」によってテンプレートを使用するのに十分な情報は与えられますが、テンプレートの実装には他の情報も必要です。
template <class Number> Number twice( Number original );
この例では Number は「テンプレートパラメータ」であり、テンプレートが記述する関数の範囲を指定します。具体的に言えば、Number は「テンプレート型パラメータ」であり、テンプレート宣言と定義の中で使用されている場合、指定したいテンプレートの型を意味しています。
テンプレートは宣言と定義の両方が必要になります。テンプレートを「定義」することで実装に必要な情報が得られます。次の例は、前述の例で宣言されたテンプレートを定義しています。
template <class Number> Number twice( Number original ) { return original + original; }
テンプレート定義は通常ヘッダーファイルで行われるので、テンプレート定義が複数のコンパイル単位で繰り返される可能性があります。しかし、すべての定義は同じでなければなりません。この制限は「単一定義ルール」と呼ばれます。
C++ 5.0 では、関数テンプレートには型名ではないテンプレートパラメータは使用できません。たとえば、次のテンプレートは、その引数が型ではなく式であるため使用できません。
template <int count> void foo( ) // パラメータが型名ではないので無効 { int x[count]supported non-type parameter for (int i = 0; i < count; ++i ) // ... x により何かを行う } foo<10>(); // テンプレート引数 10 により foo を呼び出そうとするが、無効
テンプレートは、いったん宣言すると他のすべての関数と同様に使用することができます。テンプレートを「使用」するには、そのテンプレートの名前とテンプレート引数を指定します。コンパイラは、テンプレート型引数を、関数引数の型から推測します。たとえば、以前に宣言されたテンプレートを次のように使用できます。
double twicedouble( double item ) { return twice( item ); }
クラステンプレートは、複数の関連するクラス (データ型) を記述します。グラステンプレートに記述されているクラスは、型のほかに整数値、または大域リンケージによる変数へのポインタや参照だけが互いに異なっています。クラステンプレートは、一般的ではあるが型が保証されているデータ構造を記述するのに特に便利です。
クラステンプレートの宣言では、クラスの名前とそのテンプレート引数だけを指定します。このような宣言は「不完全なクラステンプレート」と呼ばれます。
次の例は、任意の型の引数を取る Array というクラスに対するテンプレート宣言の例です。
template <class Elem> class Array;
次のテンプレートは、unsigned integer の引数を取る String というクラスに対する宣言です。
template <unsigned Size> class String;
クラステンプレートの定義では、次の例のようにクラスデータと関数メンバーを宣言しなければなりません。
template <class Elem> class Array { Elem* data; int size; public: Array( int sz ); int GetSize(); Elem& operator[]( int idx ); };
template <unsigned Size> class String { char data[Size]; static int overflows; public: String( char *initial ); int length(); };
関数テンプレートとは違って、クラステンプレートには class Elem のような型パラメータと unsigned Size のような式パラメータの両方を指定できます。式パラメータには次の情報を指定できます。
整数型または列挙型を持つ値
オブジェクトへのポインタまたは参照
関数へのポインタまたは参照
クラスメンバー関数へのポインタ
クラステンプレートを完全に定義するには、その関数メンバーと静的データメンバーを定義する必要があります。動的 (静的でない) データメンバーの定義は、クラステンプレート宣言で十分です。
テンプレート関数メンバーの定義は、テンプレートパラメータの指定と、それに続く関数定義から構成されます。関数識別子は、クラステンプレートのクラス名とそのテンプレートの引数で修飾されます。次の例は、template <class Elem> というテンプレートパラメータ指定を持つ Array クラステンプレートの 2 つの関数メンバー定義を示しています。それぞれの関数識別子は、テンプレートクラス名とテンプレート引数 Array<Elem> で修飾されています。
template <class Elem> Array<Elem>::Array( int sz ) { size = sz; data = new Elem[ size ]; } template <class Elem> int Array<Elem>::GetSize( ) { return size; }
次の例は、String クラステンプレートの関数メンバーの定義を示しています。
#include <string.h> template <unsigned Size> int String<Size>::length( ) { int len = 0; while ( len < Size && data[len] != '¥0' ) len++; return len; } template <unsigned Size> String<Size>::String( char *inital ) { strncpy( data, initial, Size ); if ( length( ) == Size ) overflow++; }
テンプレートの静的データメンバーの定義は、テンプレートパラメータの指定と、それに続く変数定義から構成されます。この場合、変数識別子は、クラステンプレート名とそのテンプレートの実引数で修飾されます。
template <unsigned Size> int String<Size>::overflows = 0;
テンプレートクラスは、型が使用できる場所ならどこででも使用できます。テンプレートクラスを指定するには、テンプレート名と引数の値を設定します。次の宣言例では、Array テンプレートに基づいた変数 int_array を作成します。この変数のクラス宣言とその一連のメソッドは、Elem が int に置き換わっている点以外は、Array テンプレートとまったく同じです (「テンプレートのインスタンス化」を参照)。
Array<int> int_array( 100 );
次の宣言例は、String テンプレートを使用して short_string 変数を作成します。
String<8> short_string( "hello" );
テンプレートクラスのメンバー関数は、他のすべてのメンバー関数と同じように使用できます。
int x = int_array.GetSize( );
int x = short_string.length( );
テンプレートの「インスタンス化」には、特定の組み合わせのテンプレート引数に対応した具体的なクラスまたは関数 (「インスタンス」) を生成することが含まれます。たとえば、コンパイラは Array<int> と Array<double> に対応した別々のクラスを生成します。これらの新しいクラスの定義では、テンプレートクラスの定義の中のテンプレートパラメータがテンプレート引数に置き換えられます。前述の「クラステンプレート」の節に示す Array<int> の例では、すべての Elem が int に置き換えられます。
テンプレート関数またはテンプレートクラスを使用すると、インスタンス化が必要になります。そのインスタンスがまだ存在していない場合には、コンパイラはテンプレート引数に対応したテンプレートを暗黙的にインスタンス化します。
コンパイラは、あるテンプレートクラスを暗黙的にインスタンス化するとき、使用されるメンバーだけをインスタンス化します。コンパイラがあるクラスを暗黙的にインスタンス化するときにすべてのメンバー関数をインスタンス化するには、コンパイラオプションの -template=wholeclass を使用します。このオプションを無効にするには、 -template=no%wholeclass を指定します。
コンパイラは、実際に使用されるテンプレート引数に対応したテンプレートだけを暗黙的にインスタンス化します。これは、テンプレートを提供するライブラリの作成には適していない可能性があります。C++ には、次の例のように、テンプレートを明示的にインスタンス化するための手段が用意されています。
テンプレート関数を明示的にインスタンス化するには、template キーワードに続けて関数の宣言 (定義ではない) を行います。関数の宣言では関数識別子の後にテンプレート引数を指定します。
template float twice<float>( float original );
テンプレート引数は、コンパイラが推測できる場合は省略できます。
template int twice( int original );
テンプレートクラスを明示的にインスタンス化するには、template キーワードに続けてクラスの宣言 (定義ではない) を行います。クラス宣言ではクラス識別子の後にテンプレート引数を指定します。
template class Array<char>;
template class String<19>;
クラスを明示的にインスタンス化すると、そのメンバーもすべてインスタンス化されます。
テンプレート関数メンバーを明示的にインスタンス化するには、template キーワードに続けて関数の宣言 (定義ではない) を行います。関数の宣言ではテンプレートクラスで修飾した関数識別子の後にテンプレート引数を指定します。
template int Array<char>::GetSize( );
template int String<19>::length( );
テンプレートの静的データメンバーを明示的にインスタンス化するには、template キーワードに続けてメンバーの宣言 (定義ではない) を行います。メンバーの宣言では、テンプレートクラスで修飾したメンバー識別子の後にテンプレート引数を指定します。
template int String<19>::orerflow;
テンプレートは、入れ子にして使用できます (定義はできません)。これは、標準 C++ ライブラリで行う場合のように、一般的なデータ構造に関する汎用関数を定義する場合に特に便利です。たとえば、テンプレート配列クラスに関して、テンプレートのソート関数を次のように宣言することができます。
template <class Elem> void sort( Array<Elem> );
template <class Elem> void sort( Array<Elem> store ) { int num_elems = store.GetSize( ); for ( int i = 0; i < num_elems-1; i++ ) for ( int j = i+1; j < num_elems; j++ ) if ( store[j-1] > store[j] ) { Elem temp = store[j]; store[j] = store[j-1]; store[j-1] = temp; } }
前の例は、事前に宣言された Array クラステンプレートのオブジェクトに関するソート関数を定義しています。次の例はソート関数の実際の使用例を示しています。
Array<int> int_array( 100 ); // intの配列を作成し、 sort( int_array ); // それをソートする。
クラステンプレートのテンプレートパラメータには、デフォルトの値を指定できます (関数テンプレートは不可)。
template <class Elem = int> class Array; template <unsigned Size = 100> class String;
テンプレートパラメータにデフォルト値を指定する場合、それに続くパラメータもすべてデフォルト値でなければなりません。テンプレートパラメータに指定できるデフォルト値は 1 つです。
次の twice の例のように、テンプレート引数を例外的に特定の形式で組み合わせると、パフォーマンスが大幅に改善されることがあります。あるいは、次の sort の例のように、テンプレート記述がある引数の組み合わせに対して適用できないこともあります。テンプレートの特殊化によって、実際のテンプレート引数の特定の組み合わせに対して代替実装を定義することが可能になります。テンプレートの特殊化はデフォルトのインスタンス化を無効にします。
前述のようなテンプレート引数の組み合わせを使用するには、その前に特殊化を宣言しなければなりません。次の例は twice と sort の特殊化された実装を宣言しています。
template <> unsigned twice<unsigned>( unsigned original );
template <> sort<char*>( Array<char*> store );
コンパイラがテンプレート引数を明確に確認できる場合には、テンプレート引数を省略することができます。次にその例を示します。
template <> unsigned twice( unsigned original );
template <> sort( Array<char*> store );
宣言するテンプレート特殊化はすべて定義しなければなりません。次の例は、前の節で宣言された関数を定義しています。
template <> unsigned twice<unsigned>( unsigned original ) { return original << 1; }
#include <string.h> template <> void sort<char*>( Array<char*> store ) { int num_elems = store.GetSize( ); for ( int i = 0; i < num_elems-1; i++ ) for ( int j = i+1; j < num_elems; j++ ) if ( strcmp( store[j-1], store[j] ) > 0 ) { char *temp = store[j]; store[j] = store[j-1]; store[j-1] = temp; } }
特殊化されたテンプレートは他のすべてのテンプレートと同様に使用され、インスタンス化されます。ただし、完全に特殊化されたテンプレートの定義はインスタンス化でもあります。
この節では、テンプレートを使用する場合の問題について説明しています。
テンプレート定義で使用される名前の中には、テンプレート引数によって、またはそのテンプレート内で、定義されていないものがある可能性があります。そのような場合にはコンパイラが、定義の時点で、またはインスタンス化の時点で、テンプレートを取り囲むスコープから名前を解決します。1 つの名前が複数の場所で異なる意味を持つために解決の形式が異なることも考えられます。
名前の解決は複雑です。したがって、汎用性の高い標準的な環境で提供されているもの以外は、非局所型名前に依存することは避ける必要があります。言い換えれば、至るところで宣言されていて、どこでも同じ意味をもつ非局所型名前だけを使用するようにしてください。この例では、テンプレート関数の converter が、非局所型名前である intermediary と temporary を使用しています。これらの名前は use1.cc と use2.cc では異なる定義を持っているため、コンパイラが異なれば結果は違うものになるでしょう。テンプレートが正しく機能するためには、すべての非局所型名前 (intermediary と temporary) がどこでも同じ定義を持つ必要があります。
use_common.h // 共通のテンプレート定義 template <class Source, class Target> Target converter( Source source ) { temporary = (intermediary)source; return (Target)temporary; } use1.cc typedef int intermediary; int temporary; #include "use_common.h"} use2.cc typedef double intermediary; unsigned int temporary; #include "use_common.h"
非局所型名前を使用する典型的な例として、1 つのテンプレート内で cin と cout のストリームの使用があります。ほとんどのプログラマは実際、ストリームをテンプレートパラメータとして渡すことは望まないので、一つの大域変数を参照するようにします。しかし、cin および cout はどこでも同じ定義を持っていなければなりません。
テンプレートインスタンス化の仕組みは、型と名前が一致することを目安に、どのテンプレートをインスタンス化または再インスタンス化する必要があるかを決定します。したがって、局所型がテンプレート引数として使用された場合には重大な問題が発生する可能性があります。自分のコードに同様の問題が生じないように注意してください。次に例を示します。
array.h template <class Type> class Array { Type* data; int size; public: Array( int sz ); int GetSize( ); }; array.cc template <class Type> Array<Type>::Array( int sz ) { size = sz; data = new Type[size]; } template <class Type> int Array<Type>::GetSize( ) { return size;} file1.cc #include "array.h" struct Foo { int data; }; Array<Foo> File1Data; file2.cc #include "array.h" struct Foo { double data; }; Array<Foo> File2Data;
file1.cc の中に登録された Foo 型は、file2.cc の中に登録された Foo 型と同じではありません。局所型をこのように使用すると、エラーと予期しない結果を生じることがあります。
テンプレートは、使用前に宣言されていなければなりません。フレンド宣言では、テンプレートを宣言するのではなく、テンプレートの使用を宣言します。フレンド宣言の前に、実際のテンプレートが宣言されていなければなりません。次の例では、作成済みオブジェクトファイルをリンクしようとするときに、operator<< 関数が未定義であるというエラーが生成されます。その結果、operator<< 関数はインスタンス化されません。
array.h // operator<< 関数に対して未定義エラーを生成する。 #ifndef ARRAY_H #define ARRAY_H #include <iosfwd> template<class T> class array { int size; public: array(); friend std::ostream& operator<<(std::ostream&, const array<T>&); }; #endif array.cc #include <stdlib.h> #include <iostream> template<class T> array<T>::array() { size = 1024; } template<class T> std::ostream& operator<<(std::ostream& out, const array<T>& rhs) { return out << '[' << rhs.size << ']'; } main.cc #include <iostream> #include "array.h" int main() { std::cout << "creating an array of int... " << std::flush; array<int> foo; std::cout << "done¥n"; std::cout << foo << std::endl; return 0; }
コンパイラは、次の宣言を array クラスの friend である正規関数の宣言として読み取っているので、コンパイル中にエラーメッセージを表示しません。
friend ostream& operator<<(ostream&, const array<T>&);
operator<< は実際にはテンプレート関数であるため、template class array を宣言する前にこの関数にテンプレート宣言を行う必要があります。しかし、operator<< はパラメータ type array<T> を持つため、関数宣言の前に array<T> を宣言する必要があります。ファイル array.h は、次のようになります。
#ifndef ARRAY_H #define ARRAY_H #include <iosfwd> // 次の 2 行は operator<< をテンプレート関数として宣言する template<class T> class array; template<class T> std::ostream& operator<<(std::ostream&, const array<T>&); template<class T> class array { int size; public: array(); friend std::ostream& operator<<(std::ostream&, const array<T>&); }; #endif
C++ 標準は、テンプレート引数に依存する修飾名を持つ型を、typename キーワードを使用して型名として明示的に示すことを規定しています。これは、それが型であることをコンパイラが認識できる場合も同様です。次の例の各コメントは、それぞれの修飾名が typename キーワードを必要とするかどうかを示しています。
struct simple { typedef int a_type; static int a_datum; }; int simple::a_datum = 0; // 型ではない template <class T> struct parametric { typedef T a_type; static T a_datum; }; template <class T> T parametric<T>::a_datum = 0; // 型ではない template <class T> struct example { static typename T::a_type variable1; // 必要 static typename parametric<T>::a_type variable2; // 必要 static simple::a_type variable3; // 不要 }; template <class T> typename T::a_type // 必要 example<T>::variable1 = 0; // 型ではない template <class T> typename parametric<T>::a_type // 必要 example<T>::variable2 = 0; // 型ではない template <class T> simple::a_type // 不要 example<T>::variable3 = 0; // 型ではない template class example<simple>
「>>」という文字を持つものは右シフト演算子と解釈されるため、あるテンプレート宣言を別のテンプレート宣言内で使用する場合は注意が必要です。隣接する「>」文字との間に、少なくとも 1 つの空白文字を入れるようにしてください。
Array<String<10>> short_string_array(100); // >> は右シフトを示す。
Array<String<10 >> short_string_array(100);
Array<String<10> > short_string_array(100);