仮の相手との戦い(下)

16806 ワード

仮の相手との戦い(下)
作者:唐風出典:
http://www.cnblogs.com/liyiwen
この著作権は作者とブログ園に共有されています.転載を歓迎しますが、この声明を保留してください.そして文章のページに明らかな位置で原文の接続を提供してください.そうでないと法律責任を追及する権利を保留しています.
前篇では、(N)RVOと右値参照を見ました.次に式テンプレートを見てみます.
Expression Template(式テンプレート、ET)
「システム」でC++のテンプレートプログラミングを学んだことがあるなら、Expression Templateという「もの」をすでに知っているはずです.テンプレート聖書の「C++templates」の18章では、この技術を一つの章を使っています.それは複雑で重要だと思います.
Expression Templateといえば、「仮の相手」も功臣の一人ですよね.やはり例で説明します.
class MyVec
{
public:
    MyVec(){
        p = new int[SIZE];
    }
    MyVec(MyVec const& a_left) {
        p = new int[SIZE];
        memcpy(p, a_left.p, SIZE * sizeof(int));
    }
    ~MyVec(){delete [] p;}
    MyVec& operator=(MyVec const& a_left) {
        if (this != &a_left) {
            delete [] p;
            p = new int[SIZE];
            memcpy(p, a_left.p, SIZE * sizeof(int));
        }
        return *this;
    }
    int& operator [](size_t a_idx) { 
        return p[a_idx];
    }
    int operator [](size_t a_idx)const { 
        return p[a_idx];
    }
    MyVec const operator + (MyVec const& a_left) const {
        MyVec temp(*this);
        temp += a_left;
        return temp;
    }
    MyVec& operator += (MyVec const& a_left) { 
        for (size_t i = 0; i < SIZE; ++i) {
            p[i] += a_left.p[i];
        }
        return *this;
    }
private:
    static int const SIZE = 100;
    int* p;
};

int main(int argc, char* argv[])
{
    MyVec a, b, c;
    MyVec d = a + b + c;
    return 0;
}
見てください.このような短いコードを書きます.
MyVec d = a + b + c;
これはよく使われている数学演算でしょう.しかもコードが直感的です.しかし、この表式には「不必要」な一時的なオブジェクトが発生するという問題があります.a+bの結果は、一時的な対象となるので、tempには、このtempにcを加えて、最後に結果をdに伝えて初期化します.これらのベクトルが長い場合、または表現にいくつかの節を加えると、これらのtempは多くの人に不快感を与えることが想像できます.
そして、もし私たちがこう書いたら、
MyVec d=a     d+=b;     d+=c;
余分な一時的なオブジェクトの発生を避けることができます.このように書くと、「相場」を知らない人がMyVec d=a+b+cを見たら、このコードを書いた人はKが足りないと思いますか?
はい、右の引用で解決できるという問題ではないですか?はい、でも、右の引用がない暗い日には、私たちは生きていません.もちろん、小学校から数学の先生が教えてくれます.問題を多く解いてください.考え方を変えてもいいです.この方法はETです.
どうやって作ったのですか?a+b+cは臨時変数が発生します.C++はリアルタイムで値を求めています.a+bを見たら、先にtempのVectorオブジェクトを計算してから、下に計算します.遅延して値を求めることができれば、表現全体を見てから計算します.このtempの発生を避けることができます.
どうしますか
本来のやり方では、operator+は直接計算を行いました.「早すぎる」計算をしたくないなら、新しいoperator+演算子を載せます.この演算では本物の演算は行われません.オブジェクトを生成するだけです.この対象の中で加算演算子の両方の操作数を保留して、次のステップの計算に参加させます.(はい、この対象も臨時です.しかし、その価格はとても小さいです.私たちはそれを無視します.)そこで私たちは下記のコードを書きます.
class MyVec;

template <typename L>
class ExpPlus {
    L const & lvec;
    MyVec const & rvec;
public:
    ExpPlus(L const& a_l, MyVec const& a_r):
      lvec(a_l), rvec(a_r)
      { }
      int operator [] (size_t a_idx) const;
};

// Point 1
template <typename L>
ExpPlus<L> operator + (L const& a_l, MyVec const & a_r) {
    return ExpPlus<L>(a_l, a_r);
}

class MyVec
{
public:
    MyVec(){
        p = new int[SIZE];
    }

