ATLAS日本基礎ネットワーク C++トレーニングコース
ここでの目標
目次
テンプレートとは、プログラムを記述するのに、型(組込型やクラスなど)をパラメータとして利用できるようにした仕組みです。なぜそのようなものが必要なのでしょうか。
例えばある整数配列の中から、もっとも大きな値を取り出す関数を考えてみましょう。それは例えば次のようになります。
int findIMax( int * iarray, int nentries ) { int maxvalue = 0; int * p = iarray; for( int i = 0; i < nentries; i ++ ) { if( 0 == i ) maxvalue = * p; else if( maxvaue < * p ) maxvalue = * p; p++; } return maxvalue; }
これは整数型の配列について検査するものでした。実数型のものを探すものは
double findRMax( double * iarray, int nentries ) { double maxvalue = 0; double * p = iarray; for( int i = 0; i < nentries; i ++ ) { if( 0 == i ) maxvalue = * p; else if( maxvaue < * p ) maxvalue = * p; p++; } return maxvalue; }
アルゴリズムとしては全く同じです。違うのはint
をdouble
に置き換えたところだけ。コードを再利用するという考え方から、int
とかdouble
とかをパラメータに出来れば、いろいろな型に適用できるので便利ではないか。そこで導入されたのがテンプレートです。このケースは関数テンプレートと呼ばれます。
template <class T> T findMax( T * iarray, int nentries ) { T maxvalue = 0; T * p = iarray; for( int i = 0; i < nentries; i ++ ) { if( 0 == i ) maxvalue = * p; else if( maxvalue < * p ) maxvalue = * p; p++; } return maxvalue; }
この書式で、T
はクラス名のパラメータです。組込型を含む任意の型を指定することが出来ます。しかし、このテンプレートの定義はT
に対して重要な要求をしています。
T maxvalue = 0;
で、整数型の引数を持つコンストラクタが実装されていなければなりません。
maxvalue = * p;
という表現から、同じ型のクラスオブジェクトを右辺値に取る代入演算子=
が定義されていなければなりません。
maxvalue < *p
という表現から、同じ方のオブジェクトを右辺値に取る比較演算子<
が定義されていなければなりません。
このように、テンプレートで使えるクラスは、そのテンプレートの構文を読んだ上で利用者が必要なメソッドを用意することで初めて使えるようになります。
この関数テンプレートを使いましょう。上述の記述を含んだ上で、
int imax; int iarray[ 100 ]; ... //iarrayの中身を埋める作業 imax = findMax< int >( iarray, 100 );
これによって、class T
のところがint
に置き換えられ、実際のソースコードに展開されます。組込型int
は上述の要求をすべて満たしていますので、うまくいきます。同様に
double rmax; double rarray[ 100 ]; ... //rarrayの中身を埋める作業 rmax = findMax< double >( rarray, 100 );
組込型double
も要求を満たしています。
いつものように作業場所を用意しましょう。
cd ~/tutorial/cplusplus
mkdir l7
cd l7
まず定義です。findMax.icxx
を作ります。関数テンプレートは色々なケースで使いたいので別ファイルに定義します。.cxx
ではなく.icxx
等のサフィックスをつける場合があります。これは通常のメンバー定義用の.cxx
ではなく、テンプレートを使用するファイルがインクルードするために用意されたことをわかるようにするためです。
template <class T> T findMax( T * iarray, int nentries ) { T maxvalue = 0; T * p = iarray; for( int i = 0; i < nentries; i ++ ) { if( 0 == i ) maxvalue = * p; else if( maxvalue < * p ) maxvalue = * p; p++; } return maxvalue; }
使う側main.cxx
を書きます。
#include <iostream> #include <stdlib.h> #include "findMax.icxx" main( int argc, char ** argv ) { int iarray[ 100 ]; double darray[ 100 ]; unsigned int iseed = 0; if( argc > 1 ) iseed = atoi( *++argv ); for( int i = 0; i < 100; i ++ ) { iarray[ i ] = rand_r( & iseed ); darray[ i ] = (double) rand_r( & iseed ); } std::cout << findMax<int>( iarray, 100 ) << std::endl; std::cout << findMax<double>( darray, 100 ) << std::endl; }
プログラムの中で、atoi
やrand_r
といった標準関数を使用しています。これらはstdlib.h
で定義されています。それぞれの意味はman atoiなどとやって調べてみてください。
実行してみましょう。
g++ main.cxx
./a.out 10
2143864633
2.14324e+09
同じ関数定義がint
とdouble
で使えました。
次に自分で用意したクラスを使う場合をやってみましょう。要求を満たすために引数付きコンストラクタとオペレータ=
、<
を定義します。
class MyClass { public : MyClass( int i ); //整数の引数コンストラクタ MyClass( ); //引数無しコンストラクタも用意。 MyClass& operator = ( MyClass & rht ); //代入演算子 bool operator < ( MyClass & rht ); //比較演算子 private : int m_value; //簡単な例を考えます。 };
メソッドの実装は
MyClass::MyClass( int i ) { m_value = i; } MyClass::MyClass( ) { m_value = 0; } MyClass& MyClass::operator = (MyClass & rht ) { m_value = rht.m_value; return * this; } bool MyClass::operator < ( MyClass & rht ) { return m_value < rht.m_value; }
このクラスを先ほどの関数テンプレートで使用するには
MyClass tarray[ 100 ]; MyClass t; ... t = findMax<MyClass>( tarray, 100 );
ということになります。
実際の型を与えられてテンプレートがソースコードに展開されることをインスタンシエートと呼びます。
上述の例は実はあまり好ましくありません。
return maxvalue;
となっています。組込型であるint
やdouble
の場合はそのままでも使えますが、それがたくさんのメンバーを持つ大きなクラスのオブジェクトだと、戻り値を用意するときにメンバーのコピーが行われ、効率が悪くなることが予想されます。T findMax( T * iarray, int nentries );
ではなくて、T& findMax( T * iarray, int nentries );
と定義し、参照を返すことで効率の低下を防ぐことが出来ます。このときの実装を考えてみてください。
上述MyClass
を記述するヘッダーファイルMyClass.h
と実装ファイルMyClass.cxx
を作り、main.cxx
にMyClass
オブジェクト配列を追加して走らせてみましょう。
MyClass.h
は例えば次のようになります。
#ifndef __MYCLASS_H #define __MYCLASS_H #includeclass MyClass { public : MyClass( int i ); //整数の引数コンストラクタ MyClass( ); //引数無しコンストラクタも用意。 MyClass& operator = ( MyClass & rht ); //代入演算子 bool operator < ( MyClass & rht ); //比較演算子 MyClass& operator = ( int pvalue ) { m_value = pvalue; } void set( int pvalue ) { m_value = pvalue; } //setter method int get( ) { return m_value; } //getter method private : int m_value; //簡単な例を考えます。 }; #endif
MyClass.cxx
は
#include#include "MyClass.h" MyClass::MyClass( int i ) { m_value = i; } MyClass::MyClass( ) { m_value = 0; } MyClass& MyClass::operator = (MyClass & rht ) { m_value = rht.m_value; return * this; } bool MyClass::operator < ( MyClass & rht ) { return m_value < rht.m_value; } std::ostream & operator << ( std::ostream & os, MyClass & obj ) { os << obj.get(); return os; }
そしてmain.cxx
は
#include#include #include "findMax.icxx" #include "MyClass.h" main( int argc, char ** argv ) { int iarray[ 100 ]; double darray[ 100 ]; MyClass oarray[ 100 ]; unsigned int iseed = 0; if( argc > 1 ) iseed = atoi( *++argv ); for( int i = 0; i < 100; i ++ ) { iarray[ i ] = rand_r( & iseed ); darray[ i ] = (double) rand_r( & iseed ); oarray[ i ] = rand_r( & iseed ); } std::cout << findMax ( iarray, 100 ) << std::endl; std::cout << findMax ( darray, 100 ) << std::endl; MyClass omax = findMax ( oarray, 100 ); std::cout << omax.get( ) << std::endl; }
もっときれいにint
やdouble
と同じ表現にそろえるためにはいくつか演算子をオーバーロードしてやる必要があります。トライしてみてください。
先ほどは関数テンプレートを見てきました。今度はクラステンプレートについて見ていきましょう。クラステンプレートの場合、メソッドの戻り値や引数の型、あるいはメンバーデータの型をパラメータとして与えます。クラステンプレートとして良く目にするのはコンテナクラスです。コンテナは容器です。オブジェクトをつっこんでいったり、取り出したり、並べ替えたり、いろいろなメソッドあるいはアルゴリズムが用意されています。それらは任意の型のオブジェクトに対して一般的に適用可能ですから、クラステンプレートとして実装するのが自然です。
簡単なコンテナクラスのテンプレートを作ってみましょう。MyContainer.h
です。
#ifndef __MYCONTAINER_H__ #define __MYCONTAINER_H__ template <class T> class MyContainer { public: MyContainer( ); virtual ~MyContainer( ); int append( T * item ); //itemを最後につっこむ。その順番を返す int insert( T * item, int offset ); //itemをoffset番目につっこむ。 int find( T * item ); //itemと同じものがあればその順番を返す。 protected: T ** m_table; int m_tablesize; int m_entrycount; }; #endif
template <class T>
はテンプレートの宣言で、この句の次に出てくるもの(この場合はクラス定義)のスコープを持ちます。class T
はパラメータで、T
という仮の名前を与えています。
次にメンバー関数を定義します。MyContainer.icxx
とします。
#include "MyContainer.h" template <class T> MyContainer<T>::MyContainer( ) : m_tablesize( 0 ), m_entrycount( 0 ) { }
クラス名が、MyContainer
ではなく、パラメータ型T
を含んだMyContainer<T>
であることに注意してください。同様に他のメソッドも定義していきます。例えば、
template <class T> int MyContainer<T>::find( T * item ) { T ** p = m_table; for( int i = 0; i < m_entrycount; i ++ ) { if( *p == item ) return i; p ++; } return -1; }
このメソッド定義の中で、if( *p == item )
で、ポインター同士を比較しています。使い方によってこれは目的とあわないかもしれません。if( ** p == * item )
で、実際にオブジェクトの値同士の比較をすることになります。この場合、class T
は、比較演算子==
を実装していなければならないことになります。些細な違いのように見えますが、非常に重大な差です。
上述のMyContainer.h
とMyContainer.icxx
を完成させてください。
皆さんは他のメソッドも実装してください。
ここまでの例では型のパラメータを一つだけ指定していましたが、複数の型を,で区切って<>
の中に指定することが出来ます。
クラステンプレートを定義したら、次はそれを利用する方法を考えましょう。これまでのクラスではクラス定義の読み込みは必要ですが、クラスの実装、メソッドの定義は分割コンパイルのおかげで読み込む必要はありませんでした。テンプレートの場合はどうなるでしょうか。クラステンプレートの定義は、まだ型がパラメータで与えられているだけなので実体化(インスタンシエート)されていません。具体的な型が与えられていないので分割コンパイルをすることが出来ません。実体化されたときに初めてメソッドのソースコードも展開されます。ということなので、クラステンプレートの場合は、クラス定義だけではなくメソッド定義も読み込まなければならないということになります。
#include "MyContainer.icxx" //クラステンプレートの定義 #include "MyClass.h" //通常のクラスMyClassの定義 ... MyContainer<MyClass> thecontainer;
クラステンプレートの言語処理系への導入は比較的新しく、コンパイラ毎に処理の仕方が違います。この例ではソースファイルを直に読み込むようにしましたが、ATLASのコンベンションではヘッダーファイルMyContainer.h
の末尾に条件付きで読み込むようにし、利用者側のプログラムとしてはクラステンプレートであっても.h
を読み込むようにすることにしています。MyContainer.h
は
#ifdef HAVE_ANSI_TEMPLATE_INSTANTIATION #include "MyContainer.icxx" #endif
として、メソッド定義ファイルをヘッダーファイルから読み込む形にしています。これにより、メソッド定義を二重に読み込むことも防いでいます。
最近のC++では、コンテナクラスやいろいろなアルゴリズムが標準テンプレートライブラリ(STL)としてサポートされています。次回は標準テンプレートライブラリについて説明します。