テンプレートメタプログラミング

出典: フリー百科事典『ウィキペディア(Wikipedia)』

これはこのページの過去の版です。Semi-Brace (会話 | 投稿記録) による 2019年7月28日 (日) 07:41個人設定で未設定ならUTC)時点の版であり、現在の版とは大きく異なる場合があります。

テンプレートメタプログラミング: template metaprogramming)は、メタプログラミング技法の一種であり、コンパイラテンプレートを使って一時的ソースコードを生成し、それを他のソースコードと結合してコンパイルする方式である。テンプレートが出力するものは、コンパイル時の定数、データ構造、関数定義などがある。テンプレートの利用は言わばコンパイル時の実行である。この技法は様々な言語で使われている(C++D言語EiffelHaskellMLXLなど)。

テンプレートメタプログラミングの構成要素

メタプログラミング手法としてのテンプレート利用には2段階の操作が必要である。まずテンプレートを定義し、次にそれをインスタンス化しなければならない。テンプレートは生成すべきコードの一般化された形式を示し、インスタンス化によってそのテンプレートから具体的なソースコードが生成される。

テンプレートメタプログラミングは一般にチューリング完全であり、コンピュータプログラムで実行できることはテンプレートメタプログラムでも実行できる。

テンプレートはマクロとは異なる。マクロもコンパイル時に使われる機能で、文字列操作によってソースコードを生成する。一般的にソースコード中の字を置き換える形式のマクロ機能は、言語の意味とか型といったものを考慮できない(LISPのマクロはこの限りではない)。

テンプレートメタプログラムには変更可能な変数がない。つまり、変数は初期化時に一回代入を行うだけである。これは一種の関数型プログラミングと言える。実際、テンプレートの実装では制御構造再帰呼び出しだけを実装していることが多い。

テンプレートメタプログラミングの利用

テンプレートメタプログラミングの文法はそのプログラミング言語本来のそれとだいぶ異なることが多いが、それでもテンプレートを使うのには理由がある。理由のひとつとしてジェネリックプログラミングの実装をするためということが挙げあられる(似たようなコードをいくつも書かないようにする)。また、コンパイル時のコードの最適化の利点を最大限生かすためという理由もある。通常では最適化されない部分もテンプレートを使っていると最適化される場合がある。

コンパイル時のクラス生成

「コンパイル時のプログラミング」とはどういう意味かを示すため、階乗関数の例を示す。以下はテンプレートを使わない場合のC++のコードである:

int factorial(int n)
{
    if (n == 0)
       return 1;
    return n * factorial(n - 1);
}

void foo()
{
    int x = factorial(4); // == (4 * 3 * 2 * 1) == 24
    int y = factorial(0); // == 0! == 1
}

このコードは 4 と 0 の階乗を求めている。

テンプレートメタプログラミングとテンプレートの特殊化を再帰のために使って階乗の計算を実装すると、実行時に階乗の計算をせずにコンパイル時に計算を行うことができる:

template <int N>
struct Factorial
{
    enum { value = N * Factorial<N - 1>::value };
};

template <>
struct Factorial<0>
{
    enum { value = 1 };
};

// Factorial<4>::value == 24
// Factorial<0>::value == 1
void foo()
{
    int x = Factorial<4>::value; // == 24
    int y = Factorial<0>::value; // == 1
}

このコードは 4 と 0 の階乗をコンパイル時に計算し、その結果を定数のように扱う。

これら2種類のバージョンはどちらもプログラムの機能という意味では同じであるが、前者は階乗をプログラム実行時に計算しているのに対して、後者はそれをコンパイル時に計算する。しかし、このようにテンプレートを使うには、テンプレートのパラメータの値がコンパイル時に判っていなければならず、Factorial<X>::valueX がコンパイル時に決まっている場合にしか使えない。換言すれば、X はコンパイル時定数(リテラルsizeof など)の呼び出し結果でなければならない。

コンパイル時のコード最適化

上記の階乗の例はコンパイル時の最適化の一例であり、プログラム内で必要とされる階乗の値を予め計算して、定数としてコード内に組み込んでコンパイルを行う。これは実行時の性能とメモリ使用量の両面で効果がある。しかし、これはそれほど大きな最適化ではない。

もっと重要なコンパイル時の最適化として、テンプレートメタプログラミングを使ったコンパイル時のループ展開がある。ここでは n 次元のベクトルクラスの作成を例として示す(n はコンパイル時に分かっている)。n 次元ベクトルではループ展開によって性能が大きく改善される。ここでは加算演算子を例として示す。

