Dr. GUI、テンプレートを語る

1998 年 9 月 7 日

今までの復習、これからの予定

前回 は、COM シリーズのまとめでした。これを「Dr. GUI の COM 基本講座」と呼んでもいいでしょう。次回からは、ATL(Active Template Library)を使って簡単な COM オブジェクトを作る方法について(ようやく)説明します。すでにお気づきのように、テンプレートは ATL にとってあまりにも重要な要素です。名前に「テンプレート」と入っているくらいですし。そこで、ATL を使った COM オブジェクトに話しを進める前に、C++ の比較的新しい機能であるテンプレートそのものをきちんと理解していることを確認しましょう。

新企画

このコラムと似たようなチュートリアルを読んだときに、内容を正確に理解できかどうか確信が持てなかったという経験はありませんか。そのような経験があるなら喜んでください。このコラムには、「やってみよう」という新しい企画を盛り込んでみました。この企画では、読者のみなさんに解いてもらいたい練習問題を出します。練習問題が解けたら、内容を完全に理解できたことになります。実践をすること以上に優れた学習方法はありません。そこで、テンプレートの説明を読み終えたら、「やってみよう」で実際にやってみてください。Dr.GUI では、各練習問題はほとんどのプログラマが 1 時間ぐらいで終了できるように設計しています。

テンプレート

テンプレートが必要な理由

さて、オブジェクトのコレクション(たとえば配列)を実装するためのクラスを作ることを考えてみてください。特定のタイプの配列を実装するクラスを作成するのは、比較的簡単です。難しいのは、任意のタイプの要素を持つ配列として使える汎用の配列クラスを作成することです。これに最も近い方法としては、共用体(たとえば VARIANT)の配列や void 型のポインタの配列を作る方法が考えられます。これらの方法のいずれもタイプに依存し、データを出し入れするためにはタイプ変換をする必要があります。

もう 1 つの方法としては、以前に基本クラスとして説明したクラスを使う方法があります。派生クラスは、void ポインタを受け付けるすべての関数のラッパーを実装します。必要なタイプ変換は、ラッパーが実施します。これはタイプに依存しませんが、ラッパー クラスをすべて作るのは面倒です(しかし、Java ではこれがタイプに依存しない唯一の方法です)。

では、コレクションの中のデータのタイプを、クラスに対するパラメータとして指定できるクラスを作ったらどうでしょうか。その方法なら、正しいパラメータを指定するだけで、必要なクラスが簡単に作れます。クラスを作るときにタイプをパラメータとして指定できるようにするのがテンプレートの役割なのです。

パラメータを使わない場合

Dr. GUI と同じように古いバージョンの BASIC でプログラミングをした経験がある人なら、これらの古いバージョンの原始的な関数呼び出しメカニズムであるあの非常に強力な(冗談です!)GOSUB ステートメントに欲求不満を覚えたことがあるはずです。

GOSUB では確かにサブルーチンにジャンプして戻ることができましたが、昔の BASIC にはグローバル変数しかなかったため、サブルーチンはあらかじめ定義した変数しか操作できませんでした。サブルーチンの変数に値をコピーし、サブルーチンを呼び出し、そして戻ってきたら変数をそこからコピーしていました。なんて大変だったんでしょう!

これを考えると、サブルーチンを呼び出して、必要な変数や式をすべて盛り込んだ引数リストを渡せるというパラメータ化された変数は、大きな前進でした。サブルーチンが正しいデータを操作するようにするのは、言語を実装する側の責任なので、値を出し入れするためのコードを作る必要がなくなります。

テンプレートは、パラメータを受け付けるクラスや関数のためのパターンを作れるようにして、同様の改革を実現しています。テンプレートを書いても、コードは生成されません。テンプレートをインスタンス化すると、そのときに渡したパラメータに適したクラスまたは関数が生成されます。テンプレートのパラメータには、クラスや型だけでなく、あらゆるタイプのものを指定できる点に注目してください。テンプレートの定義とインスタンス化の方法については、あとで説明します。

マクロ? まっぴらです

