テンプレート

 ここまで述べたC言語での二つの共通化テクニック,すなわち共通コードの関数化とマクロはいずれも便利な反面,限界を持っています.関数化では型をパラメータ化できないという限界がありますし,マクロは型の問題から逃れられても単なるソースの置き換えにすぎないという限界があります.

 しかし,C++になると関数化とマクロの問題を解決するテンプレートというしくみが利用できます.テンプレートを一言で説明するなら,

・型をパラメータ化できるしくみ

です.しかし後で説明しますが,型だけではなく,予想もしないものまでパラメータ化できるため,C言語はもちろんのこと,C++のエキスパートを自負する人にも想像がつかない意表をついた使い方ができます.C++を使っているのに,まるで別のプログラミング言語を使っているような錯覚を感じるほどです.

 話を戻して,さきほどのabs_,max_をテンプレートで書き直したabsolute,maximumをリスト7に示します.

〔リスト7〕共通部分をテンプレートにする
template <class T>
T absolute(T iD)
{
    return (iD < 0) ? -iD : iD;
}

template <class T>
T maximum(T i1,T i2)
{
    return (i1 < i2) ? i2 : i1;
}

 Tというシンボルはパラメータであり,ここがintであればint型用,doubleであればdouble型用になります.Test()はリスト8のように書き換えられます.

 ここでテンプレートの基本的な書式を見てみましょう.テンプレートはその開始を意味する「template」で始まり,直後に「<」と「>」で囲んだ部分に必要なパラメータを記述します.パラメータが複数あるのなら「,」で区切ります.注意してほしいのは,パラメータを指定するときに「class」を前につけていますが,これは必ずしもパラメータがクラスであることを意味しません注2 .記述上の約束でしかありません.事実,リスト8で示している例ではint型を指定していますが,いうまでもなくintはクラスではありません.

〔リスト8〕テンプレートを利用する例
static void Test()
{
    int a1,a2,a3;
    
    a1 = f();
    a1 = absolute<int>(a1);
    a2 = g();
    a2 = absolute<int>(a2);
    a3 = h();
    a3 = absolute<int>(a3);
    a1 = maximum<int>(a1,a2);
    a1 = maximum<int>(a1,a3);
    std::cout << "max absolute value = " << a1 << std::endl;
}

 パラメータの記述の後にclassまたは関数が続きます.つまりテンプレートには,

・クラステンプレート
・関数テンプレート

の2種類があることになります.

 一方,テンプレートを利用する側は,テンプレートにパラメータを与えて実体化させる必要があります.これを「テンプレートのインスタンス生成注3 」と称します.このときに,どの型で生成すべきかを指示するためにテンプレート名に続けて「<」と「>」で囲んだ部分に型を指定します.

 なお,テンプレートに関する詳細な仕様については「プログラミング言語C++第3版」の第13章「テンプレート」も参照してください.


注2:C/C++でよく見受ける一つのシンボルに複数の意味を持たせてしまう悪しき伝統(?)であろうか.最新の規格ではclassだけではなくtypenameも使えるので混乱するのが嫌な人はそちらも検討すべきと思われる.
注3:template instantiation.「プログラミング言語C++第3版」のC.13.7「インスタンス生成」を参照.

改良されたマクロ

 テンプレートは「改良されたマクロ」という見方もできます.マクロの特徴である,

・同じことの繰り返しをまとめられる
・パラメータを指定することでバリエーション(変型)を作りやすくなる
・実行時ではなくコンパイル時に挙動を固定できるので,できあがったコードの実行効率が向上する

といったメリットが期待できます.その反面,

・どのようなコードに展開されるか予測がつかないことがある
・エラーメッセージが難解で,どこで記述を失敗したのか簡単にわからないことがある
・できあがったコードのサイズが膨れ上がる

