程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> 關於C++ >> C++箴言:謹慎使用模板元編程

C++箴言:謹慎使用模板元編程

編輯:關於C++

template metaprogramming (TMP)(模板元編程)是寫 template-based(基於模板)的運行於編譯期間的 C++ 程序的過程。考慮一下:一個 template metaprogram(模板元程序)是用 C++ 寫的運行於 C++ 編譯器中的程序。當一個 TMP 程序運行完成,它的輸出——從 templates(模板)實例化出的 C++ 源代碼片斷——隨後被正常編譯。

如果你僅把它看作古怪的特性而沒有打動你,那你就不會對它有足夠的深入的思考。

C++ 並不是為 template metaprogramming(模板元編程)設計的,但是自從 TMP 在 1990 年代早期被發現以來,它已被證明非常有用,使 TMP 變容易的擴展很可能會被加入到語言和它的標准庫之中。是的,TMP 是被發現,而不是被發明。TMP 所基於的特性在 templates(模板)被加入 C++ 的時候就已經被引進了。所需要的全部就是有人注意到它們能夠以一種精巧的而且意想不到的方式被使用。

TMP 有兩個強大的力量。首先,它使得用其它方法很難或不可能的一些事情變得容易。第二,因為 template metaprograms(模板元程序)在 C++ 編譯期間執行,它們能將工作從運行時轉移到編譯時。一個結果就是通常在運行時才能被察覺的錯誤能夠在編譯期間被發現。另一個結果是 C++ 程序使得 TMP 的使用在以下每一個方面都能更有效率:更小的可執行代碼,更短的運行時間,更少的內存需求。(然而,將工作從運行時轉移到編譯時的一個結果就是編譯過程變得更長。使用 TMP 的程序可能比它們的 non-TMP 對等物占用長得多的編譯時間。)

考慮STL的advance偽代碼。(在《C++箴言:為類型信息使用特征類》中。你現在可能需要讀該文,因為在本文中,我假設你已經熟悉了該文的內容。),我突出表示代碼中的偽代碼部分:

template<typename IterT, typename DistT>
void advance(IterT& iter, DistT d)
{
if (iter is a random access iterator) {
iter += d; // use iterator arithmetic
} // for random access iters
else {
if (d >= 0) { while (d--) ++iter; } // use iterative calls to
else { while (d++) --iter; } // ++ or -- for other
} // iterator categories
}

我們可以用 typeid 把偽代碼變成真正的代碼。這就產生了一個解決此問題的“常規”的 C++ 方法——它的全部工作都在運行時做:

template<typename IterT, typename DistT>
void advance(IterT& iter, DistT d)
{
if (typeid(typename std::iterator_traits<IterT>::iterator_category) ==
typeid(std::random_access_iterator_tag)) {
iter += d; // use iterator arithmetic
} // for random access iters
else {
if (d >= 0) { while (d--) ++iter; } // use iterative calls to
else { while (d++) --iter; } // ++ or -- for other
} // iterator categories
}

《C++箴言:為類型信息使用特征類》中指出這個 typeid-based(基於 typeid)的方法比使用 traits 的方法效率低,因為這個方法,(1)類型檢測發生在運行時而不是編譯期,(2)用來做運行時類型檢測的代碼必須出現在可執行代碼中。實際上,這個例子展示了 TMP 如何能比一個“常規”C++ 程序更高效,因為 traits 方法是 TMP。記住,traits 允許編譯時在類型上的 if...else 計算。

我先前談及一些事情在 TMP 中比在“常規”C++ 中更簡單,而 advance 提供了這方面的一個例子。Item 47 提到 advance 的 typeid-based(基於 typeid)的實現可能會導致編譯問題,而這就是一個產生問題的例子:

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

考慮 advance 為上面這個調用生成的版本。用 iter 和 10 的類型取代 template parameters(模板參數)IterT 和 DistT 之後,我們得到這個:

void advance(std::list<int>::iterator& iter, int d)
{
if (typeid(std::iterator_traits<std::list<int>::iterator>::iterator_category) ==
typeid(std::random_access_iterator_tag)) {
iter += d; // error!
}
else {
if (d >= 0) { while (d--) ++iter; }
else { while (d++) --iter; }
}
}

