Chapter 1
C++プログラムを簡潔に,美しく,そして高品質に
テンプレートプログラミングの世界

 C言語では,繰り返し記述を簡潔に行うためにマクロが使われてきた.しかしマクロは,ときに予期していない副作用が現れることがあり,その使用には十分な注意が必要とされる.これに対してC++言語では“テンプレート”の概念が導入され,型を意識しないプログラミングが可能なほか,コンパイル時に挙動を固定できるために,コードの実行効率が向上することも期待できる.  そこで本章では,今回の特集を概観すべく,テンプレートの概念を短いサンプルプログラムを交えながら解説する. (編集部)


 テンプレートプログラミングとは文字どおり「テンプレートを使ったプログラミング」のことです.しかし,それだけでは従来のプログラミングとどう違うのか,わざわざ習得するメリットがあるのかどうか,はっきりしません.そもそもテンプレートを使うと,どのようなメリットがあるのか,あるいは問題点が解決されるのか,そこが気になるところでしょう.

 そこで本章ではテンプレートの基本的な考え方や,テンプレートを使うことで従来のプログラムでは難しかった実装がいかに楽にできるかを説明していきましょう.また本特集で取り上げているSTLやBoostなどの背景としてかかわってくる,ジェネリックプログラミングの考えなども説明します.

同じ記述への対応

 プログラミングをするとかならず「似通った記述」や「同じ記述」に出会うものです.たとえば,リスト1のようなC言語でなされたコーディングを見てみましょう.

〔リスト1〕似通った記述のあるコード(1)
static void Test()
{
    int a1,a2,a3;
    
    a1 = f();
    if(a1 < 0){
        a1 = -a1;
    }
    a2 = g();
    if(a2 < 0){
        a2 = -a2;
    }
    a3 = h();
    if(a3 < 0){
        a3 = -a3;
    }
    if(a1 < a2){
        a1 = a2;
    }
    if(a1 < a3){
        a1 = a3;
    }
    printf("max absolute value = %d\n",a1);
}

 見るからに要領が悪そうなコーディングがされていますが,すぐに気づくのは,

  if(x < 0){
   x = -x;
  }

という記述パターンと,

  if(x < y){
   x = y;
  }

という記述パターンが見受けられ,それが鼻につく点です.最初のパターンは「絶対値にする」ですし,次のは「二つのうち大きい値にする」であることがわかります.プログラミングの基本的な戦術として「似通ったパターンや同じパターンがあれば共通化する」があります.難しい言葉が好きな人なら「抽象化」とでも表現するところでしょうか 注1.具体的には,

1)サブルーチンにする
2)マクロにする

あたりがC言語レベルでは常套手段でしょう.


注1:厳密にいうと「共通化」と「抽象化」は別物で,共通化は表面的な類似をまとめてしまう行為なのに対し,抽象化とは本質的なものをまとめてしまう行為を意味する.

サブルーチンの導入

 さきほどのプログラムで共通する記述パターンはリスト2のような二つの関数にできます.これらを利用して,さきほどのTest()を書き換えるとリスト3のようになります.最初の冗長な記述と比較すると,かなり行数が縮みました.もちろん,ここからさらに行数を縮めることも可能です.サブルーチンを導入することで見通しが良くなり,その結果,さらに見通しを良くするくふうを検討する余地ができました.

〔リスト2〕共通部分を関数にする(1)
int abs_int(int x)
{
    if(x < 0){
        x = -x;
    }
    return x;
}

int max_int(int x,int y)
{
    if(x < y){
        x = y;
    }
    return x;
}

〔リスト3〕共通部分の関数を利用する例
static void Test()
{
    int a1,a2,a3;
    
    a1 = f();
    a1 = abs_int(a1);
    a2 = g();
    a2 = abs_int(a2);
    a3 = h();
    a3 = abs_int(a3);
    a1 = max_int(a1,a2);
    a1 = max_int(a1,a3);
    printf("max absolute value = %d\n",a1);
}