というデメリットも出てくるので,かならずしも手放しで喜べない場合があります.

 エラーメッセージが難解になるのは,エラーが発生するのがテンプレートを使用した箇所ではなく,コンパイラがコードを展開しようとした箇所,つまりテンプレートの実装部分,テンプレートの利用者が関知していない部分だからです注4

 できあがるコードのサイズが増えるのは指定したパラメータごとにコードが作られるせいで,これはテンプレート自体の特性なので,根絶することはおそらく不可能に近いでしょう.

 さきほど,キャリアが長い人でも意外とマクロの使用頻度が少なかったり,マクロが苦手な人は珍しくないと述べたとおり,マクロやテンプレートのプログラミングはある意味,通常のプログラミングのセンスとは別のセンスを要求されます.下手をするとプログラミングではなくて,パズルあるいは手品をやっているような錯覚にとらわれることさえあります.

 また絶対に勘違いしてほしくないのですが,C++の仕様だからといってテンプレートはオブジェクト指向だとは思わないことです.どちらかというと関数パラダイムに考えや感触が近い感じです.


注4:この点に関してはテンプレートの実装者自身も気づいていて,実装者側から対策を試みている場合もある.たとえばBoostではBoost Concept Check Libraryという試みがある.

ジェネリックプログラミング

  C++のテンプレートを使ったプログラミングを「ジェネリックプログラミング(generic programming)」と称することがあります.C++の参考書を読むとよく出てくるのですが,ジェネリックプログラミングの明確な定義をあまり目にしません.genericを英和辞典で引くと「形容詞−全般的な,包括的な」という漠然とした意味です.おおざっぱにいえば,

・特定の型に依存しない汎用的な記述を目ざしたプログラミング

とでもいえばいいでしょうか.ちなみにC++の原作者であるBjarne Stroustrup氏のサイト注5 にある用語集注6 では,

  programming using templates to express algorithms and data structures parameterized by datatypes,operations,and polices.

  (訳)データタイプ,操作,ポリシ(注:policesはpolicyの複数形)がパラメータ化されたアルゴリズムやデータ構造を表現するためにテンプレートを使うプログラミング

と書かれています.つまりパラメータ化される対象として型だけではなく,他の要素も考慮されているわけです.このことを頭の中に置いておかないとジェネリックプログラミングがされているソースを読むときに混乱します.また自分でジェネリックプログラミングを目指したソースを書いたつもりでも,真の意味でジェネリックでないために,誰からも利用されないという悲しい結果になることもあります.


注5:http://www.research.att.com/~bs/
注6:Bjarne Stroustrup's C++Glossary.http://www.research.att.com/~bs/glossary.html

関数テンプレートの引き数の推定

 ここからはテンプレートプログラミングやジェネリックプログラミングを実装するために利用するC++の仕様(ほとんどの入門記事では取り上げない範ちゅう)を紹介します.というのもテンプレートプログラミングを利用しているソースを読むと頭をかかえるような記述でついていけなくなる恐れがあるからです.

 さて,さきほどのabsoluteを使って今度はdouble型に対応した記述をしてみましょう.単純に「absolute<int>」と書いたのを「absolute<double>」に書き換えてもいいのですが,リスト9のように型の指定を省略してもかまいません.というのも関数テンプレートの引き数が指定されていない場合,コンパイラが自動的に型を推定して当てはめるようになっているからです注7 . 

〔リスト9〕テンプレートの引き数の省略
static void Test()
{
    double d1,d2,d3;
    
    d1 = df();
    d1 = absolute(d1);
    d2 = dg();
    d2 = absolute(d2);
    d3 = dh();
    d3 = absolute(d3);
    d1 = maximum(d1,d2);
    d1 = maximum(d1,d3);
    std::cout << "max absolute value = " << d1 << std::endl;
}

関数テンプレートの多重定義

 コンパイラが自動的に推定するという仕様は恐ろしいことに通常関数にまで及んでいるため,関数テンプレートと通常関数を混ぜることも可能です注8 .たとえば,すでにabsoluteという関数があったとします.これとテンプレート版のabsoluteが混ぜて使えるわけです(リスト10).

〔リスト10〕テンプレートと通常関数の混用
int absolute(int iD)
{
    std::cout << "function version\n";
    return (iD < 0) ? -iD : iD;
}

