C++ 程式設計/模板/模板超程式設計
模板超程式設計 (TMP) 指的是使用 C++ 模板系統在程式碼中進行編譯時計算。在很大程度上,它可以被認為是“用型別程式設計”——因為 TMP 使用的“值”主要是特定的 C++ 型別。使用型別作為基本計算物件允許使用型別推斷規則的全部功能來進行通用計算。
預處理器允許在編譯時執行某些計算,這意味著在程式碼編譯完成後,決策已經做出,並且可以從編譯後的可執行檔案中排除。以下是一個非常人為的示例
#define myvar 17
#if myvar % 2
cout << "Constant is odd" << endl;
#else
cout << "Constant is even" << endl;
#endif
這種構造除了條件包含平臺特定程式碼外,沒有太多應用。特別是沒有辦法迭代,因此它不能用於通用計算。使用模板進行編譯時程式設計的工作方式類似,但功能更強大,實際上是圖靈完備的。
特質類是簡單形式的模板超程式設計的常見示例:給定型別的輸入,它們計算與該型別相關的屬性作為輸出(例如,std::iterator_traits<> 採用迭代器型別作為輸入,並計算屬性,例如迭代器的 difference_type、value_type 等等)。
模板超程式設計比普通慣用的 C++ 更接近函數語言程式設計。這是因為 '變數' 都是不可變的,因此必須使用遞迴而不是迭代來處理集合中的元素。這對學習 TMP 的命令式程式設計師來說增加了另一層挑戰:除了學習它的機制,他們還必須學習用不同的方式思考。
由於模板超程式設計是從模板系統的意外使用中發展而來的,因此它經常很繁瑣。通常很難讓維護人員清楚地瞭解程式碼的意圖,因為所用程式碼的自然含義與它的使用目的截然不同。最有效的處理方法是依賴成語;如果你想成為一名高效的模板元程式設計師,你將必須學會識別常見的成語。
它也挑戰了舊編譯器的能力;一般來說,2000 年左右及以後的編譯器能夠處理大部分實際的 TMP 程式碼。即使編譯器支援它,編譯時間也可能非常長,並且在出現編譯錯誤的情況下,錯誤資訊往往難以理解。有關模板例項化偵錯程式,請參閱 TempLight。
一些編碼標準甚至可能禁止模板超程式設計,至少在 Boost 等第三方庫之外。
從歷史上看,TMP 有點像是意外;在標準化 C++ 語言的過程中,人們發現它的模板系統碰巧是圖靈完備的,即原則上能夠計算任何可計算的東西。第一個具體的證明是由 Erwin Unruh 編寫的程式,它計算了素數儘管它實際上沒有完成編譯:素數列表是編譯器在嘗試編譯程式碼時生成的錯誤訊息的一部分。[1] 從那時起,TMP 已經有了很大的進步,現在是 C++ 庫構建者的實用工具,但它的複雜性意味著它通常不適用於大多數應用程式或系統程式設計環境。
#include <iostream>
template <int p, int i>
class is_prime {
public:
enum { prim = ( (p % i) && is_prime<p, i - 1>::prim ) };
};
template <int p>
class is_prime<p, 1> {
public:
enum { prim = 1 };
};
template <int i>
class Prime_print { // primary template for loop to print prime numbers
public:
Prime_print<i - 1> a;
enum { prim = is_prime<i, i - 1>::prim };
void f() {
a.f();
if (prim)
{
std::cout << "prime number:" << i << std::endl;
}
}
};
template<>
class Prime_print<1> { // full specialization to end the loop
public:
enum { prim = 0 };
void f() {}
};
#ifndef LAST
#define LAST 18
#endif
int main()
{
Prime_print<LAST> a;
a.f();
}
TMP 中的 '變數' 實際上不是變數,因為它們的值不能改變,但你可以擁有命名值,就像你在普通程式設計中使用變數一樣。在用型別程式設計時,命名值是型別定義
struct ValueHolder
{
typedef int value;
};
你可以將其視為“儲存”int 型別,以便可以在 value 名稱下訪問它。整數值通常儲存為 enum 中的成員
struct ValueHolder
{
enum { value = 2 };
};
這再次儲存了該值,以便可以在 value 名稱下訪問它。這兩個示例本身都沒有用,但它們構成了大多數其他 TMP 的基礎,因此它們是必須注意的重要模式。
函式將一個或多個輸入引數對映到一個輸出值。TMP 中的類似物是模板類
template<int X, int Y>
struct Adder
{
enum { result = X + Y };
};
這是一個將兩個引數相加並將結果儲存在 result enum 成員中的函式。你可以在編譯時使用類似 Adder<1, 2>::result 的方法呼叫它,它將在編譯時展開,並在你的程式中與文字 3 完全相同。
條件分支可以透過編寫模板類的兩個替代特化來構造。編譯器將選擇適合所提供型別的特化,然後可以訪問例項化類中定義的值。例如,考慮以下部分特化
template<typename X, typename Y>
struct SameType
{
enum { result = 0 };
};
template<typename T>
struct SameType<T, T>
{
enum { result = 1 };
};
這告訴我們它例項化的兩個型別是否相同。這可能看起來沒什麼用,但它可以看穿型別定義,否則這些型別定義可能會掩蓋型別是否相同,並且它可以用於模板程式碼中的模板引數。你可以像這樣使用它
if (SameType<SomeThirdPartyType, int>::result)
{
// ... Use some optimised code that can assume the type is an int
}
else
{
// ... Use defensive code that doesn't make any assumptions about the type
}
上面的程式碼不太慣用:由於型別可以在編譯時識別,因此 if() 塊將始終具有一個微不足道的條件(它將始終解析為 if (1) { ... } 或 if (0) { ... })。但是,這確實說明了可以實現的這種型別的事情。
由於在使用模板程式設計時無法使用可變變數,因此無法迭代值序列。在標準 C++ 中可能透過迭代完成的任務必須用遞迴重新定義,即一個函式呼叫自身。這通常採用模板類的形式,其輸出值遞迴地引用自身,並且一個或多個特化會為其提供固定值以防止無限遞迴。您可以將其視為上面描述的函式和條件分支思想的結合。
計算階乘自然地透過遞迴完成:,對於 ,。在 TMP 中,這對應於一個名為“factorial”的類模板,其通用形式使用遞迴關係,並且其特化終止遞迴。
首先,通用(未特化)模板表示 factorial<n>::value 由以下給出:n*factorial<n-1>::value:
template <unsigned n>
struct factorial
{
enum { value = n * factorial<n-1>::value };
};
接下來,零的特化表示 factorial<0>::value 計算為 1
template <>
struct factorial<0>
{
enum { value = 1 };
};
現在,一些在編譯時“呼叫”factorial 模板的程式碼
int main() {
// Because calculations are done at compile-time, they can be
// used for things such as array sizes.
int array[ factorial<7>::value ];
}
觀察到 factorial<N>::value 成員是根據 factorial<N> 模板表示的,但這不能無限期地繼續:每次它被評估時,它都會使用一個越來越小的(但非負)數字呼叫自身。這最終必須達到零,此時特化生效,評估不會進一步遞迴。
示例:編譯時“If”
[edit | edit source]以下程式碼定義了一個名為“if_”的元函式;這是一個類模板,可用於根據編譯時常量在兩種型別之間進行選擇,如以下 main 中所示
template <bool Condition, typename TrueResult, typename FalseResult>
class if_;
template <typename TrueResult, typename FalseResult>
struct if_<true, TrueResult, FalseResult>
{
typedef TrueResult result;
};
template <typename TrueResult, typename FalseResult>
struct if_<false, TrueResult, FalseResult>
{
typedef FalseResult result;
};
int main()
{
typename if_<true, int, void*>::result number(3);
typename if_<false, int, void*>::result pointer(&number);
typedef typename if_<(sizeof(void *) > sizeof(uint32_t)), uint64_t, uint32_t>::result
integral_ptr_t;
integral_ptr_t converted_pointer = reinterpret_cast<integral_ptr_t>(pointer);
}
在第 18 行,我們使用真值評估 if_ 模板,因此使用的型別是提供的第一個值。因此,整個表示式 if_<true, int, void*>::result 計算為 int。類似地,在第 19 行,模板程式碼計算為 void *。這些表示式與在原始碼中將型別寫為字面量值的效果完全相同。
第 21 行開始變得巧妙:我們定義了一個依賴於平臺相關 sizeof 表示式的值的型別。在指標為 32 位或 64 位的平臺上,這將在編譯時選擇正確的型別,無需任何修改,也無需預處理器宏。一旦型別被選中,它就可以像任何其他型別一樣使用。
為了比較,這個問題在 C90 中最好用以下方法解決
# include <stddef.h>
typedef size_t integral_ptr_t;
typedef int the_correct_size_was_chosen [sizeof (integral_ptr_t) >= sizeof (void *)? 1: -1];
碰巧的是,庫定義的型別 size_t 應該是任何平臺上此特定問題的正確選擇。為了確保這一點,第 3 行用作編譯時檢查,以檢視所選型別是否確實足夠大;如果不是,陣列型別 the_correct_size_was_chosen 將使用負長度定義,導致編譯時錯誤。在 C99 中,<stdint.h> 可能定義型別 intptr_t 和 uintptr_t。
除錯 TMP
[edit | edit source]截至 2017 年,這無法以任何有意義的方式完成。通常,拋棄模板並重新開始比嘗試破譯由模板元程式中單個位元組的錯誤造成的編譯器輸出的複雜迷宮更容易。
請考慮來自 C++ 標準化委員會秘書 Herb Sutter 的以下觀察結果
Herb: Boost.Lambda, is a marvel of engineering… and it worked very well if … if you spelled it exactly right the first time, and didn’t mind a 4-page error spew that told you almost nothing about what you did wrong if you spelled it a little wrong. …
Charles: talk about giant error messages… you could have templates inside templates, you could have these error messages that make absolutely no sense at all.
Herb: oh, they are baroque.
來源:https://ofekshilon.com/2012/09/01/meta-programming-is-still-evil/