template<int dimension>
Vector<dimension>& Vector<dimension>::operator+=(const Vector<dimension>& rhs)
{
    for (int i = 0; i < dimension; ++i)
        value[i] += rhs.value[i];
    return *this;
}

コンパイラがこのテンプレート関数をインスタンス化すると、次のようなコードが生成される:

template<>
Vector<2>& Vector<2>::operator+=(const Vector<2>& rhs)
{
    value[0] += rhs.value[0];
    value[1] += rhs.value[1];
    return *this;
}

コンパイラは、テンプレートのパラメータ dimension がコンパイル時に定数なので、for ループをコンパイル時に展開できる。この手法の具体的実装例がBoost C++ Library等にある[1]


静的ポリモーフィズム

ポリモーフィズムを使えば、派生クラスのオブジェクトを元となるクラスのオブジェクトのように扱いつつ、メソッドは派生クラスのものを使うことができる。例えば、次のようなコードである:

class Base
{
    public:
        virtual void method() { std::cout << "Base"; }
};

class Derived : public Base
{
    public:
        virtual void method() { std::cout << "Derived"; }
};

int main()
{
    Base *pBase = new Derived;
    pBase->method(); //outputs "Derived"
    return 0;
}

virtual メソッドの呼び出しでは最も下位のクラスのメソッドが使われる。「動的ポリモーフィズム」では、virtualメソッドを持つクラスについて仮想関数テーブル (virtual look-up table) が生成され、実行時にそのテーブルを参照してどのメソッドを呼び出すかが決められる。従って、「動的ポリモーフィズム」では実行時のオーバヘッドが避けられない。

しかし、多くの場合ポリモーフィズムは動的である必要がなく、コンパイル時に決定可能である。そこで、Curiously Recurring Template パターン (CRTP) を使って「静的ポリモーフィズム」を実現できる。これはコード上はポリモーフィズムに似せた手法であってポリモーフィズムそのものではないが、コンパイル時に解決されるため、オーバヘッドを削減できる。以下に例を示す:

template <class Derived>
struct base
{
    void interface() {
         // ...
         static_cast<Derived*>(this)->implementation();
         // ...
    }
};

struct derived : base<derived>
{
     void implementation();
};

ここで、メンバ関数本体が宣言のずっと後にインスタンス化されるという事実を利用して、static_cast を使って派生クラスのメンバをベースクラスのメンバ関数内で使い、コンパイル時にポリモーフィズム的な機能を実現している。CRTPは実際にBoostイテレータライブラリで広く使われている[1]

類似の使用法として "Barton-Nackman trick" がある。

テンプレートメタプログラミングの利点と欠点

  • コンパイル時と実行時のトレードオフ: テンプレートのコードはコンパイル時に処理/評価/展開されるので、コンパイルに時間がかかることになるが、実行コードはより最適化される。このオーバヘッドは一般に小さいが、巨大なプロジェクトやテンプレートを多用する場合は無視できない。
  • ジェネリックプログラミング: テンプレートメタプログラミングによってプログラマはアーキテクチャに集中でき、コンパイラに細かい実装の生成を任せることができる。従って、テンプレートメタプログラミングによって真にジェネリックなコードが実現し、コードの縮小と保守性の向上をもたらす。
  • 可読性: C++ではテンプレートメタプログラミングを使ったコードは本来のC++のコードとは異なり、慣れないと読解に時間がかかる。従って、テンプレートメタプログラミングに不慣れなプログラマにとっては、コードの保守性が低下すると言える(もっとも、これは各言語のテンプレートの実装に依存する)。
  • 移植性: C++では、テンプレートメタプログラミングのサポート状況がコンパイラによって異なるため(特に新しい形式)、移植性の問題が発生する可能性がある。

関連項目

参考文献

  • Ulrich W. Eisenecker: Generative Programming: Methods, Tools, and Applications, Addison-Wesley, ISBN 0-201-30977-7
  • Andrei Alexandrescu: Modern C++ Design: Generic Programming and Design Patterns Applied, Addison-Wesley, ISBN 3-8266-1347-3
  • David Abrahams, Aleksey Gurtovoy: C++ Template Metaprogramming: Concepts, Tools, and Techniques from Boost and Beyond, Addison-Wesley, ISBN 0-321-22725-5
  • David Vandervoorde, Nicolai M. Josuttis: C++ Templates: The Complete Guide, Addison-Wesley, ISBN 0-201-73484-2
  • Manuel Clavel: Reflection in Rewriting Logic: Metalogical Foundations and Metaprogramming Applications, ISBN 1-57586-238-7
  • What's Wrong with C++ Templates? by Jacob Matthews

外部リンク

  1. ^ コンパイル時の最適化(ループ展開)関連: