ATLAS日本基礎ネットワーク C++トレーニングコース

テンプレート

ここでの目標

目次

1.テンプレートとは

テンプレートとは、プログラムを記述するのに、型(組込型やクラスなど)をパラメータとして利用できるようにした仕組みです。なぜそのようなものが必要なのでしょうか。

例えばある整数配列の中から、もっとも大きな値を取り出す関数を考えてみましょう。それは例えば次のようになります。

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;
}

アルゴリズムとしては全く同じです。違うのはintdoubleに置き換えたところだけ。コードを再利用するという考え方から、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に対して重要な要求をしています。

このように、テンプレートで使えるクラスは、そのテンプレートの構文を読んだ上で利用者が必要なメソッドを用意することで初めて使えるようになります。

この関数テンプレートを使いましょう。上述の記述を含んだ上で、

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も要求を満たしています。

実習1.

いつものように作業場所を用意しましょう。

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;

}

プログラムの中で、atoirand_rといった標準関数を使用しています。これらはstdlib.hで定義されています。それぞれの意味はman atoiなどとやって調べてみてください。

実行してみましょう。

g++ main.cxx
./a.out 10

2143864633
2.14324e+09

同じ関数定義がintdoubleで使えました。

次に自分で用意したクラスを使う場合をやってみましょう。要求を満たすために引数付きコンストラクタとオペレータ=<を定義します。

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;となっています。組込型であるintdoubleの場合はそのままでも使えますが、それがたくさんのメンバーを持つ大きなクラスのオブジェクトだと、戻り値を用意するときにメンバーのコピーが行われ、効率が悪くなることが予想されます。T findMax( T * iarray, int nentries );ではなくて、T& findMax( T * iarray, int nentries );と定義し、参照を返すことで効率の低下を防ぐことが出来ます。このときの実装を考えてみてください。

実習2.

上述MyClassを記述するヘッダーファイルMyClass.hと実装ファイルMyClass.cxxを作り、main.cxxMyClassオブジェクト配列を追加して走らせてみましょう。

MyClass.hは例えば次のようになります。

#ifndef __MYCLASS_H
#define __MYCLASS_H
#include 

class 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;

}

          

もっときれいにintdoubleと同じ表現にそろえるためにはいくつか演算子をオーバーロードしてやる必要があります。トライしてみてください。

2.クラステンプレートの定義

先ほどは関数テンプレートを見てきました。今度はクラステンプレートについて見ていきましょう。クラステンプレートの場合、メソッドの戻り値や引数の型、あるいはメンバーデータの型をパラメータとして与えます。クラステンプレートとして良く目にするのはコンテナクラスです。コンテナは容器です。オブジェクトをつっこんでいったり、取り出したり、並べ替えたり、いろいろなメソッドあるいはアルゴリズムが用意されています。それらは任意の型のオブジェクトに対して一般的に適用可能ですから、クラステンプレートとして実装するのが自然です。

簡単なコンテナクラスのテンプレートを作ってみましょう。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は、比較演算子==を実装していなければならないことになります。些細な違いのように見えますが、非常に重大な差です。

実習3.

上述のMyContainer.hMyContainer.icxxを完成させてください。

皆さんは他のメソッドも実装してください。

ここまでの例では型のパラメータを一つだけ指定していましたが、複数の型を,で区切って<>の中に指定することが出来ます。

3.クラステンプレートの利用

クラステンプレートを定義したら、次はそれを利用する方法を考えましょう。これまでのクラスではクラス定義の読み込みは必要ですが、クラスの実装、メソッドの定義は分割コンパイルのおかげで読み込む必要はありませんでした。テンプレートの場合はどうなるでしょうか。クラステンプレートの定義は、まだ型がパラメータで与えられているだけなので実体化(インスタンシエート)されていません。具体的な型が与えられていないので分割コンパイルをすることが出来ません。実体化されたときに初めてメソッドのソースコードも展開されます。ということなので、クラステンプレートの場合は、クラス定義だけではなくメソッド定義も読み込まなければならないということになります。

#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)としてサポートされています。次回は標準テンプレートライブラリについて説明します。


前へ 上へ 次へ


2017年7月22日