問題在突出顯示的行,使用了 += 的那行。在當前情況下,我們試圖在一個 list<int>::iterator 上使用 +=,但是 list<int>::iterator 是一個 bidirectional iterator(雙向迭代器)(參見《C++箴言:為類型信息使用特征類》),所以它不支持 +=。只有 random access iterators(隨機訪問迭代器)才支持 +=。此時,我們知道我們永遠也不會試圖執行那個 += 行,因為那個 typeid 檢測對於 list<int>::iterators 永遠不成立,但是編譯器被責成確保所有源代碼是正確的,即使它不被執行,而當 iter 不是一個 random access iterator(隨機訪問迭代器)時 "iter += d" 是不正確的。traits-based(基於 traits)的 TMP 解決方案與此對比,那裡針對不同類型的代碼被分離到單獨的函數中,其中每一個都只使用了可用於它所針對的類型的操作。

TMP 已經被證明是 Turing-complete(圖靈完備)的,這意味著它強大得足以計算任何東西。使用 TMP,你可以聲明變量,執行循環,編寫和調用函數,等等。但是這些結構看起來與其在“常規”C++ 中的樣子非常不同。例如,《C++箴言:為類型信息使用特征類》展示了 if...else 條件在 TMP 中是如何通過 templates(模板)和 template specializations(模板特化)被表達的。但那是 assembly-level(匯編層次)的 TMP。針對 TMP 的庫提供了一種更高層次的語法,雖然還不至於讓你把它誤認為是“常規”C++。

為了一窺其它東西在 TMP 中如何工作,讓我們來看看 loops(循環)。TMP 中沒有真正的 looping construct(循環結構),因此 loops(循環)的效果是通過 recursion(遞歸)完成的。(如果你對 recursion(遞歸)感到不舒服,在你斗膽進入 TMP 之前一定要解決它。TMP 很大程度上是一個 functional language(函數性語言),而 recursion(遞歸)之於 functional language(函數性語言)就像電視之於美國流行文化:是密不可分的。)然而,甚至 recursion(遞歸)都不是常規樣式的,因為 TMP loops 不涉及 recursive function calls(遞歸函數調用),它們涉及 recursive template instantiations(遞歸模板實例化)。

TMP 的 "hello world" 程序在編譯期間計算一個階乘。它不是一個很令人興奮的程序,不過,即使不是 "hello world",也有助於語言入門。TMP 階乘計算示范了通過 recursive template instantiation(遞歸模板實例化)實現循環。它也示范了在 TMP 中創建和使用變量的一種方法。看:

template<unsigned n> // general case: the value of
struct Factorial { // Factorial<n> is n times the value
// of Factorial<n-1>
enum { value = n * Factorial<n-1>::value };
};
template<> // special case: the value of
struct Factorial<0> { // Factorial<0> is 1
enum { value = 1 };
};

給出這個 template metaprogram(模板元程序)(實際上只是單獨的 template metafunction(模板元函數)Factorial),你可以通過引用 Factorial<n>::value 得到 factorial(n) 的值。

代碼的循環部分出現在 template instantiation(模板實例化)Factorial<n> 引用 template instantiation(模板實例化)Factorial<n-1> 的地方。就像所有正確的 recursion(遞歸)有一個導致遞歸結束的特殊情況。這裡,它就是 template specialization(模板特化)Factorial<0>。

Factorial template 的每一個 instantiation(實例化)都是一個 struct,而每一個 struct 都使用 enum hack聲明了一個名為 value 的 TMP 變量。value 用於持有階乘計算的當前值。如果 TMP 有一個真正的循環結構,value 會在每次循環時更新。因為 TMP 在循環的位置使用 recursive template instantiation(遞歸模板實例化),每一個 instantiation(實例化)得到它自己的 value 的拷貝,而每一個拷貝擁有適合於它在“循環”中所處的位置的值。

你可以像這樣使用 Factorial:

int main()
{
std::cout << Factorial<5>::value; // prints 120
std::cout << Factorial<10>::value; // prints 3628800
}

