Expression Template

式テンプレートについて(テンプレート導入)


テンプレートを使う
漸くテンプレートの出番です. Vect の代入演算子は(とりあえず)なんでも受け付けるようにし, 型情報を返す operator+() をテンプレートで作成します.
class Vect {
    /* snip */
    template<class T>
    Vect& operator=(const T& rhs)
    {
        rhs.eval(*this);
        return *this;
    }
};

上記の追加により, (あまりよろしくないけど)何かよく分からない型を受け付けて, それに対して eval を実行して *this を得ることができます.

VaddV, VaddV_addV の方は, 一般的に Exp (expression) という名前のテンプレートにしてみましょう.

template<class L>
class Exp {
    const L& lhs_;
    const Vect& rhs_;
public:
    Exp(const L& lhs, const Vect& rhs)
        : lhs_(lhs)
        , rhs_(rhs)
    {
    }
    void eval(Vect& dest) const
    {
        dest = lhs_; /* lhs_.eval(dest);と等価 */
        Vect::add(rhs_, dest);
    }
};

template<class L>
Exp<L> operator+(const L& lhs, const Vect& rhs)
{
    return Exp<L>(lhs, rhs);
}

中身は型が変わった以外, 前回の VaddV_addV とほぼ同じです. この変更により, w = x + y; も w = x + y + z; も w = x + x + x + ... + y; にも対応しました. コンパイラがコンパイル時に必要なだけ関数を生成してくれるわけです. 素晴らしい.

特殊化の追加
ちょっと待った. DEBUG を有効にすると, コピーが一回行われているようです. 何故?

それは上記一般化において L = Vect のとき, eval の中で不要な dest = lhs_; が実行されるからです. ここは単に Vect::add(rhs_, lhs_, dest); でよいはず. そのために, L = Vectの時のeval() の特別バージョンを追加します.

template<>
void Exp<Vect>::eval(Vect& dest) const
{
    Vect::add(lhs_, rhs_, dest);
}

これにより, 無事コンストラクタやコピーが一度も発生しないようにできました(et3.h).

より汎用的に
前回まででいくつもの加算がOKになったのですが, 少し欠点があります. w = x + (y + z); がコンパイルエラーになってしまうのです. なんとかしたいものです.

このエラーは右辺の型が Vect しか受け付けないことに起因しています. これを解消するには両辺共に自由な型にしないといけません. というわけで, Exp が二つの方を持てるように

template<class L, class R>
class Exp {
    const L& lhs_;
    const R& rhs_;
public:
    Exp(const L& lhs, const R& rhs)
        : lhs_(lhs)
        , rhs_(rhs)
    {
    }
    void eval(Vect& dest) const
    {
        Vect tmpl(lhs_);
        Vect tmpr(rhs_);
        Vect::add(tmpl, tmpr, dest);
    }
};

とします. operator+() も同様に

template<class L, class R>
Exp<L, R> operator+(const L& lhs, const R& rhs)
{
    return Exp<L, R>(lhs, rhs);
}

とします. それから一時変数を作らせないために特殊化を用意します. 右側だけが Vect, 左側だけが Vect, 両方 Vect の三つを作ります. eval() の中身から, それぞれの特殊化に合わせて不要な一時変数を削除します.

それから最後に Vect w(x + y); というコードも処理できるように Vect にコンストラクタを追加しましょう.

    template<class L, class R>
    Vect(const Exp<L, R>& rhs)
    {
        rhs.eval(*this);
    }

これらの修正により

    w = x + (y + z) + x + y + z + x + (y + z) + x;

のような複雑(?)な式も処理できるようになりました(et4.h). 残念ながら, このコードはVC6/bcc32ではコンパイルできません. VC7.1またはgcc 3.2以上でないと駄目です.

2005/3/23:初版は中途半端なコードで間違えてました. すいません. dsk さんに感謝
ベクトルらしく
これまでは Vect の性質を何も使っていませんでした. しかし Vect の加算は成分ごとの整数の加算でできるという性質を持っています. この性質を利用して少し違った評価を行う式テンプレートを構成してみます.

今まではベクトルごとに足すということをしていました. つまり,

┌    ┐     ┌    ┐   ┌    ┐                  ┌    ┐
|w[0]|     |x[0]|   |y[0]|                  |z[0]|
|w[1]|     |x[1]|   |y[1]|                  |z[1]|
|w[2]| =(((|x[2]| + |y[2]|) + ...) + ...) + |z[2]|
|... |     |... |   |... |                  |... |
|w[N]|     |x[N]|   |y[N]|                  |z[N]|
└    ┘     └    ┘   └    ┘                  └    ┘

としているわけですが, これを成分ごとに足してみるわけです.

  w[0] = (((x[0] + y[0]) + ...) + ...) + z[0]
  w[1] = (((x[1] + y[1]) + ...) + ...) + z[1]
  w[2] = (((x[2] + y[2]) + ...) + ...) + z[2]
  ...
  w[N] = (((x[N] + y[N]) + ...) + ...) + z[N]

とするのです. 今までは dest を何度もなめる必要がありましたが, こうすると dest をなめるのは一度だけですむのでメモリアクセスコストを大幅に減らせます. eval() を i 番目の成分のみ得るようにし, Vect 本体で それらの eval を 0 から N-1 まで評価するように変更します.

Vect 側

class Vect {
    /* snip */
    template<class L, class R>
    Vect& operator=(const Exp<L, R>& rhs)
    {
        eval(rhs);
        return *this;
    }
    template<class L, class R>
    Vect(const Exp<L, R>& rhs)
    {
        eval(rhs);
    }
    int eval(int i) const { return p_[i]; }
private:
    template<class L, class R>
    void eval(const Exp<L, R>& rhs)
    {
        for (int i = 0; i < N; i++) {
            p_[i] = rhs.eval(i);
        }
    }
};

template<class L, class R>
class Exp {
    const L& lhs_;
    const R& rhs_;
public:
    Exp(const L& lhs, const R& rhs)
        : lhs_(lhs)
        , rhs_(rhs)
    {
    }
    int eval(int i) const { return lhs_.eval(i) + rhs_.eval(i); }
};

ただ int eval(int i) というのは結局は operator[] と同じなので上記 eval() を operator[] に置き換えるとスマートです(vect1.h).

    int operator[](int i) const { return lhs_[i] + rhs_[i]; }

今回の場合, 今までと違って Exp の eval(operator[]) の中で一時変数を必要としないため, 特殊化を用意する必要はありません. だからVC6でもコンパイルできます.

演算子の種類を増やす
ついでに成分ごとの掛け算も作ってみましょう. Exp の eval の掛け算用を用意します. そのために Exp に演算子を区別するためのテンプレート引数を増やします(単に違う名前のExpをふやしてもよいですが).
template<class L, class Ope, class R>
class Exp {
    const L& lhs_;
    const R& rhs_;
public:
    Exp(const L& lhs, const R& rhs)
        : lhs_(lhs)
        , rhs_(rhs)
    {
    }
    int operator[](int i) const { return Ope::eval(lhs_[i], rhs_[i]); }
};

Ope にいれるべきクラスを作ります.

struct ExpAdd {
    static int eval(int lhs, int rhs)
    {
        return lhs + rhs;
    }
};

struct ExpMul {
    static int eval(int lhs, int rhs)
    {
        return lhs * rhs;
    }
};

んで, 演算子を定義します.

template<class L, class R>
Exp<L, ExpAdd, R> operator+(const L& lhs, const R& rhs)
{
    return Exp<L, ExpAdd, R>(lhs, rhs);
}

template<class L, class R>
Exp<L, ExpMul, R> operator*(const L& lhs, const R& rhs)
{
    return Exp<L, ExpMul, R>(lhs, rhs);
}

これで w = (x + y) * z; も計算できるようになりました. ちなみにVC7.1でコンパイルすると N = 4 なら全部計算してしまうようで(DEBUGオフ時), アセンブリ言語出力を見ると w に直接定数値が代入されていました. x, y, zは影も形もありません(んなアホな).

; 44   :         w = x * (y + z);
; 45   :         w = w * x;
; 50   :     }
; 52   :     cout << "w=" << w << endl;

  00005    8d 44 24 08     lea     eax, DWORD PTR _w$[esp+24]
  00009    50         push     eax
  0000a    68 00 00 00 00     push     OFFSET FLAT:$SG9889
  0000f    68 00 00 00 00     push     std::cout
    00 00 00     mov     DWORD PTR _w$[esp+36], 7
    00 00 00     mov     DWORD PTR _w$[esp+40], 32 ; 00000020H
    00 00 00     mov     DWORD PTR _w$[esp+44], 81 ; 00000051H
    00 00 00     mov     DWORD PTR _w$[esp+48], 160 ; 000000a0H
  00034    e8 00 00 00 00     call     std::operator<<<std::char_traits<char> >

より最適化が難しいと思われる et4.h ですら w = x + (y + z); を計算完了しています. w = x * (y + z) * x; は流石にしなかったけど, w = x * (y + z); w = w * x; は計算完了してました. 恐るべし.

ついでに右からのスカラー倍も作っておきましょう. R = int の特殊化を追加すればOKです(vect2.h).

template<class L>
class Exp<L, ExpMul, int> {
    const L& lhs_;
    const int rhs_;
public:
    Exp(const L& lhs, int rhs)
        : lhs_(lhs)
        , rhs_(rhs)
    {
    }
    int operator[](int i) const { return lhs_[i] * rhs_; }
};
    w = (x + y) * z * 3;

続く.


御意見は光成滋生<herumi@nifty.com>までお願いします