讀書筆記 effective c++ Item 48 瞭解模板元編程

來源:互聯網
上載者:User

標籤:err   expr   lin   pen   執行個體   結構體   運行時   有趣   訪問者   

1. TMP是什嗎?

模板元編程(template metaprogramming TMP)是實現基於模板的C++程式的過程,它能夠在編譯期執行。你可以想一想:一個模板元程式是用C++實現的並且可以在C++編譯器內部啟動並執行一個程式,它的輸出——從模板中執行個體化出來的C++源碼片段——會像往常一樣被編譯。

2. 使用TMP的優勢

如果這沒有衝擊到你,是因為你沒有足夠儘力去想。

 

C++不是為了模板元編程而設計的,但是自從TMP早在1990年被發現之後,它就被證明是非常有用的,為了使TMP的使用更加容易,在C++語言和標準庫中加入了一些擴充。是的,TMP是被發現的,而不是被發明。當模板被添加到C++中的時候TMP這個特性就被引入了。對於某些人來說所有需要做的就是關注如何以一種聰明的和意想不到的方式來使用它。

TMP有兩種強大的力量。第一,它使得一些事情變得容易也即是說如果沒有TMP,這些事情做起來很難或者不可能實現第二,因為模板元編程在C++編譯期執行,它們可以將一些工作從運行時移動到編譯期。一個結果就是一些原來通常在運行時能夠被發現的錯誤,現在在編譯期就能夠被發現了。另外一個結果就是使用TMP的C++程式在基本上每個方面都更加高效:更小的執行體,更短的已耗用時間,更少的記憶體需求。(然而,將工作從運行時移到編譯期的一個後果就是編譯時間增加了。使用TMP的程式比沒有使用TMP的程式可能消耗更長的時間來進行編譯。)

3. 如何使用TMP?3.1 再次分析Item 47中的執行個體

考慮在Item 47中為STL的advance寫出來的虛擬碼。我已經為虛擬碼部分做了粗體:

 1 template<typename IterT, typename DistT> 2 void advance(IterT& iter, DistT d) 3 { 4 if (iter is a random access iterator) { 5  6 iter += d;                           // use iterator arithmetic 7  8 }                                        // for random access iters 9 10 else {                                11 12 13 if (d >= 0) { while (d--) ++iter; } // use iterative calls to14 else { while (d++) --iter; } // ++ or -- for other15 } // iterator categories16 }

 

我們可以使用typeid替換虛擬碼,讓程式能夠執行。這就產生了一個“普通的”C++方法——也就是所有工作都在運行時開展的方法:

 1 template<typename IterT, typename DistT> 2 void advance(IterT& iter, DistT d) 3 { 4 if ( typeid(typename std::iterator_traits<IterT>::iterator_category) == 5 typeid(std::random_access_iterator_tag)) { 6  7 iter += d;                           // use iterator arithmetic 8  9 }                                        // for random access iters10 11 else {                               12 13 14 if (d >= 0) { while (d--) ++iter; } // use iterative calls to15 else { while (d++) --iter; } // ++ or -- for other16 } // iterator categories17 }

 

Item 47指出這種基於typeid的方法比使用trait效率更低,因為通過使用這種方法,(1)類型測試發生在運行時而不是編譯期(2)執行運行時類型測試的代碼在啟動並執行時候必須可見。事實上,這個例子也展示出了為什麼TMP比一個“普通的”C++程式更加高效,因為traits方式屬於TMP。記住,trait使得在類型上進行編譯期if…else運算成為可能。

我已經在前面提到過一些東西說明其在TMP中比在“普通”C++中更加容易,Item 47中也提供了一個advance的例子。Item 47中提到了advance的基於typeid的實現會導致編譯問題,看下面的例子:

1 std::list<int>::iterator iter;2 ...3 advance(iter, 10);               // move iter 10 elements forward;4 // won’t compile with above impl.

 

考慮為上面調用所產生的advance的版本,將模板參數IterT和DistT替換為iter和10的類型之後,我們得到下面的代碼:

 1 void advance(std::list<int>::iterator& iter, int d) 2 { 3 if (typeid(std::iterator_traits<std::list<int>::iterator>::iterator_category) == 4 typeid(std::random_access_iterator_tag)) { 5  6 iter += d; 7  8 // error! won’t compile 9 10 11 }12 else {13 if (d >= 0) { while (d--) ++iter; }14 else { while (d++) --iter; }15 }16 }

 

有問題的是高亮部分,就是使用+=的語句。在這個例子中,我們在list<int>::iterator上使用+=,但是list<int>::iterator是一個雙向迭代器(見Item 47),所以它不支援+=。只有隨機訪問迭代器支援+=。現在,我們知道了+=這一行將永遠不會被執行到,因為為list<int>::iteraotr執行的typeid測試永遠都不會為真,但是編譯器有責任確保所有的源碼都是有效,即使不被執行到,當iter不是隨機訪問迭代器“iter+=d”就是無效代碼。將它同基於tratis的TMP解決方案進行比較,後者把為不同類型實現的代碼分別放到了不同的函數中,每個函數中進行的操作只針對特定的類型。

3.2 TMP是圖靈完全的

TMP已經被證明是圖靈完全的(Turing-Complete),這也就意味著它足夠強大到可以計算任何東西。使用TMP,你可以聲明變數,執行迴圈,實現和調用函數等等。但是這些概念同“普通”C++相對應的部分看起來非常不同。例如,Item 47中if…else條件在TMP中是如何通過使用模板和模板特化來表現的。但這是程式層級(assembly-level)的TMP。TMP庫(例如,Boost MPL,見Item 55)提供了更進階別的文法,這些文法不會讓你誤認為是“普通的”C++。

3.3 TMP中的迴圈通過遞迴來實現

再瞥一眼事情在TMP中是如何工作的,讓我們看一下迴圈。TMP中沒有真正的迴圈的概念,所以迴圈的效果是通過遞迴來完成的。(如果一提到遞迴你就不舒服,在進入TMP 冒險之前你就需要處理好它。TMP主要是一個函數式語言,遞迴對於函數式語言就如同電視對美國流行文化一樣重要:它們是不可分割的。)即使是遞迴也不是普通的遞迴,因為TMP迴圈沒有涉及到遞迴函式調用,所涉及到的是遞迴模板執行個體化(template instantiations)。

TMP的“hello world”程式是在編譯期計算階乘。它算不上是令人激動的程式,“hello world”也不是,但是這兩個例子對於介紹語言都是有協助的。TMP階乘計算通過對模板執行個體進行遞迴來對迴圈進行示範。也同樣示範了變數是如何在TMP中被建立和使用的,看下面的代碼:

 1 template<unsigned n>          // general case: the value of 2  3 struct Factorial {                   // Factorial<n> is n times the value 4  5  6 // of Factorial<n-1> 7 enum { value = n * Factorial<n-1>::value }; 8 }; 9 template<> // special case: the value of10 struct Factorial<0> { // Factorial<0> is 111 enum { value = 1 };12 };

 

考慮上面的模板元編程(真的僅僅是單一的元函數Factorial),你通過引用Factorial<n>::value來得到factorial(n)的值。

代碼的迴圈部分發生在模板執行個體Factorial<n>引用模板執行個體Factorial<n-1>的時候。像所有遞迴一樣,有一種特殊情況來讓遞迴終止。在這裡是模板特化Factorial<0>。

每個Factorial模板的執行個體都是一個結構體,每個結構體使用enum hack(Item 2)來聲明一個叫做value的TMP變數。Value持有遞迴計算的當前值。如果TMP有一個真正的迴圈結構,value將會每次迴圈的時候進行更新。既然TMP使用遞迴模板執行個體來替換迴圈,每個執行個體會得到它自己的value的拷貝,每個拷貝都會有一個和“迴圈”中位置想對應的合適的值。

你可以像下面這樣使用Facorial:

1 int main()2 {3 std::cout << Factorial<5>::value; // prints 1204 5 std::cout << Factorial<10>::value;       // prints 36288006 7 }        

                                             

 如果你認為這比冰激淩更酷,你就已經獲得模板元程式員需要的素材。如果模板和特化,遞迴執行個體和enum hacks,還有像Factorial<n-1>::value這樣的輸入使你毛骨悚然,你還是一個“普通的”C++程式員。

3.4 TMP還能夠做什嗎?

當然,Factorial對TMP的功能進行了示範,如同“hello world”程式對任何傳統程式設計語言的功能進行示範一樣。為了讓你明白為什麼TMP是值得瞭解的,知道它能夠做什麼很重要,這裡有三個例子:

  • 確保因次單位(dimensional unit)的正確性。在科學和工程應用中,把因次單位(例如,品質,距離和時間)正確的拼到一起是很必要的。將表示品質的變數賦值給表示速度的變數是錯誤的,但是用距離變數除以時間變數然後將結果賦值被速度變數就沒有問題。通過使用TMP,確保(在編譯期間)程式中的所有因次單元組合的正確性就是可能的,不管計算有多複雜。(這也是使用TMP來偵測早期錯誤的一個例子。)TMP這種用法的一個有趣的方面是它能夠支援分數因次的指數。這需要在編譯期間將分數簡化,然後編譯器才能夠確認,例如,單元 time1/2同time4/8是相同的。
  • 最佳化矩陣操作。Item 21中解釋了有一些函數(包括 operator*)必須返回新的對象,Item 44中引入了SquareMatrix類,考慮下面的代碼:

    1 typedef SquareMatrix<double, 10000> BigMatrix;2 BigMatrix m1, m2, m3, m4, m5; // create matrices and3 ... // give them values4 BigMatrix result = m1 * m2 * m3 * m4 * m5; // compute their product

     用“普通的”方式來計算result會有四次建立臨時matrice對象的調用,每次調用都應用在對operator*調用的傳回值上面。這些獨立的乘法在矩陣元素上產生了四          次迴圈。使用TMP的進階模板技術——運算式模板(expression templates),來消除臨時對象以及合并迴圈是有可能的,並且不用修改上面的用戶端代碼的文法。最   後的程式使用了更少的記憶體,而且運行速度會有很大的提升。

  • 產生個人化的設計模式實現。像策略模式,觀察者模式,訪問者模式等等這些設計模式能夠以很多方式被實現。使用基於模板的技術被叫做policy-based設計,我們可以建立表示獨立設計選擇(choice或者叫”policies”)的 模板,這些模板可以以任意的方式進行組合來產生個人化的模式實現。例如,使用這種技術能夠建立一些實現智能指標行為策略(policies)的模板,使用它能夠產生(在編譯期)上百種不同的智能指標類型。這項技術已經超越了編程工藝領域,如設計模式和智能指標,它成為了生殖編程(generative programming)的基礎。
4. TMP現狀分析

TMP並不是為每個人準備的。因為文法不直觀,支援的相關工具也很弱。(像為模板元編程提供的調試器。)作為一個“突然性“的語言它只是最近才被發現的,TMP編程的一些約定正在實驗階段。然而通過將工作從運行時移到編譯期所帶來的效率提升帶給人很深刻的印象,對一些行為表達的能力(很難或者不可能在運行時實現)也是很迷人的。

對於TMP的支援正在上升期。很可能下個版本的C++就是顯示的支援它。TR1中已經支援了(Item 54)。關於這個主題的書籍已經開始出來了,網上的一些關於TMP資訊也越來越多。TMP可能永遠不會成為主流,但是對於一些程式員來說——尤其是程式庫的實現者——幾乎必然會成為主要手段。

5. 總結
  • 模板元編程可以將工作從運行時移到編譯期,這樣可以更早的發現錯誤,並且提高運行時效能。
  • 基於策略選擇(policy choices)的組合TMP能夠被用來產生個人化的代碼,也能夠用來防止為特定類型產生不合適的代碼。

讀書筆記 effective c++ Item 48 瞭解模板元編程

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.