如果你覺得這比吃了冰淇淋還涼快,你就具有了一個 template metaprogrammer(模板元程序員)應有的素質。如果 templates(模板)以及 specializations(特化)以及 recursive instantiations(遞歸實例化)以及 enum hacks 以及對類似 Factorial<n-1>::value 這樣的類型的需要使你毛骨悚然,好吧,你是一個不錯的常規 C++ 程序員。

當然,Factorial 示范的 TMP 的效用大約就像 "hello world" 示范的任何常規編程語言的效用一樣。為了領會為什麼 TMP 值得了解,更好地理解它能做什麼是很重要的。這裡是三個示例:

Ensuring dimensional unit correctness(確保計量單位正確性)。在科學和工程應用中,計量單位(例如,質量,距離,時間,等等)被正確組合是基礎。例如,將一個代表質量的變量賦值給一個代表速度的變量是一個錯誤,但是用一個時間變量去除距離變量並將結果賦給一個速度變量就是正確的。使用 TMP,不論計算多麼復雜,確保(在編譯期間)一個程序中所有計量單位組合都是正確的是有可能的。(這是一個如何用 TMP 進行早期錯誤診斷的例子。)這個 TMP 的使用的一個有趣的方面是能夠支持分數指數。這需要這個分數在編譯期間被簡化以便於編譯期能夠確認,例如,單位 time1/2 與單位 time4/8 是相同的。

Optimizing matrix operations(優化矩陣操作)。《C++箴言:必須返回對象時別返回引用》中闡釋了一些函數,包括 operator*,必須返回新的 objects,而《C++箴言:從模板中分離出參數無關的代碼》一文中則引入了 SquareMatrix class,所以考慮如下代碼:

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

用“常規”方法計算 result 需要四個臨時矩陣的創建,用於每一次調用 operator* 的結果。此外,獨立的乘法產生了一個四次循環遍歷矩陣元素的序列。使用一種與 TMP 相關的被稱為 expression templates(表達式模板)的高級模板技術,完全不改變上面的客戶代碼的語法,而消除臨時對象以及合並循環是有可能的。最終的軟件使用更少的內存而且運行速度戲劇性地更快。

Generating custom design pattern implementations(生成自定義的設計模式實現)。像 Strategy(參見《C++箴言:考慮可選的虛擬函數的替代方法》),Observer,Visitor 等設計模式能用很多方法實現。使用一種被稱為 policy-based design(基於 policy 設計)的 TMP-based(基於 TMP)的技術,使得創建代表獨立的設計選擇的 templates ("policies") 成為可能,這種 templates 能以任意的方法組合以產生帶有自定義行為的模式實現。例如,這種技術經常用於允許幾個實現了 smart pointer behavioral(智能指針行為)的 policies 的 templates 生成(在編譯期間)數百個不同的 smart pointer(智能指針)類型。將類似設計模式和智能指針這樣的編程器件的范圍大大地擴展,這項技術是通常所說的 generative programming(產生式編程)的基礎。

  TMP 並不適合於每一個人。它的語法是不符合直覺的,工具支持也很弱(template metaprograms 的調試器?哈!)作為一個相對晚近才發現的“附屬”語言,TMP programming 的規則仍然帶有試驗性質。然而,通過將工作從運行時轉移到編譯時所提供的效率提升還是能給人留下深刻的印象,而表達在運行時很難或不可能實現的行為的能力也相當有吸引力。

TMP 的支持程度在不斷提升。很可能在 C++ 的下一個版本中將對它提供直接的支持,而且 TR1 已經這樣做了。關於這一主題的書籍也即將開始出版(目前,C++ Template Metaprogramming: Concepts, Tools, and Techniques from Boost and Beyond 已經出版——譯者注),而 web 上的 TMP 信息也正在保持增長。TMP 也許永遠不會成為主流,但是對於某些程序員——特別是庫開發者——它幾乎必然會成為主料。

Things to Remember

·template metaprogramming(模板元編程)能將工作從運行時轉移到編譯時,這樣就能夠更早察覺錯誤並提高運行時性能。

·TMP 能用於在 policy choices 的組合的基礎上生成自定義代碼,也能用於避免為特殊類型生成不適當的代碼。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved