Super Technique 講座

パラメータ化クラス

「パラメータ化クラス」という機能は C や Java にはない。が、なかなか面白い機能であるので解説をしようと思う。→ Java 講座の「その他の多相」

どうやらこの機能は Ada の「汎用体」から来ているようだ。つまり、「すべての型」を総称して、1つの型であるかのように定義するが、利用の場面では個別に「具体的な型」を与えて、あらかじめ「具体的な型」による定義がされていたのかのような顔をするのが「汎用体」である。

どうやらこの考え方をオブジェクト指向言語に採り入れたのは、Bertrand Meyer のようである。Meyer が作った言語 Eiffel では、この機能が実装されており、この影響をどうやら Stroustrup が受けたようで、C++ にも実装されいる。ただし、C++ では「使わない方がよい機能(van der Linden「エキスパートCプログラミング」chap11)」に挙げられてしまうように、評判がよいわけではない。(まあ、Eiffel はその他にも「表明プログラミング」など面白い機能が満載である。言語ハッカーならば一度は使ってみると面白い。)

もう少しどんなものかを解説しよう。たとえばソートなどで、swap(交換)をさせたい場合がある。この時に、こんなマクロを書いた憶えのあるプログラマは多かろう。

#define swap(a,b)  {int tmp; tmp=(a);(a)=(b);(b)=tmp;}

しかし、これだと交換可能なのは int 型の変数(あるいは int にキャストして精度が落ちない short, char 型)に限られるし、ポインタを渡したりしたらコンパイラは文句をいう。そこで、ちょっとしたトリックを使う。

#define swap(type,a,b) {type tmp;tmp=(a);(a)=(b);(b)=tmp;}

    double d1, d2;
    .................
    swap( double, d1, d2 );
とやってやると、実際には swap の行が次のように展開される。
    {double tmp;tmp=(d1);(d1)=(d2);(d2)=tmp;}

となり、目出度く double 型の交換もできるのである。これはプリプロセッサが、単に字面しか見ていないために、可能なのである。

このマクロでは「あたかも引数として変数型を取る」かのような見かけを持っている。言い替えれば「変数型をパラメータとして取る関数」があるかのようである。これをマジメに実装したのが、「汎用体」である。

余談:ちなみにマジにCの関数で汎用 swap を実装してみよう。こんな感じかな。要するに swap だけなら、型情報は不要で、単にサイズだけが必要なだけであるから、第3引数にサイズを渡すようにしてやる。

void swap( void *a, void *b, size_t s ) 
{
    void *tmp = malloc( s ); /* ホントは alloca の方が良い */
    /* 効率を考えて、小さいサイズの場合用に固定のバッファを用意しておくとかね */
    memcpy( tmp, a, s );
    memcpy( a, b, s );
    memcpy( b, tmp, s );
    free( tmp );
}

........................
    int x = 10, y = 20;
    swap( &x, &y, sizeof(x) );
........................
    double x = 10.0, y = 20.0;
    swap( &x, &y, sizeof(x) );

Ada の汎用体(generic unit)は「変数型をパラメータとして取る手続きやパッケージ」を定義できる。まあ、こんなもんである。

generic
  type T is private;
procedure SWAP (A, B: in out T);

procedure SWAP (A, B: in out T) is
  TMP: T;
begin
  TMP := X; X := Y; Y := TMP;
end SWAP;

なんとなく、読めばわかると思う。つまり、T型は「任意の型」を受け取るためのシンボルに過ぎない。これは次のようにして、実際の「型」を渡して、現実の型に対する SWAP 手続きを実現する。

declare
  procedure SWAP_INT is new SWAP(INTEGER);
  procedure SWAP_CHAR is new SWAP(CHARACTER);

  I, J: INTEGER;
  A, B: CHARACTER;
begin
  I := 3; J := 5;
  SWAP_INT( I, J );
  A := 'a'; B := 'b';
  SWAP_CHAR( A, B );
end;

つまり、汎用体 SWAP に対して、具体的な型 INTEGER を与えて具現化し、その具現化した手続きに SWAP_INT という名前をつけている。これを実際のプログラム内で利用している。