ここで、プリプロセッサのマクロ機能を使えば同じことが実現できると考えるかもしれません。しかし、たくさんのクラスを対象にマクロを使って経験があるならわかると思いますが、頭がおかしくなるくらいたいへんです。マクロは、パラメータのテキストを単純に置換するだけなので、式が何回も評価されたり、隠れた優先順位の問題が起きたりなど、たくさんの問題が発生します。また、C++ のマクロ機能は短いマクロを想定しているため、論理行は 1 行で表現できなければならないという制限があります。マクロを展開すると文字列長の制限を超えるコンテナ クラスも十分に考えられます。最後の、そして最も重要なことは、テンプレートがプリプロセッサではなくコンパイラによって評価されるため、テンプレートのおける型の不一致の問題はコンパイラのタイプ システムを使って解決されるという点です。

テンプレートの目的は ?

テンプレートは主にコレクション クラスで使われますが、ほかにも用途があります。たとえば、Visual C++ と ATL はどちらも、スマート COM ポインタのためにテンプレート クラスを提供します。これらのテンプレートの 1 つをインスタンス化すると、それを使ってポイントしたい特定の型のためにカスタマイズされたスマート ポインタが得られます。しかし、テンプレートにはこの 2 つ以外にも数多くの用途があります。クラスのコードを、別の型やパラメータで動作するように一般化できるのなら、その汎用性を表すテンプレートを作ることができます。

テンプレート ライブラリ

ATL、すなわち Active Template Library は、テンプレートを多用します。「テンプレート」という言葉が名前に入っているほどです。Visual C++ にも、一般的なアルゴリズムから文字列やコンテナに至るまで、驚くほど多様なテンプレートを備えた標準テンプレート ライブラリ(STL:Standard Template Library)があります。どちらも Visual C++ の資料に説明があるので確認してください。

ATL テンプレートの多くは COM インターフェイス実装のテンプレートです。また、ウィンドウ、ダイアログ ボックスからスレッド モデルに至るまで、多くのものを表すクラスやテンプレート クラスがあります。おもしろいことに、ATL にはコレクション テンプレートがありません

ATL を使って COM オブジェクトを作成する方法については、今後の記事で詳しく説明します。

関数やクラス ライブラリと比較した場合の利点

テンプレート ライブラリはソース コードで配布されるため、関数ライブラリに比べ大きな利点があります。コンパイラは、関数をインラインで使用でき、不要なコードを除外するなど、関数を最大限に最適化できます。優れたオプティマイザを使えば、テンプレート ライブラリから生成されたコードは、注意深く手作業で作成されたコードと同じくらい優れたものになります(作業量は格段に少なくてすみます)。ATL コンポーネントは処理が非常に高速であるにもかかわらず比較的簡単に書けるのは、この驚異的な効率性と、非常に高度な抽象化が組み合わされているためです。

つまり、テンプレート ライブラリを使えば、まさに必要としているコードを得ることができるのです。最近 MFC ソース コードを覗いたことがあればわかると思いますが、if ステートメントを使って多数の条件ブロックを形成して、コードがアプリケーションか、DLL か、ActiveX コントロールか、インプレース編集サーバーのどこで実行されるかに応じて異なる処理をするようになっています。MFC ライブラリは 1 つしかないため、すべて のケースのコードをライブラリに含める必要があり、ライブラリは非常に大きなものになります。結果として、MFC と静的にリンクするものもすべて非常に大きくなります。MFC がテンプレート ライブラリであったら、自分の条件に一致するコードだけが実行コードに含められます。ATL の最適化は驚くほど効率的です。たとえば、シングル スレッドのオブジェクトを構築する場合、参照カウントには組み込みのインクリメント演算子とデクリメント演算子が使われます。しかし、アパートメント モデル オブジェクトを構築する場合、参照カウントでは Win32 API のスレッドセーフな InterlockedIncrement と InterlockedDecrement. が使われます。ATL を使うなら、テンプレートをインスタンス化するときにどのケースを使ったらいいかを ATL に指示するマクロを含めることだけで、最も簡潔で単純なコードが得られます。

テンプレートの作成と使用

これで、テンプレートの素晴らしさが理解できたはずですので、次は、作成方法です。

テンプレートには、関数テンプレートとクラス テンプレートという 2 つのタイプがあります。独立型の関数テンプレートは、クラス テンプレートほど一般的ではありませんが、テンプレート クラスの中のメンバ関数はしばしばそれ自体がテンプレートとなっていることがあります。

