模板元編程

模板元編程是一種元編程技術,編譯器使用模板產生暫時性的源碼,然後再和剩下的源碼混合併編譯。這些模板的輸出包括編譯時期常量、數據結構以及完整的函式。如此利用模板可以被想成編譯期的運行。這種技術被許多語言使用,最為知名的當屬C++,其他還有Curl、D、Eiffel,以及語言擴展,如Template Haskell。

基本介紹

  • 中文名:模板元編程
  • 外文名:Template metaprogramming
  • 分類:計算機編程
  • 縮寫:TMP
模板元編程的構成要素,使用模板元編程,編譯期類別生成,編譯期代碼最最佳化,靜態多態,模板元編程的優缺點,相關內容,

模板元編程的構成要素

使用模板作為元編程的技術需要兩階段的操作。首先,模板必須被定義;第二,定義的模板必須被實體化才行。 模板的定義描述了生成源碼的一般形式,而使實體化則導致了某些源碼的組合根據該模板而生成。
模板元編程是一般性地圖靈完全(Turing-complete),這意味著任何可被電算軟體表示的運算都可以透過模板元編程以某種形式去運算。
模板與宏(macros)是不同的。所謂宏只不過是以文字的操作及替換來生成代碼,雖然同為編譯期的語言特色,但宏系統通常其編譯期處理流的能力(compile-time process flow abilities)有限,且對其所依附之語言的語義及類型系統缺乏認知(一個例外是LISP的宏)。
模板元編程沒有可變的變數——也就是說,變數一旦初始化後就不能夠改動。因此他可以被視為函式式編程(functional programming)的一種形式。

使用模板元編程

模板元編程的語法通常與一般的程式語法迥異,他有其實際的用途。一些使用模板元編程的共同理由是為了實現泛型編程generic programming)或是展現自動編譯期最最佳化,像是只要在編譯期做某些事一次就好,而無需每次程式運行時去做。

編譯期類別生成

以下將展示究竟何謂"編譯期程式設計"。階乘是一個不錯的例子,在此之前我們先來回顧一下一般C++中階乘函式的遞歸寫法:
int factorial(int n) {    if (n == 0)       return 1;    return n * factorial(n - 1);}void foo(){    int x = factorial(4); // == (4 * 3 * 2 * 1 * 1) == 24    int y = factorial(0); // == 0! == 1}
以上的代碼會在程式運行時決定4和0的階乘。
現在讓我們看看使用了模板元編程的寫法,模板特化提供了"遞歸"的結束條件。這些階乘可以在編譯期完成計算。以下源碼:
template <int N>struct Factorial {    enum { value = N * Factorial<N - 1>::value };};template <>struct Factorial<0> {    enum { value = 1 };};// Factorial<4>::value == 24// Factorial<0>::value == 1void foo(){    int x = Factorial<4>::value; // == 24    int y = Factorial<0>::value; // == 1}
代碼如上在編譯時期計算4和0的階乘值,使用該結果值仿佛他們是預定的常量一般。
雖然從程式功能的觀點來看,這兩個版本很類似,但前者是在運行期計算階乘,而後者卻是在編譯期完成計算。 然而,為了能夠以此方式使用模板,編譯器必須在編譯期知道模板的參數值,也就是Factorial<X>::value只有當X在編譯期已知的情況下才能使用。換言之,X必須是常量(constant literal)或是常量表示式(constant expression),像是使用sizeof運算符。

編譯期代碼最最佳化

以上階乘的示例便是編譯期代碼最最佳化的一例,該程式中使用到的階乘在編譯期時便被預先計算並作為數值常量植入運行碼當中,節省了運行期的經常開銷(計算時間)以及存儲器足跡(memory footprint)。
編譯期循環展開(loop-unrolling)是一個更顯著的例子,模板元編程可以被用來產生n維(n-dimensional)的向量類別(當然n必須在編譯期已知)。與傳統n維向量比較,他的好處是循環可以被展開,這可以使性能大幅度提升。考慮以下例子,n維向量的加法可以被寫成:
template <int dimension>Vector<dimension>& Vector<dimension>::operator+=(const Vector<dimension>& rhs) {    for (int i = 0; i < dimension; ++i)        value[i] += rhs.value[i];    return *this;}
當編譯器實體化以上的模板函式,可能會生成如下的代碼:
template <>Vector<2>& Vector<2>::operator+=(const Vector<2>& rhs) {    value[0] += rhs.value[0];    value[1] += rhs.value[1];    return *this;}
因為模板參數dimension在編譯期是常量,所以編譯器應能展開for循環。

靜態多態

多態是一項共通的標準編程工具,派生類的對象可以被當作基類的對象之實體使用,但能夠調用派生對象的函式,或稱方法(methods),例如以下的代碼:
class Base{    public:    virtual void method() { std::cout << "Base"; }};class Derived : public Base{    public:    virtual void method() { std::cout << "Derived"; }};int main(){    Base *pBase = new Derived;    pBase->method(); //outputs "Derived"    delete pBase;    return 0;}
喚起的virtual函式是屬於位於繼承最下位之類別的。這種動態多態(dynamically polymorphic)行為是藉由擁有虛函式的類別所產生的虛函式表(virtual look-up tables)來實行的。虛函式表會在運行期被查找,以決定該喚起哪個函式。因此動態多態無可避免地必須承擔這些運行期成本。
然而,在許多情況下我們需要的僅是可以在編譯期決定,無需變動的多態行為。那么一來,奇怪的遞歸模板樣式(Curiously Recurring Template Pattern;CRTP)便可被用來達成靜態多態。如下例:
template <class Derived>struct base{    void interface()    {         // ...         static_cast<Derived*>(this)->implementation();         // ...    }};struct derived : base<derived>{     void implementation();};
這裡基類模板有著這樣的優點:成員函式的本體在被他們的宣告之前都不會被實體化,而且它可利用static_cast並透過自己的函式來使用派生類的成員,所以能夠在編譯時產生出帶有多態特性的對象複合物。在現實使用上,Boost疊代器庫[1]便有採用 CRTP 的技法。
其他類似的使用還有"Barton-Nackman trick(英文)",有時候被稱作"有限制的模板擴張",共同的功能被可以放在一個基類當中,作為必要的構件使用,以此確保在縮減多餘代碼時的一致行為。

模板元編程的優缺點

  • 編譯期對運行期:因為模板的運算以及展開都是在編譯期,這會花相對較長的編譯時間,但能夠獲得更有效率的運行碼。這項編譯期花費一般都很小,但對於大項目或是普遍依賴模板的程式,也許會造成很大的編譯開銷。
  • 泛型程式設計:模板元編程允許程式設計師專注在架構上並委託編譯器產生任何客戶碼要求的實現。因此,模板元編程可達成真正的泛用代碼,促使代碼縮小並較好維護。
  • 可讀性:對於C++來說,模板元編程的語法及語言特性比起傳統的C++編程,較難以令人理解。因此對於那些在模板元編程經驗不豐富的程式設計師來說,程式可能會變的難以維護。(這要視各語言對於模板元編程語法的實現)
  • 移植性:對於C++來說,由於各編譯器的差異,大量依賴模板元編程(特別是最新形式的)的代碼可能會有移植性的問題。

相關內容

相關詞條

熱門詞條

聯絡我們