マクロの導入

 しかし,こんな調子でなんでもかんでも共通部分をまとめられるわけではありません.その理由は“型”の存在です.たとえば,さきほどのTest()がリスト4のように記述されている場合はどうでしょうか.

〔リスト4〕似通った記述のあるコード(2)
static void Test()
{
    double d1,d2,d3;
    
    d1 = df();
    if(d1 < 0){
        d1 = -d1;
    }
    d2 = dg();
    if(d2 < 0){
        d2 = -d2;
    }
    d3 = dh();
    if(d3 < 0){
        d3 = -d3;
    }
    if(d1 < d2){
        d1 = d2;
    }
    if(d1 < d3){
        d1 = d3;
    }
    printf("max absolute value = %lf\n",d1);

 最初のTest()と似通っていますが,int型で取り扱っていたものが,すべてdouble型に変更されています.ということは,せっかく作ったサブルーチンabs_intもmax_intもそのままでは使えません.型キャストをしても使えません.なぜなら,double型で表現している数値データはint型にキャストされる段階で小数点以下が切り捨てられるので使い物にならないからです.

 となるとdouble型で使えるバージョンの関数を新たに作るハメになります.たとえば,リスト5のようになるでしょう.

〔リスト5〕共通部分を関数にする(2)
double abs_double(double x)
{
    if(x < 0){
        x = -x;
    }
    return x;
}

double max_double(double x,double y)
{
    if(x < y){
        x = y;
    }
    return x;
}

 しかしコードを見てわかるとおり,中身はabs_intとmax_intに似ている,というよりまったく同じ代物です.型が違うだけで,すでにある関数が流用できないという情けない状況です.

 C言語ではこういうときにマクロを使うとよいのはご存知のとおりです.リスト6のようにすればいいわけです.こうしておくとabs_とmax_はint型だろうがdouble型だろうが流用できるはずです.

 しかし注意しないといけないのは,マクロは単なる文字列の置き換えにすぎない点です.置き換えられた後のソースが期待しているものとかい離する可能性があります.たとえばリスト6のプログラムを縮めて,

〔リスト6〕共通部分をマクロにする
#define abs_(X) (((X) < 0) ? -(X) : (X))
#define max_(X,Y) (((X) < (Y)) ? (Y) : (X))

static void Test()
{
    int a1,a2,a3;
    
    a1 = f();
    a1 = abs_(a1);
    a2 = g();
    a2 = abs_(a2);
    a3 = h();
    a3 = abs_(a3);
    a1 = max_(a1,a2);
    a1 = max_(a1,a3);
    printf("max absolute value = %d\n",a1);
}

  a1 = abs_(f());

としたとします.もしもf()が2回以上呼び出したときに同じ値を返さない場合はどうなるでしょうか.この行はプリプロセッサを通過した後は,

  a1 = (((f()) < 0) ? -(f()) : (f()))

となるため,期待した結果にはならないでしょう.

 また,単なる置き換えであることを十分に意識し,演算子の優先順位にも注意する必要があります.マクロの記述で,やたらにカッコが多いのが目立つのはそのせいです.たとえば,abs_を,

  #define abs_(X) (X < 0) ? -X : X

にしてもいいのではないかと思う人がいるかもしれません.一見問題がないように見えますが,もしも,

  int a1 = 5;
  int a2 = -10;
  int a3 = abs_(a1+a2);

だったらどうでしょうか.この場合,最後の行は,

  int a3 = (a1+a2 < 0) ? -a1+a2 : a1+a2;

と処理され,その結果,

  int a3 = (5-10 < 0) ? -5-10 : 5-10;

となり,a3には期待している5ではなく,−15が入ります.

 いうまでもなくabs_intやabs_doubleを使った場合は,このような不具合は生じません.

 マクロは短い記述ですむのであれば便利ですが,長い記述は書くこと自体が苦痛ですし,後から検証やデバッグがやりにくいのも問題です.そのせいかどうかは知りませんが,C言語のキャリアが長い人でも,意外とマクロの使用頻度が少なかったりと,マクロが苦手な人は珍しくありません.

Copyright 2004 宮坂 電人