模板元編程的構成要素
使用模板作為元編程的技術需要兩階段的操作。首先,模板必須被定義;第二,定義的模板必須被
實體化才行。 模板的定義描述了生成源碼的一般形式,而使實體化則導致了某些源碼的組合根據該
模板而生成。
模板元編程是一般性地圖靈完全(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(英文)",有時候被稱作"有限制的模板擴張",共同的功能被可以放在一個基類當中,作為必要的構件使用,以此確保在縮減多餘代碼時的一致行為。