関数テンプレート

関数テンプレートを使用すると、1 回の簡単な定義で、オーバーロードされた関数のファミリ全体を定義することができます。

関数テンプレートの定義

たとえば、min 関数テンプレートは次のように定義できます。

  //min の関数テンプレート
template <typename T> inline T min(T a, T b) { return a < b ? a : b; }

任意のタイプ T について、min(T, T) はタイプ T の 2 つのパラメータを受け取り、タイプ T の値を戻します。min は、オペランドとしてパラメータを使って、タイプ T に定義されているすべての演算子、< を呼び出します。演算子 < がゼロを戻した場合(「偽」を意味する)、min は 2 番目のパラメータを戻します。演算子 < がゼロ以外の値を戻した場合(「真」を意味する)、min は最初のパラメータを戻します。指定されたタイプに対応する演算子 < がない場合には、コンパイラがエラーを代し、テンプレートは失敗します。テンプレートは、クラス T が演算子 < を、「小なり」という意味以外の意味に定義している場合には、何の通告もなく失敗します。

C++ の古いバージョンには新しい "typename" キーワードがありませんでした。その代わりにキーワード "class" を使います。"typename" の方が明確なため、Dr. GUI としてはこちらを使うことをお勧めしますが、これは自分でコードに合わせて選択してください。古いコンパイラで "typename" を含むコードをコンパイルする必要がある場合には、"#define typename class" と指定すれば問題ありません。Dr. GUI の例では、これによって悪い結果は何も生じないので、常にこの方法を使っています。

古いキーワードを使った場合のテンプレートは次のようになります。

  template <class T> inline T min(T a, T b) { return a < b ? a : b; }

テンプレートを使った場合と、次のようなマクロによる実装との違いに注目してください。

  #define min(a, b) ((a) < (b) ? (a) : (b))

  • テンプレートでは、パラメータのタイプと、戻されるタイプは必ず同じになります。マクロの場合は、a と b は異なるタイプになることがあります。これは、異なるタイプのクラスを扱う場合には本当に困ることです。エラーは、マクロ「呼び出し」のソース コードの中で現れるので、原因を知るのも大変です。

  • テンプレートは嫌らしいテキスト展開の問題のいくつかも処理してくれます。特筆すべきことは、パラメータ参照ごとに括弧で囲む必要がないことです。たとえば、最初のパラメータとして "x == y" を渡した場合など、優先順位を混乱させる式を渡してしまうことがあります。括弧がないと、マクロの場合には "(x == (y < b)" となり、(x == y) < b とはなりません。マクロ呼び出しを別の式の中に組み込む場合には、式全体を括弧で囲む必要があります。テンプレートの場合には、C++ の文法を理解していないプリプロセッサではなく、これを理解しているコンパイラによってインスタンス化されるため、これらの括弧はどれも必要ありません。

  • テンプレートでは、あらゆる関数と同様に、すべてのオペランドを 1 度だけ評価することが保証されています。マクロは、2 つのパラメータのいずれかを 2 度評価します。"a++" のような副作用のある式をパラメータの 1 つとして渡した場合には、これにより問題が生じることがあります。

関数テンプレートのインスタンス化

関数テンプレートのインスタンス化は非常に簡単です。関数を呼び出すだけです。

   // min テンプレートのテスト
 printf("min(3, 5) is %d\n", min(3, 5)); // int
 printf("min(3.7, 8.3) is %f\n", min(3.7, 8.3)); // double

しかし、問題が 1 つあります。コンパイラはテンプレートをインスタンス化するときに、オペランドのタイプ変換は行いません。たとえば、"min(5, 3.5)" では 2 つのパラメータのタイプが異なるため、コンパイラ エラーが発生します。

   // min(3.7, 8) はエラーになります。
 // テンプレートのインスタンス化では変換は許されないためです。

しかし、もちろんタイプ変換機能を使って両方のパラメータを同じタイプにすることができます。または、関数を呼び出すときにどのテンプレート インスタンス化処理を使うかを指定することができます。

   printf("min(3.7, 8) is %f\n", min<double>(3.7, 8));