template <class T>
T absolute(T iD)
{
    std::cout << "template version\n";
    return (iD < 0) ? -iD : iD;
}

static void Test()
{
    int a1,a2;
    double d1,d2;
    
    a1 = 123;
    d1 = -123.456;
    
    d2 = absolute(d1);      //template版が呼ばれる
    std::cout << "d2 = " << d2 << std::endl;
    a2 = absolute(a1);      //関数版が呼ばれる
    std::cout << "a2 = " << a2 << std::endl;
    a2 = absolute<int>(a1); //template版が呼ばれる
    std::cout << "a2 = " << a2 << std::endl;
}

 ただし,筆者個人の意見ですが,多重定義は,あとからソースを検証する段階で目的の関数を取り違えてしまったり,バグの原因にもなりうるので濫用はつつしむべきかと思います. 


注8:「プログラミング言語C++第3版」の13.3.2「関数テンプレートの多重定義」を参照.

ユーザー定義の特殊化

 特定の引き数のときに実装を変えたいという状況はありがちです.absoluteの例でいえば複素数の絶対値を求める関数は「マイナスの数値なら再びマイナスにする」という求め方では不都合です.仮に複素数を扱うComplexクラスがあったとしましょう.この場合は,リスト11のようにComplex型のabsoluteを提供するところです.

〔リスト11〕複素数の絶対値を得る
#include <cmath>

class Complex {
    double mReal,mImage;
public:
    Complex(double iReal = 0,double iImage = 0){
        mReal = iReal;
        mImage = iImage;
    }
    double getReal() const {
        return mReal;
    }
    double getImage() const {
        return mReal;
    }
    double getAbs() const {
        return std::sqrt(mReal * mReal + mImage * mImage);
    }
};

Complex absolute(Complex iC)
{
    return Complex(iC.getAbs(),0);
}

 しかし,提供された側がうっかりテンプレート版のabsoluteを利用したらどうなるでしょうか.この場合,エラーになったり,あるいは予想しない結果が起きてしまうでしょう.これを防止するため「ユーザー定義の特殊化」注9 を使います.たとえば,リスト12のように記述します.ユーザー定義の特殊化は「template<>」という記述に続けて,定義したいコーディングを続けます.

〔リスト12〕ユーザー定義の特殊化の例
template<> Complex absolute<Complex>(Complex iC)
{
     //Complex absolute(Complex iC)を呼び出す
    return absolute(iC);
}

static void Test()
{
    Complex c1(1,1);
    Complex c2;
    
    c2 = absolute(c1);          //OK
    std::cout << "c2#real = " << c2.getReal() << std::endl;
    c2 = absolute<Complex>(c1); //こちらもOK
    std::cout << "c2#real = " << c2.getReal() << std::endl;
}

 ユーザー定義の特殊化はエラー回避やバグ対策に限ったものではなく,特定のパラメータでの例外的な記述,実行効率の向上,コードサイズの肥大化対策注10 によく使われます.


注9: user-defined specialization.「プログラミング言語C++第3版」の13.5「特別バージョン」を参照.訳書では「ユーザー定義の特別バージョン」と翻訳されている.
注10:「プログラミング言語C++第3版」の13.5「特別バージョン」にvoidポインタバージョンを利用することでコードの肥大化を抑えるくふうを紹介している.

型以外のテンプレートの引き数

 通常,テンプレートの引き数としてclass(あるいはtypename)によって型を指定させますが,intなどの通常の型の引き数や,テンプレート引き数も指定できます注11


注11:「プログラミング言語C++第3版」の13.2.3「テンプレート引数」を参照.

以降の内容は本誌を参照ください

インデックス
同じ記述への対応/サブルーチンの導入/マクロの導入
◆テンプレート/改良されたマクロ/ジェネリックプログラミング/関数テンプレートの引き数の推定/関数テンプレートの多重定義/ユーザー定義の特殊化/型以外のテンプレートの引き数

今月号特集トップページへ戻る


Copyright 2004 宮坂 電人