    MyVec(MyVec const& a_r) {
        p = new int[SIZE];
        memcpy(p, a_r.p, SIZE * sizeof(int));
    }

    template <typename Exp>
    MyVec(Exp const& a_r) {
        p = new int[SIZE];
        for (size_t i = 0; i < SIZE; ++i) {
            p[i] += a_r[i];
        }
    }

    ~MyVec(){delete [] p;}

    MyVec& operator = (MyVec const& a_r) {
        if (this != &a_r) {
            delete [] p;
            p = new int[SIZE];
            memcpy(p, a_r.p, SIZE * sizeof(int));
        }
        return *this;
    }

    template <typename Exp>
    MyVec& operator = (Exp const & a_r) {
        delete [] p;
        p = new int[SIZE];
        for (size_t i = 0; i < SIZE; ++i) {
            p[i] += a_r[i];
        }
        return *this;
    }

    int& operator [](size_t a_idx) { 
        return p[a_idx];
    }

    int operator [](size_t a_idx)const { 
        return p[a_idx];
    }
private:
    static int const SIZE = 100;
    int* p;
};

template <typename L>
int ExpPlus<L>::operator [] (size_t a_idx) const {
    return lvec[a_idx] + rvec[a_idx];
}

int main(int argc, char* argv[])
{
    MyVec a, b, c;
    MyVec d = a + b + c;
    return 0;
}
前のコードに比べて、このコードにはいくつかの重要な修正があります.まず、テンプレートタイプのExpPlusを追加して、足し算の「表式」を表します.しかし、足し算をする時、それ自体は本当の計算をしません.このクラスに対しては、下付き演算子が定義されています.この演算子では本格的な加算が行われます.そして、元のMyVecについては、割り当て時にExpPlusの下付き演算子で計算結果を取得するように、その割当値演算子を積載します.
上の言葉は、ETを知らない人にとっては、まだ分かりにくいかもしれません.
d=a+b+cという式の中で、まずa+bと出会います.この時、テンプレート関数のoperator+が呼び出されます.(コードに「Point 1」と注釈されています.)この時は、一時的なExpPlus<MyVec>オブジェクトを生成します.計算はしません.計算された左の右の操作数(つまり、aとb)を残します.次に、t 1+cです.同じoperator+を再度呼び出しても、オブジェクトを生成するだけです.このオブジェクトのタイプはExpPlusです.同じように、t 2はここで両方の操作数だけ残しています.表式全体が「完了」するまでは、計算されたものはなく、実際にはExpPlusというテンプレート類で計算式の情報を記録しています.
最後に、d=t 2を行うと、MyVecの割当演算子が呼び出される(t 2でパラメータとする).なお、この呼び出し中の文p[i]=t 2[i]は、t 2[i]を積載した下付き演算子により、t 1[i]+c[i]に展開され、同理t 1[i]を再度展開し、a[i]+b[i]となり、最終的にはp[i]=t 2[i]は、p[i]=a[i]+b]の内部連結機能となります.「マジック」と同じように、ExpPlusを通じて「遅延計算」を行い、大きなMyVecの臨時オブジェクトの発生を回避しました.
これは基本的にETの「原理」でしょう.私達は「専門化」に来ました.
  • To create a doman-specific embedded language(DSEL)in C++
  • To support lazy evalution of C++expressions(e.g.mathe matical expressions)、which can be executed much later in the program from the point of their definition
  • To pass an expression-not the result of the expression-as a parameter to a function
  • このように、ETを使って「直観」と「効率」を考えられます.
    ETの中のC++の中のクラスにはすでに多くの応用があります.(bootstの中の複数のサブライブラリと、Blitz++などの高性能の数学ライブラリを含みます.)
    まとめ(N)RVOはコンパイラが作ってくれた最適化手段であり、最適化が可能な場合、NRVOの表現は非常に良いです.それでこそ、一時的なオブジェクトの発生を本当に避けられます.右の値は(rvalue reference)とmoveの意味を引用して(N)RVOの足りないところを補って、一時的な対象のオーバーヘッドを最小にすることが可能になりますが、これも制限があります.Expression Templateは表現の直観と効率の両方を維持していますが、明らかに複雑すぎて、主にクラスの設計者としての武器です.また、使用者に「新しい」ものを理解させることもできます.例えば、表式の中間値を記憶したいなら、「ExpPlus<ExpPlus><MyVec>…」はきっと頭が大きいと思います.  全文完了