ここで 1 つ警告しておきます。パラメータが別になれば、必ず新しいインスタンス化処理が生成されることになります。たとえば、long と int を対象に min を使うと、データ形式が同じである(少なくとも Win32 では)にもかかわらず、またコードが同じであるにもかかわらず、2 つの別々のインスタンス化処理が得られることになります(しかし、頭のよいコンパイラやリンカを使えば、コードが同じであることから生じるコードの膨張を避けることができます)。

どんなに優れたコンパイラやリンカでも解決できない問題が 1 つあります。タイプ間の変換が可能であった場合に生じる問題です。たとえば、両方のパラメータを long double に変換し、min の結果を long double として戻し、これを呼び出し元が目的のタイプに変換すれば、どのタイプの数値でも min を使うことができます。これは効率の悪い方法ですが、可能ではあります。

テンプレートの仕様では、この問題を保守的に扱っています。パラメータが「少しでも」異なるのなら、新しいインスタンス化処理が生成されます。通常は、この方法のほうが安全です(最善ではないとしても)。これらの関数をすべて生成するのが問題であれば、次に説明する方法で、テンプレートを特殊化できます。

明示的な特殊化

タイプに応じてテンプレートをオーバーライドすることもできます。この例で言えば、テンプレートを適用するクラスに演算子 < がない場合、またはこの演算子の意味が適合しない場合にオーバーライドするとよいでしょう。たとえば、アドレスではなく、ポインタが指している文字列を検査するために 2 つの char ポインタを渡して min を呼び出したい場合には、「明示的に特殊化」したテンプレートを作成することができます。

  // 明示的な特殊化
char * min(char *a, char *b) {
 return (strcmp(a, b) < 0) ? a : b;
}

この特殊化は、両方のパラメータが char ポインタの場合に呼び出されます。

  // 明示的な特殊化のテスト
printf("min(\"abc\", \"def\") is %s\n", min("abc", "def"));

また、特定のインスタンス化処理を使用するように明示的に指定することもできます。たとえば、次のように指定できます。

   // 特定の関数を呼び出せる、強制的に変換
      // 8 を double に変換
 printf("min<double>(3.7, 8) is %f\n", min<double>(3.7, 8));

クラス テンプレート

関数テンプレートはいいものですが、もっと便利なのはクラス全体のテンプレートです。これを使用すると、必要とするテンプレートへのパラメータを指定して、簡単にクラス全体をインスタンス化することができます。

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

次のように、簡単な配列テンプレートを定義できます。

  // インラインとクラスの外部の両方で宣言された
// 非常に簡単で高速な配列テンプレート関数
template <typename T, int N> class FastArray {
 public:
 inline FastArray();
 ~FastArray() { delete pArray; }
 inline void Set(int i, T val);
 T Get(int i) { return pArray[i]; }
 protected:
 T *pArray;
};

これは、配列のためにメモリを動的に確保する(デストラクタがメモリを解放するので、確保してくれなければ困るのですがね)ための、非常に簡単な配列クラス テンプレートです。このクラスは、Get および Set 関数も提供します。

関数の半分は、インラインとして定義されています。これは、コードの本体が実際にはクラス宣言の中にあるからです。残りの半分は、本体がもっと後にならないと現れないにも関わらず、インラインとして明示的に定義されています。ようするに、暗黙であれ、明示的であれ、すべてインラインになっています。

このテンプレートには、2 つの パラメーターがあります。配列の要素のタイプと、配列のサイズです。テンプレートの中に、"int N" のようにタイプが明示的に指定されているパラメータがあるのを見て驚いたかもしれませんが、これがとても便利なのです。そしてパラメータには、int だけではなく、あらゆるタイプを指定できます。1 つの制限は、テンプレートをインスタンス化できるようにするためには、コンパイル時にすべてのパラメータの値が既知でなければならないという点です。つまり、値は定数にする必要があります。

