標籤:c++ template 高效c++
Template metaprogramming(TMP,模板元編程)是編寫template-based C++程式,編譯的過程。template metaprogramming是用C++寫的模板程式,編譯器編譯出具體化的過程。也就是說,TMP程式執行後,從templates具體化出來C++源碼,不再是模板了。
TMP有兩個作用,一是它讓某些事更容易。例如編寫STL容器,使用模板,可是存放任何類型元素。二是將執行在運行期的某些工作轉移到了編譯期。還有一個結果是使用TMP的C++程式可能在其他方面更高效:較小的可執行檔、較短的運行期、較少的記憶體需求。但是將運行期的工作轉移到了編譯期,編譯期可能變長了。
再看一下條款 47中的advance偽碼
template<typename Iter, typename DistT> void advance(IteT& iter,DistT d) { if(iter is a random access iterator) iter+=d; else { if(d>=0) while(d--) ++iter; else while(d++) --iter; } }
可以使用typeid讓判斷iter類型的偽碼運行
template<typename Iter, typename DistT> void advance(IteT& iter,DistT d) { if(typeid(typename std::iterator_traits<IterT>::iterator_category) ==typeid(std::random_access_iterator_tag)) iter+=d; else { if(d>=0) while(d--) ++iter; else while(d++) --iter; } }
typeid-based解法效率比traits解法低,因為在此方案中,1類型測試發生在運行期而不是編譯期,2運行期類型測試代碼在(或被串連於)可執行檔中。這個例子可以說明TMP比正常的C++程式更高效,因為traits解法就是TMP。
一些東西在TMP比在正常的C++更容易,advance提供一個好例子。advance的typeid-based實現方式可能導致編譯期問題
std::list<int>::iterator iter; …… advance(iter,10); void advance(std::list<int>::iterator& iter,int d) { if(typeid(typename std::iterator_traits<std::list<int>::iterator>::iterator_category) ==typeid(std::random_access_iterator_tag)) iter+=d;//錯誤 else { if(d>=0) while(d--) ++iter; else while(d++) --iter; } }
在+=這個操作符上是錯誤調用。因為list::iterator不支援+=,它是bidirectional迭代器。我們知道不會執行+=那一行,因為typeid那一行總是不相等;但是編譯期要確保所有源碼都有效,即使是不會執行的代碼。traits-based TMP解法針對不同類型執行不同代碼,不會出現上述問題。
TMP已被證明是個圖靈完全機器,也就是說它的威力足以計算任何事物。可以使用TMP聲明變數、執行迴圈、編寫調用函數……。有時候這會和正常C++對應物看起來很是不同,例如條款 47展示的TMP if-else是由templas和其特化具體表現出來。不過那是組合語言級的TMP。針對TMP設計的程式庫(例如Boost’s MPL,**條款**55)提供更進階的文法。
為了再次認識下事物在TMP中如何運作,來看下迴圈。TMP沒有真正迴圈,迴圈由遞迴(recursion)完成。TMP遞迴甚至不是正常的遞迴,因為TMP遞迴不涉及遞迴函式調用,而是涉及遞迴模板化(recursive template instantiation)。
TMP的起手程式是在編譯期計算階乘。TMP的階乘運輸示範如何通過遞迴模板具體化實現迴圈,以及如何在TMP中建立和使用變數
template<unsigned n> struct Factorial{ enum {value=n*Factorial<n-1>::value}; }; template<> struct Factorial<0>{ //特殊情況,Factorial<0>的值是1 enum {value=1}; };
有了這個template metaprogram,只要指涉Factorial::value就可以得到n階乘值。迴圈發生在template具體化Factorial內部指涉另一個template具體化Factorial之時。特殊情況的template特化版本Factorial<0>是遞迴的結束。
每個Factorial template具體化都是一個struct,每個struct都聲明一個名字為value的TMP變數,用來儲存當前計算所獲得的階乘值。TMP以遞迴模板具體化取代迴圈,每個具體化有自己一份value,每個value有其迴圈內適當值。
用Factorial示範TMP就像用hello world示範程式設計語言一樣。為了領悟TMP之所以值得學習,就要先對它能夠達成什麼目標有一個比較好的理解。下面舉三個例子:
- 確保量度單位正確。使用TMP就可以確保在編譯期所有量度單位的組合都正確。
- 最佳化矩陣運算。條款 21曾經提到過某些函數包括operator * 必須返回新對象,在條款 44中有一個SquareMatrix。如果這樣使用
typedef SquareMatrix<double,1000> BigMatrix; BigMatrix m1,m2,m3,m4,m5; …… BigMatrix result=m1 * m2 * m3 * m4 * m5;
上面乘法會產生四個臨時性矩陣,乘法還可能產生了4個作用在矩陣元素身上的迴圈。如果使用進階、與TMP相關的template(即expression templates),就有可能消除那些臨時對象併合並迴圈。所以TMP使用較少記憶體,執行速度也有提升。
- 可以產生客戶定製之設計模式(custom design pattern)實現品。使用policy-based design之TMP-based技術,有可能產生一些templates用來表述獨立的設計項(所謂policies),然後可以任意結合它們,導致模式實現品帶著客戶定製的行為。
TMP目前還不完全成熟,文法不直觀,支援的工具還不充分。但TMP對難以或甚至不可能於運行期實現出來的行為表現能力很吸引人。雖然TMP不會成為主流,但是會成為一些程式員(特別是程式庫的開發人員)的主要糧食。
總結
- Template metaprogramming(TMP,模板元編程)可將工作由運行期移到編譯期,因而得以實現早期錯誤偵測和更高的執行效率。
- TMP可被用來產生“基於政策選擇組合”(based on combinations of policy choices)的客戶定製代碼,也可用來避免產生對某些特殊類型並不適合的代碼。
《Effective C++》:條款48:認識template元編程