これにオブジェクト指向の衣をまぶせると、「任意のクラスと型をパラメータとして取るメソッド」が定義できることになる。これをマジに実装したのが Meyer の Eiffel であり、Eiffel はその他にもいろいろと面白い機能が多い。

そこで新しいもの好きの C++ では、「テンプレート」としてこの「パラメータ化クラス」を実装した。こういう由来を考えてみると、要するに「パラメータ化クラス」というのは、マクロ風の機能であることが判るだろう。C++ の「テンプレート」は次のようにして使う。

#include <iostream.h> 

template<class T> class swapper {
  T tmp;
public:
  void swap( T *a, T *b ) {  tmp = *a; *a = *b; *b = tmp; }
};

void main( int argc, char **argv ) {
  swapper<double>sw;
  double x = 3.1415;
  double y = 2.718;
  sw.swap( &x, &y );
  cout << "x=" << x << " y=" << y << "\n";
}

この時、swapper クラスは「テンプレートクラス」であり、型のパラメータとして「T」というシンボルで代表される型を変数のように持つ。そして、「T」への代入(のような型の解決)は、「swapper<double>sw;」のようなインスタンス宣言で行う。だから、このインスタンス化が行われれば、swapperクラスは次のように宣言されていることになる。

class swapper {
  double tmp;
public:
  void swap( double *a, double *b ) {  tmp = *a; *a = *b; *b = tmp; }
};

なんとなく目出度いが、実はそれほど目出度いものではない。テンプレートクラスには大きな制限があり、落し穴が多すぎる。たとえば、次のようなプログラムさえうまくコンパイルできない。

template<class T> 
class swapper {
  T tmp;
public:
  void swap( T *a, T *b ) {  tmp = *a; *a = *b; *b = tmp; }
  int doit( );  /* 新しいメソッド.. T は使っていない */
};

int swapper::doit( )  { return 10; }  /* クラス定義の外にある */

int main( int argc, char **argv ) {
  swapper<double>sw;  /* 後略 */

この時、doit メソッドが class 宣言の外部に出ているのが問題なのである。これをうまく動かすためには、doit メソッドの宣言を次のように修正する。

templete<class T> int swapper<T>::doit( ) { return 10; }
/* T を使っていなくっても、冗長な宣言が必要 */

また、swapper クラスについてヘッダを作り、main 関数を含むファイルと別ファイルにしてやってもうまくいかない。これは要するにトリックが過ぎるのである。実際には、テンプレートクラスは「クラス」ではなくて「テンプレート」であるに過ぎないのだ。

(これをうまく動かすためには、export というキーワードでテンプレートクラスを修飾してやる必要があるが、このキーワードはまだほとんどのコンパイラで実装されていない、新しいキーワードである....無念!)

言い替えれば、「テンプレートクラス」は、一種のマクロであり、マクロの置換は「テンプレートクラス」として宣言されたブロックを単位として置換を行う。だから、この置換をうまく行うために、「テンプレートクラス」がそれ自体として完結したものでなくてはならないのだ。ANSI-C++ 標準では、次のように書いている。

クラステンプレート名は、プログラムにおいて唯一である必要があり、同じスコープにある他のテンプレート、クラス、関数、オブジェクト、値、もしくは型を参照するように宣言されていてもいけない。(r.14.2)

つまり、テンプレートクラスには通常のクラスでは考えられない程に、強烈な制限がかかっているのである。結論:C++ のテンプレートクラスは使い物にならない。Peter van der Linden の仰る事御尤も!である(勿論、汎用型の本家である Ada や、まともにこれに取り組んだ Eiffel ではこんな阿呆なことはない。C++ はいろいろと新しい機能を中途半端に取り入れている困った言語である...)。

中途半端第二弾:J2SDK1.5 から、Java でも「Generic型」の名前で導入されるようである。これも実はコンパイラがコンパイル時にチェックするだけで、ちょっと中途半端な実装であるようだ。詳細は「Deep Side of Java〜Iterator〜1.5で導入される Generic 型について」を参照のこと。



copyright by K.Sugiura, 1996-2006