ちょっと脇道:int パラメータは、テンプレートのユーザーが汎用のテンプレートをインスタンス化する方法を選べるようにするために便利に使えます。たとえば、"which" という int パラメータを次のように使えます。

  template <typename T, int which> class Foo {
 void f() {
  switch (which) {
   case 1: DoOne(); break;
   case 2: DoTwo(); break;
   // etc.
   default: DoDefault(); break;
  }
  // etc.

これではコードが膨れ上がってしまうと思うかもしれませんが、そうはなりません。which は定数なので、switch ステートメントはコンパイル時に評価され、オプティマイザが(少なくともリリース ビルドをするときには)未使用のコードと、switch ステートメントの評価コードをすべて除去します。つまり、この switch ステートメントからは 1 つの関数呼び出しが得られます。それ以上にはなりません。

これがテンプレートの最も優れた性質なのです。コードの中であらゆるケースを処理することができるにも関わらず、実際に使われたケースだけがコードを生成するということです。これは、リリース ビルドの中では無効にしておきたいコードをデバッグするときに非常に便利です。同じことを別の方法で実現するやり方をまたあとで説明します。

クラス外部の定義

テンプレート クラスの内部で関数を定義しない場合には、あとで定義する必要があります。通常のメンバー関数の場合には、構文は通常の関数テンプレートに似たものになります。

  template <typename T, int N> void FastArray<T, N>::Set(int i, T val) {
 pArray[i] = val;
}

もっとも大きな違いは、クラスの名前(とすべてのメンバ関数)を完全に指定する必要があるということです。クラス名にはパラメータを含める必要があります。この場合には "FastArray<T, N>" です。

これは、FastArray と呼ばれるテンプレートでは、FastArray<Param1, Param2> はクラスの名前になることを意味します。つまり、クラス名を指定できるところでは、完全指定のテンプレート名を指定できるということです。たとえば、この例のようにスコープ指定演算子の前や、オブジェクトを宣言するとき、派生リストの中、そしてテンプレート パラメータの中でさえも指定できます(マクロを使った場合には、クラス名とクラスの実装で、個別のマクロが必要になります。とても面倒です)。

クラスの外部で定義するコンストラクタとデストラクタの命名規則には多少驚くかもしれません。

  template <typename T, int N> FastArray<T, N>::FastArray() {
 pArray = new T[N];
}

2 回目に現れるクラス名は、"FastArray<T, N>" ではなく、"FastArray" と指定されている点に注目してください。コンストラクタとデストラクタの名前は、テンプレートのどのインスタンス化処理でも同じなので、テンプレート パラメータを繰り返す理由はありません。また、これを繰り返すようにしたら、完全に同じように指定する必要があるので間違いも起きやすくなります。

クラス テンプレートのインスタンス化

クラス テンプレートは関数テンプレートとは異なり、自動的にインスタンス化されることはありません。テンプレートからインスタンス化するクラスを参照するたびに、パラメータを提供する必要があります。そしてパラメータが変われば、コンパイラは新しいテンプレートをインスタンス化します。たとえば、FastArray<int, 5> と FastArray<int, 6> は 2 つの別々の配列タイプです(この問題は、テンプレートではなくコンストラクタにサイズを渡すようにして、解決できます。方法はまたあとで説明します)。

配列テンプレートは次のようにインスタンス化して、使うことができます。

  // 配列のテスト
const int nSize = 5; // 全体のサイズ
printf("Fastarray--no error checking\n");
FastArray<char, nSize> fachar;
for (int i = 0; i < nSize; i++) // OK
 fachar.Set(i, 'A' + i);
for (i = 0; i <= nSize; i++) // 1 大きすぎだが、例外にはならない
 putchar(fachar.Get(i));

2 番目のループでは配列のインデックスを 1 つオーバーしていますがエラーは発生しません。次のテンプレートでは、境界検査をします。

インスタンス化されるクラスを参照しても、これをインスタンス化することになるとは限りません。クラスは、オブジェクトを作成したとき、またはオブジェクトへの参照を作成したときにのみインスタンス化されます。また、メンバ関数は、呼び出されない限りインスタンス化されません。ほかから呼び出されるライブラリを作成する場合を除けば、通常はこれで十分です。ほかから呼び出されるのなら、ファイル スコープで次のように宣言するだけで、クラス テンプレート全体を明示的にインスタンス化できます。

  template class FastArray<double, 30>; // すべてのメンバー関数をインスタンス化

上記のように、ファイル スコープで宣言すれば、ファイル スコープと個々のメンバ関数のどちらでも、テンプレート関数を強制的にインスタンス化できます。

Microsoft C++ は、クラスまたは関数をインスタンス化することなく、上記のようなテンプレート宣言で "extern" キーワードが使えるような拡張されています。これにより、実際にインスタンス化をせずに、テンプレート クラスのインスタンス化処理を事前宣言できます。

クラス テンプレートからの継承

Dr. GUI が "class_template_name<parameters>" という表現がクラス名であり、クラス名が使えるところならどこでも使えると言ったのは本気です。「どこでも」という中には、クラス派生リストも含まれます。そこで、新しいテンプレート クラスを派生して、配列クラスにエラー検査を追加することにします。

  template <typename T, int N> class RobustArray : FastArray<T, N> {
 public:
 RobustArray() { if (pArray == NULL) throw "Not enough memory"; }
 void Set(int i, T val) {
  BoundsOK(i);
  FastArray<T, N>::Set(i, val);
 };
 T Get(int i) {
  BoundsOK(i);
  return FastArray<T, N>::Get(i);
 }
 protected:
 void BoundsOK(int i) { // だめなら例外をスローする
  if (i >= N || i < 0) throw "Array index out of bounds";
 }
};

基本クラスではサイズ指定パラメータが必要でした。したがって、継承リストの中で使えるように、派生クラスでもサイズ指定パラメータが必要です。配列のサイズだけ変更されたときに別の配列クラスが生成される問題はあとで解決しますが、このクラスではしません。

エラー検査については、基本クラスのメンバ関数の呼び出し前、あるいはあとにエラー検査ルーチンを実行することを方針としています。GetSet の場合には、実際の処理をするために基本クラスのメンバ関数を呼び出す前に境界を検査します(基本クラスを参照する構文を見てください。これはテンプレート式の使用のもう 1 つの例です)。

このクラスではデストラクタを定義しません。基本クラスのデストラクタは自動的に呼び出され、必要にして十分な処理を実行するので、私たちが追加するべきものはありません。

コンストラクタの仕組みには、多少理解しにくいところがあります。基本クラスのコンストラクタは、派生クラスのコンストラクタより先に自動的に呼び出されます。派生クラスのコンストラクタの中で new を呼び出していないのはこのためです。あと必要なことは、new が NULL を戻していないことを確認することだけです。

このテンプレートをインスタンス化し、テストするためのコードは、基本クラスのコードと似ていますが、スローされる例外を処理するために、try/catch ブロックを含めている点だけが異なります。

   try {
  printf("\n\nRobustArray has error checking\n");
  RobustArray<char, nSize> rachar;
  for (i = 0; i < nSize; i++) // OK
   rachar.Set(i, 'A' + i);
  for (i = 0; i <= nSize; i++) // 1 だけ大きすぎる。例外をスローする
   putchar(rachar.Get(i));
  printf("\n\n");
 }
 catch (char * str) { // 対処したいのでキャッチする
  printf("\n\a%s exception (RobustArray)\n\n", str);
 }

max のテンプレート

すでに説明したように、テンプレート クラスのインスタンス化処理は、普通のクラスが使えるところならどこでも使えます。テンプレートのパラメータにも使えます。

  RobustArray<RobustArray<char, 5>, 10> five_by_ten;

このテンプレートは RobustArray の RobustArray を宣言します。

山括弧に注意してください。並べて 2 つ指定するときは、間にスペースを入れる必要があります。さもないと、コンパイラはこれを >> 演算子と誤って解釈してしまいます。また、山括弧を含む式をテンプレートに渡す必要がある場合には、括弧を使ってください。

  Foo<Goo<String,(a > b)> >; // 括弧とスペースが必要な点に注意

テンプレートを使ってカスタマイズされたコードを生成する

テンプレートに渡したパラメータに応じて、エラー検査をしたりしなかったりする配列クラスがあれば便利だとは思いませんか。さらに言えば、エラーが発生したときに呼び出してもらいたい関数を作成することで、エラー処理をカスタマイズできたらいいと思いませんか。

テンプレートに関数(実際は関数ポインタ)を 1 つ 2 つ渡して、テンプレート クラスのメンバから関数を呼び出すことは可能です。

しかし、この名医にはもっといい考えがあります。必要なエラー検査関数をメンバとするクラスを作成するのです。たとえば、エラーが発生した場合に例外をスローするクラスは、次のようになります(エラー検査ごとに、個別のスタティックなメンバ関数があります)。

  // 柔軟な配列テンプレートのヘルパー クラス。エラー時に例外を発生
class ErrorThrowsException {
 public:
 static void BoundsOK(int i, int N) {
  if (i >= N || i < 0) throw "Array index out of bounds";
 }
 static void CheckNullPointer(void *p) {
  if (!p) throw "Not enough memory";
 }
};

別のクラスを置き換えることにより、エラー検査を排除できます。

  // 柔軟な配列テンプレートのヘルパー クラス。エラー検査なし
// リリース ビルド時には、コンパイラがこれらのインライン関数を
// 最適化して、最終的には何も残らない状態にする。
class NoChecks {
 public:
 static void BoundsOK(int i, int N) { }

 static void CheckNullPointer(void *p) { }
};

あとは、エラー検査を実行するためにこれらのクラスの 1 つを使う配列クラスのテンプレートを作成するだけです。

  // 高速または安全な配列、エラー クラスの 1 つを使用
// エラー クラスのデフォルト パラメータに注意。
template <typename T, typename E = ErrorThrowsException>
class FlexArray {
 public:
 FlexArray(int N_) {
  N = N_;
  pArray = new T[N];
  E::CheckNullPointer(pArray);
 }
 ~FlexArray() { delete pArray; }
 void Set(int i, T val) {
  E::BoundsOK(i, N); // だめなら例外をスローする
  pArray[i] = val;
 };
 T Get(int i) {
  E::BoundsOK(i, N); // だめなら例外をスローする
  return pArray[i];
 }
 protected:
 T *pArray;
 int N;
};

このテンプレートは、まだ説明していない機能を利用しています。テンプレート パラメータは、デフォルトの引数値を持つことができるのです。ここでは、エラーが発生した場合に例外を投じるクラスをデフォルトとして指定しています。

また、配列のサイズのためのテンプレート パラメータはない点に注意してください。つまり、要素タイプとエラー検査クラスの各組み合わせごとに 1 つの FlexArray クラスがありますが、すべてのサイズに対してクラスは 1 つしかなく、個々のサイズごとにクラスがあるわけではありません。これによりコードの量が大幅に減ります。テンプレートにサイズを渡す代わりに、コンストラクタにサイズを渡して、メンバ変数の中に記憶させています。

2 つのエラー検査クラスはポリモーフィックに見えますが、そうではありません。ポリモーフィズムとは、実行時に動的に決まるオブジェクトに応じて、どの関数を呼び出すかを実行時に選ぶことを意味します。しかしここでは、テンプレート パラメータにだけ基づいて、どの関数を呼び出すかをコンパイル時に決定します。ポリモーフィックに見えながらそうでないというところから、Dr. GUI は、これを冗談で「コンパイル時ポリモーフィズム」と呼んでいます。

コンパイラは、継承機能を利用した場合とは異なり、クラスが正しい関数を実装しているかどうかを確認することができません。しかし、これはたいした問題ではありません。配列クラスの関数呼び出しがうまく機能すれば、パラメータ タイプが違ってもうまく機能するからです。

配列テンプレートは次のコードを使ってテストできます。

   printf("FlexArray, no checks\n");
 FlexArray<double, NoChecks> fadouble1(nSize);
 for (i = 0; i < nSize; i++)
  fadouble1.Set(i, i);
 for (i = 0; i <= nSize; i++) // インデックス オーバー、検査なし
  printf("%f ", fadouble1.Get(i));
 printf("\n\n");
 try {
  printf("FlexArray with error checking\n");
  FlexArray<double> fadouble2(nSize);
  for (i = 0; i < nSize; i++)
   fadouble2.Set(i, i);
  for (i = 0; i <= nSize; i++)
   printf("%f ", fadouble2.Get(i)); // インデックス オーバー、例外をスローする
  printf("\n\n");
 }
 catch (char * str) {
  printf("\n\a%s exception (FlexArray)\n\n", str);
 }

このコードでは、2 つの配列クラスをインスタンス化します。どちらも配列要素のタイプが double です。一方のクラスは、例外をスローするクラスを使い、もう一方は何もしないエラー検査クラスを使います。コンパイル時のテンプレート パラメータと、実行時のコンストラクタ パラメータでは、配列のサイズが違うことに注意してください。

リリース ビルドのアセンブラ コードを見ると、テンプレートとインライン関数の魔法がわかります。たとえば、set ループ内のステートメントは次のようにコンパイルされます。

  ; C++ code: fadouble1.Set(i, i);
fild  dword ptr [i]
inc   dword ptr [i]
fstp  qword ptr [ecx]
add   ecx,8

実際のところ、これら 4 つの命令は、Set 関数とは何の関係もない 2 つの命令(inc と add)を含んでいるので誤解を招くかもしれません。この 2 つの命令は、外側のループの "i++" 部分を実装します(1 つは i をインクリメントし、1 つは double の配列へのポインタを 8 バイトだけインクリメントします)。

Set 関数の実際の機能は、int オペランドを double に変換し、その結果を浮動小数点スタックの一番上に記憶する fild 命令と、浮動小数点スタックの一番上にある数値をメモリに記憶し、スタックをポップする fstp 命令です。これら 2 つの命令は、その結果と同じように簡潔です。Set への呼び出しがなくなり、2 つの命令で置き換えられます。BoundsOK への呼び出しは、BoundsOK 関数が何もしないのでなくなります。Dr. GUI が、サイズとスピードの両方の性能を気にする患者に、テンプレートを上手に使うことをお勧めする理由がわかってもらえたでしょうか。

このエラー検査では不十分な場合はどうでしょうか。たとえば、DEBUG が定義されているときにはエラー検査をして、それ以外のときにはしたくないような場合にはどうしたらいいのでしょう。DEBUG シンボルをテストするエラー検査クラスを作るのは簡単です。また、DEBUG は定数なので、コンパイラが不要なコードをすべて排除し、余分なオーバーヘッドは残りません。クラスを作成し、配列をインスタンス化するときにクラスを渡すだけです。

明示的な特殊化

テンプレート関数と同じように、特殊化したテンプレート クラスを作成できます。次のコードのように、効率を高めるためにこれを行ってもかまいませんし、ただの興味からやってみてもかまいません。

  // 式テンプレートは再帰、特殊化を使用
// インスタンス化はコンパイル時に完全に評価される
template <int N> class Factorial {
public:
 enum {value = N * Factorial<N-1>::value};
};
// 1 の特殊化、再帰的評価の終了
class Factorial<1> {
public:
 enum {value = 1};
};

これは、見かけどおりに機能します。コンパイラは毎回 1 だけ減る N の値について、テンプレートを再帰的に評価します。値が 1 になると、コンパイラは特殊化されたテンプレートを使用するため、再帰が終了します。

次のように書くだけで、階乗を求められます。

  int f12 = Factorial<12>::value;

コンパイラは階乗を評価し、コンパイラが計算した定数を変数に移動する MOV 命令を 1 つ生成します。これより以上速い方法は考えられません。これらのテンプレートは、「式テンプレート」と呼ばれることがあります。

やってみよう

さて、テンプレートをいくつか紹介したところで、練習問題はどうですか。やってみてください。

  • Swap を実装する関数テンプレートを作ってください。実装には、どのような制約があるでしょう。

  • お好きなコレクション テンプレートを作ってください。単純にしておくこと!

  • DEBUG フラグを尊重する FlexArray テンプレートのための追加のエラー検査クラスを作ってください。 コードをテストしてください。新しく作ったものも含めて、これらのエラー検査クラスを再利用するように、自分のコレクションを改造してみましょう。

  • N 番目のフィボナッチ数のような式を評価する式テンプレートを作成してください。n 番目のフィボナッチ数は、n-1 番目、および n-2 番目のフィボナッチ数の合計です。ただし、n = 1 と 2 の場合は例外で、その場合フィボナッチ数は 1 です(ヒント:クラスは 3 つ必要です)。

Dr. GUI の回答コードは次回のコラムで提供されます。

今までの復習、これからの予定

今回は、高度な使用法も含めて、テンプレートについて説明しました。これを実際に使ったのだから、理解もできたはずです(上記の「やってみよう」の節を参照)。次回は、ついに ATL について説明します。とても簡単な ATL コントロールを構築することになります。