式鋳型を適当に解説

※あんまり用語とか詳しくないから、用法の間違い等を見つけたら、「ぷぷぷ、れジタルゴーストは言葉も知らないらめらめらめっこなのれす」とか心の中で思いつつ、優しく訂正してあげてください。


これは、本来C++で足し算的なものを実装しようとすると、

vector const operator+(vector const & lhs, vector const & rhs) {
    return vector(lhs[0] + rhs[0], lhs[1] + rhs[1], lhs[2] + rhs[2]);
}

というように一時オブジェクトを必要とするような場面でも、それを生成せずになんとかしようという方法である。
うーん、書くことが無くなってしまった。というか別に私がわざわざ下手な解説なんて書かなくてもExpression Templateでググればすぐ出てくるし。まあなんでもいいや。
上のベクトルを表現したクラスが例えば1000要素もあったりすると初期化やら代入がすごい手間ということが分かる。
もちろん、

vector& add(vector &out, vector const & v1, vector const & v2) {
    for (unsigned int i = 0; i < 1000; ++i) {
        out[i] = v1[i] + v2[i];
    }
    return out;
}

このように書けば一時オブジェクトは必要ない。ただ、ベクトルやら行列の計算のたびに、

add(v1, add(v1, v2, v3), v4);

と書いて、後で見たときに「ハァ?」と言うのを我慢すればいいだけの話である。もちろんvectorでoperator+=をオーバーロードして

v1 += v2;
v1 += v3;
v1 += v4;

とか書けばまぁそれはそれなりに見栄えも良くなる。v1を3回も書いてることを除いて。
しかし依然としてすっきりしない、くしゃみが出そうで出ないが如く。

v1 = v2 + v3 + v4;

と書きたい。どうしても。だが一時オブジェクトは作りたくない。
そこで、"v1 + v2"という記述を「v1とv2の要素ごとに足し算した結果」と解釈するのではなく、「左側がv1で右側がv2で加算する式」と考えてみる。これをC++で簡単に表すと、

struct vector_add {
    vector const & lhs_;
    vector const & rhs_;
    vector_add(vector const & lhs, vector const & rhs) : lhs_(lhs), rhs_(rhs) {}
};

vector_add operator+(vector const & lhs, vector const & rhs) {
    return vector_add(lhs, rhs);
}

というような感じになる(私の頭の中では)。ところでvectorは、operator[]で指定した位置の要素を取得できる(ということにしておく)。で、vector_addはvector型ではない。しかしベクトル + ベクトルなのだから当然ベクトルであるべきだ。そこで、

struct vector_add {
    vector const & lhs_;
    vector const & rhs_;

    vector_add(vector const & lhs, vector const & rhs) : lhs_(lhs), rhs_(rhs) {}

    int operator[](std::size_t i) const {    // operator[] 追加
        return lhs_[i] + rhs_[i];            // 要素はint(ということにしておく)
    }
};

// operator+は省略

というようなoperator[]を付けてやれば、(v1 + v2)[i]で足し合わせたベクトルの要素を取得できるからこれは立派にベクトルとして扱えるはずだ。しかしそれを許さない者がいる。vector_addは確かにベクトルのはずなのに、コンパイラはそれを認めてくれない。

(v1 + v2 + v3)[i];  // エラー: v1 + v2の結果(つまりvector_addオブジェクト)を引数にとるoperator+がない

本当のところ、v1 + v2をベクトルとして認めないようにしたのは書いた本人、即ち私である。生まれながらにしてvector_addに悲壮な運命を背負わせた罪は重い。早速改善して償うとしよう。

template<typename T1, typename T2>
vector_add operator+(T1 const & lhs, T2 const & rhs) {
    return vector_add(lhs, rhs);
}

これでoperator+はvectorでもvector_addでも引数に取れる。しかし引数に取れるだけだ。vector_addはvector_addのメンバーにはなれない。なんということだ、自らが自分によって迫害されているではないか。こんな悲しみの連鎖は一刻も早く断ち切らなければならない。

template<typename Lhs, typename Rhs>
struct vector_add {
    Lhs const & lhs_;
    Rhs const & rhs_;

    vector_add(Lhs const & lhs, Rhs const & rhs) : lhs_(lhs), rhs_(rhs) {}

    int operator[](std::size_t i) const {    // 追加
        return lhs_[i] + rhs_[i];
    }
};

template<typename Lhs, typename Rhs>
vector_add<Lhs, Rhs> operator+(Lhs const & lhs, Rhs const & rhs) {
    return vector_add<Lhs, Rhs>(lhs, rhs);
}

というようにすれば、vector_addはvector_addを引数に取ることができる。もちろんvectorも。めでたしめでたし。
とまぁここで話を終わらせるとvector_addは報われないオブジェクトとなる。これらの式の結果は当然どこかに格納できなければ不便で仕方がない。
このままだと残念なことに式の結果は誰にも知られずにスタックフレームの間に消えていくことになる。それはあまりにも寂しすぎる。
彼らの存在を受け止めてあげるにはvector::operator=を変更すればよいだろう。

class vector {
    int storage[1000];
    ...
public:
    template<typename Vec>
    vector& operator=(Vec const & vec) {
        for (unsigned int i = 0; i < 1000; ++i) {
            this->storage[i] = vec[i];        // storageは要素が格納されている場所(ということにしておく)
        }
        return *this;
    }
    ...
};

これでvector_addだろうがなんだろうが、operator[]さえあれば内容をコピーできる。これでようやく幾千ものintをスタックに用意することなくv1 = v2 + v3 + v4と書ける。
ただ、実際はもっと考えて作るべきである。例えばこの実装だとoperator+やvector::operator=は引数として何でも受け付けてしまう。そこで自分がベクトルと認めている型だけ受け付けるように工夫する必要があるし、足し算以外にも引き算やら何ちゃら積を書こうとしたときに同じようなコードを書かずに済むようにまとめられるものはまとめたり、vectorの要素型をintと特定しなくてもvector + vectorみたいな計算できるようにしたりと色々やった結果が昨日のコードというわけである。

ところで、このExpression Templateは式がいい感じに書けるようになるだけではない。上の例で、v1 + v2のn番目の要素だけに用があるとき、普通に書いたコードだとv1 + v2と書いた時点で1000要素全てが計算されてしまう。ところがETで書いてあるvector_addだと、(v1 + v2)[n]という式ならn番目の要素しか計算されない。これはすばらしく計算をサボっている評価を遅延して必要な部分だけを評価するようにして効率を上げている。もちろんいつもそうとは限らない。場合によっては逆に計算量が増えてしまうこともある(行列の乗算を何個も繋げるとか)。それこそ一時オブジェクトを使うべきだ。



えんいー