C++ 模板元編程之模板特化的概念從何而來
0. 前言
C++ 里的模板能做什么呢?它好比 C 語言中的宏、C# 和 Java 中的自省(restropection)和反射(reflection),是 C++ 語言的外延。更極端一點地理解:它是一門新的圖靈完備的編程語言(也就是說,C++ 模板能實現(xiàn)圖靈機模型里的全部功能)。在《Modern C++ Design》中,作者拋出了以下幾個問題:
(1)如何撰寫更高級的C++程式?
(2)如何應(yīng)付即使在很干凈的設(shè)計中仍然像雪崩一樣的不相干細(xì)節(jié)?
(3)如何構(gòu)建可復(fù)用組件,使得每次在不同程式中應(yīng)用組件時無需大動干戈?
解決上述問題的方法就是模板元編程。元(meta)本身就是個很“抽象(abstract)”的詞,因為它的本意就是“抽象”。元編程,也可以說就是“編程的抽象”。用更好理解的說法,元編程意味著你撰寫一段程序A,程序A會運行后生成另外一個程序B,程序B才是真正實現(xiàn)功能的程序。那么這個時候程序A可以稱作程序B的元程序,撰寫程序A的過程,就稱之為“元編程”。
C++中,元編程的手段,可以是宏,也可以是模板。
1、為什么需要泛型編程:從宏到模板,再到元編程
如果元編程中所有的變量(或者說元編程的參數(shù)),都是類型,那么這樣的編程,我們有個特定的稱呼,叫“泛型”。
模板的發(fā)明,僅僅是為了做和宏幾乎一樣的替換工作嗎?可以說是,也可以說不是。
一方面,模板可以用來替換類型,這點和宏沒什么區(qū)別。只是宏在編譯階段基于文本做純粹替換,被替換的文本本身沒有任何語義。而模板會在分析模板時以及實例化模板的時候都會進行檢查,而且源代碼中也能與調(diào)試符號一一對應(yīng),所以無論是編譯時還是運行時,排錯都相對簡單。
另一方面,模板和宏也有很大的不同,模板最大的不同在于它是“可以運算”的。我們來看一個例子:
- void Add(uint8_t, unit8_t) {}
上述函數(shù)實現(xiàn)了一個 uint8_t 和 uint8_t 類型的加法運算,如果現(xiàn)在要實現(xiàn) int16 和 int16 類型的加法運算,該怎么辦呢?簡單點的方法如下:
- if (type == 8) {
- Add(uint8_t, uint8_t)
- } else if (type == 16) {
- Add(uint16_t, uint16_t)
- }
但是這里有兩個難點:
- 首先, if(type == x) 是不存在于 C++ 中的
- 其次,即便存在獲取變量 type 的方法(Boost.Any 中的 typeid ),我們也不希望它在運行時判斷,這樣會變得很慢。是否可以不引入 if/else,在編譯期就把 Add 的方法確定呢?
有人說,重載、虛函數(shù)也能解決如上問題:
- void Add(uint8_t, uint8_t) {}
- void Add(uint16_t, uint16_t) {}
甚至在 C 語言中定義新的結(jié)構(gòu)體 Variant 或使用 void* 也能解決該問題:
- struct Variant
- {
- union
- {
- uint_8 x;
- uint_16 y;
- } data;
- uint32_t typeId;
- };
沒錯,但是如果我還有 uint9_t、uint10_t 等各種類型的加法運算呢?Anyway,不管是哪種方法都很難避免 if/else 的存在。
模板與上述這些方法最大的區(qū)別在于:模板無論其參數(shù)或者是類型,它都是一個編譯期分派的方法。編譯期就能確定的東西既可以做類型檢查,編譯器也能進行優(yōu)化,砍掉任何不必要的代碼執(zhí)行路徑。
2、類模板的特化:模板世界里的 if/else
2.1 根據(jù)類型執(zhí)行代碼
我們先來看看一個模板的例子:
- template <typename T> T AddFloatOrMulInt(T a, T b);
- // 我們希望這個函數(shù)在 T 是 float 的時候做加法運算,在 T 是 int 類型的時候做乘法運算
那么當(dāng)傳入兩個不同類型的變量,或者不是 int 和 float 變量,編譯器就會提示錯誤。
從能力上來看,模板能做的事情都是編譯期完成的。編譯期完成的意思就是,當(dāng)你編譯一個程序的時候,所有的量就都已經(jīng)確定了。比如下面的例子:
- int a = 3, b = 5;
- Variant aVar, bVar;
- aVar.setInt(a); // 我們新加上的方法,怎么實現(xiàn)的無所謂,大家明白意思就行了。
- bVar.setInt(b);
- Variant result = AddFloatOrMulInt(aVar, bVar);
從上述代碼中我們可以看到:aVar 和 bVar 都一定會是整數(shù)。所以如果有合適的機制,編譯器就能知道此處的 AddFloatOrMulInt 中只需要執(zhí)行 int 路徑上的代碼,而且編譯器在此處也能單獨為 int 路徑生成代碼,從而去掉那個不必要的 if。在模板代碼中,這個“合適的機制”就是指“特化”和“部分特化(Partial Specialization)”,后者也叫“偏特化”。
2.2、如何寫模板特化的代碼
1.0 版本 - 偽代碼
- int/float AddFloatOrMulInt(a, b) // 類的靜態(tài)函數(shù)
- {
- if(type is int) {
- return a * b;
- } else if (type is float) {
- return a + b;
- }
- }
- void foo()
- {
- float a, b, c;
- c = addFloatOrMulInt(a, b); // c = a + b;
- int x, y, z;
- z = addFloatOrMulInt(x, y); // z = x * y;
- }
2.0 版本 - 函數(shù)重載
- float AddFloatOrMulInt(float a, float b)
- {
- return a + b;
- }
- int AddFloatOrMulIntDo(int a, int b)
- {
- return a * b;
- }
- void foo()
- {
- float a, b, c;
- c = AddFloatOrMulInt(a, b); // c = a + b;
- int x, y, z;
- z = AddFloatOrMulInt(x, y); // z = x * y;
- }
3.0 版本 - 純模板
- // 這個是給float用的。
- template <typename T> class AddFloatOrMulInt
- {
- T Do(T a, T b)
- {
- return a + b;
- }
- };
- // 這個是給int用的。
- template <typename T> class AddFloatOrMulInt
- {
- T Do(T a, T b)
- {
- return a * b;
- }
- };
- void foo()
- {
- float a, b, c;
- // 我們需要 c = a + b;
- c = AddFloatOrMulInt<float>::Do(a, b);
- // ... 覺得哪里不對勁 ...
- // ??!有兩個 AddFloatOrMulInt,class 看起來一模一樣,要怎么區(qū)分呢!
- }
好吧,問題來了!如何要讓兩個內(nèi)容不同,但是模板參數(shù)形式相同的類進行區(qū)分呢?特化!特化(specialization)是根據(jù)一個或多個特殊的整數(shù)或類型,給出模板實例化時的一個指定內(nèi)容。
4.0 版本 - 模板特化
- // 首先,要寫出模板的一般形式(原型,即初始化,不能?。?nbsp;
- template <typename T> class AddFloatOrMulInt
- {
- static T Do(T a, T b) // 注意這里必須得是靜態(tài)方法?。?!
- {
- return T(0);
- }
- };
- // 其次,我們要指定T是float時候的代碼:
- template <> class AddFloatOrMulInt<float>
- {
- public:
- static float Do(float a, float b)
- {
- return a + b;
- }
- };
- // 再次,我們要指定T是int時候的代碼,這就是特化:
- template <> class AddFloatOrMulInt<int>
- {
- public:
- static int Do(int a, int b) //
- {
- return a * b;
- }
- };
- int foo()
- {
- return AddFloatOrMulInt<float>::Do(1.0, 2.0);
- }
- int main()
- {
- std::cout << foo(); // 輸出結(jié)果 3.0
- }
解釋:
- // 我們這個模板的基本形式是什么?
- template <typename T> class AddFloatOrMulInt;
- // 但是這個類,是給T是 int 的時候用的,于是我們寫作
- class AddFloatOrMulInt<int>;
- // 當(dāng)然,這里編譯是通不過的。
- // 但是它又不是個普通類,而是類模板的一個特化(特例)。
- // 所以前面要加模板關(guān)鍵字template,以及模板參數(shù)列表
- template </* 這里要填什么? */> class AddFloatOrMulInt<int>;
- // 最后,模板參數(shù)列表里面填什么?因為原型的T已經(jīng)被int取代了。所以這里就不能也不需要放任何額外的參數(shù)了。所以這里放空。
- template <> class AddFloatOrMulInt<int>
- {
- // ... 針對 int 的實現(xiàn) ...
- }
- // Done!
至此,第一個模板特化的代碼已經(jīng)寫完了。這里的 AddFloatOrMulInt 如同是一個函數(shù),卻只能在編譯期間執(zhí)行。如果你體味到了這一點,那么恭喜你,你的模板元編程已經(jīng)開悟了。
3、總結(jié)
本文核心只講了兩個問題:一是為什么需要泛型編程,重點介紹了宏、模板和元編程的關(guān)系;二是模板類的特化代碼如何編寫。關(guān)于特化,還有很多細(xì)節(jié)知識,在之后的文章中我們繼續(xù)探究,另外將還介紹偏特化等知識點,敬請期待。