與臨時對象的鬥爭(下)

Expression Template(表達式模板,ET)

若是有「系統地」學習過 C++ 的模板編程,那麼你應該已經知道 Expression Template 這個「東西」。在模板聖經《C++ templates》的第 18 章專門用了一整章來說這個技巧,(是的,我認爲它是一種技巧)。足以見得它比較複雜,也很重要。express

提及 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; } 

看,咱們寫下這麼小段代碼:dom

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。this

怎麼作的呢?a + b + c 會產生臨時變量是由於 C++ 是即時求值的,在看到 a + b,就先算出一個 temp 的Vector對象,而後再向下算。若是能進行延遲求值,看完整個表達式再來計算,那麼就能夠避免這個temp的產生。lua

怎麼作?spa

原來的作法中,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>對象(咱們叫它 t1 吧),不作計算,只是保留計算的左右操做數(也就是a和b),接着,t1 + c ,再次調用一樣的 operator + ,並且也只是生成一個對象(咱們叫它 t2 吧),這個對象的類型是 ExpPlus<ExpPlus<MyVec>>,一樣,t2 在這裏只是保留了兩邊的操做數(也就是 t1 和 c)。直到整個表達式「作完」,沒有任何東西進行了計算,所作的事情實際上只是用 ExpPlus 這個模板類把計算式的信息記錄下來了(固然,這些信息就是參與計算的操做數)。

最後,當進行 d = t2 的時候,MyVec 的賦值運算符被調用(用 t2 做參數)。注意,這個調用中的語句 p[i] = t2[i],其中 t2[i] 經過重載的下標運算符,展開成 t1[i] + c[i],同理 t1[i] 又再次展開,成爲 a[i]+b[i],最終,p[i] = t2[i] 就變成了:p[i] = a[i] + b[i] + c[i])(固然,裏面參雜了內聯的效果,這些函數都是很是容易被內聯的)。就像變「魔術」同樣,咱們經過ExpPlus完成了「延遲計算」,並避免了大型的 MyVec 臨時對象的產生。

這基本上就是 ET 的「原理」了吧。咱們來「專門化」一下 ET 的好處:

  • To create a domain-specific embedded language (DSEL) in C++
  • To support lazy evaluation of C++ expressions (e.g., mathematical 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++ 中的類庫裏已經有很是多的應用了(包括 boost 中的多個子庫,以及 Blitz++ 等高性能數學庫)

總結

(N)RVO 是編譯器爲咱們作的優化手段,在能進行優化的狀況下,NRVO 的表現是很是好的,由於它才真正的避免了臨時對象的產生(rvalue reference 和 expression template 中均可能還存在一些小型臨時對象),但 (N)RVO 有不少的限制條件。右值引用(rvalue reference )和 move 語意彌補了 (N)RVO 的不足之處,使得臨時對象的開銷最小化成爲可能,但這也是有侷限的,好比,嗯,若是一個類自己不動態地擁有資源……,那 move 就沒有意義了。Expression Template 保持了表達式直觀和效率二者,很強大,但很顯然它太複雜,主要是做爲類庫的設計者的武器。另外,它也可能使得使用者要理解一些「新」東西,好比,若是我想存儲表達式的中間值,那麼 <ExpPlus<ExpPlus<...<MyVec>...> 必定會讓我很頭大(不過有了 C++0x 的 auto 就好多了,呵呵)。

 

全文完!

相關文章
相關標籤/搜索