https://m.toutiao.com/is/JuK5kQU/
故事要從一個看起來非常簡單的功能開始:
請計(jì)算兩個數(shù)的和。
如果你對Python很熟悉,你一定會覺得:“哇!這太簡單了!”,然后寫出以下代碼:
def Plus(lhs, rhs): return lhs + rhs
那么,C語言又如何呢?你需要面對這樣的問題:
/* 這里寫什么?*/ Plus(/* 這里寫什么?*/ lhs, /* 這里寫什么?*/ rhs){ return lhs + rhs;}
也許你很快就能想到以下解法中的一些或全部:
int Plus(int lhs, int rhs){ return lhs + rhs;}
顯然,這不是一個好的方案。因?yàn)檫@樣的Plus函數(shù)接口強(qiáng)行的要求兩個實(shí)參以及返回值的類型都必須是int,或是能夠發(fā)生隱式類型轉(zhuǎn)換到int的類型。此時,如果實(shí)參并不是int類型,其結(jié)果往往就是錯誤的。請看以下示例:
int main(){ printf('%d\n', Plus(1, 2)); // 3,正確 printf('%d\n', Plus(1.999, 2.999)); // 仍然是3!}
int Plusi(int lhs, int rhs){ return lhs + rhs;}long Plusl(long lhs, long rhs){ return lhs + rhs;}double Plusd(double lhs, double rhs){ return lhs + rhs;}// ...
這種方案的缺點(diǎn)也很明顯:其使得代碼寫起來像“匯編語言”(movl,movq,...)。我們需要針對不同的類型調(diào)用不同名稱的函數(shù)(是的,C語言也不支持函數(shù)重載),這太可怕了。
#define Plus(lhs, rhs) (lhs + rhs)
這種方案似乎很不錯,甚至“代碼看上去和Python一樣”。但正如許許多多的書籍都討論過的那樣,宏,不僅“拋棄”了類型,甚至“拋棄”了代碼。是的,宏不是C語言代碼,其只是交付于預(yù)處理器執(zhí)行的“復(fù)制粘貼”的標(biāo)記。一旦預(yù)處理完成,宏已然不再存在??上攵?,在功能變得復(fù)雜后,宏的缺點(diǎn)將會越來越大:代碼晦澀,無法調(diào)試,“莫名其妙”的報錯...
看到這里,也許你會覺得:“哇!C語言真爛!居然連這么簡單的功能都無法實(shí)現(xiàn)!”。但請想一想,為什么會出現(xiàn)這些問題呢?讓我們回到故事的起點(diǎn):
請計(jì)算兩個數(shù)的和。
仔細(xì)分析這句話:“請計(jì)算...的和”,意味著“加法”語義,這在C語言中可以通過“+”實(shí)現(xiàn)(也許你會聯(lián)想到匯編語言中的加法實(shí)現(xiàn));而“兩個”,則意味著形參的數(shù)量是2(也許你會聯(lián)想到匯編語言中的ESS、ESP、EBP等寄存器);那么,“數(shù)”,意味著什么語義?C語言中,具有“數(shù)”這一語義的類型有十幾種:int、double、unsigned,等等,甚至char也具有“數(shù)”的語義。那么,“加法”和“+”,“兩個”和“形參的數(shù)量是2”,以及“數(shù)”和int、double、unsigned等等之間的關(guān)系是什么?
是抽象。
高級語言的目的,就是對比其更加低級的語言進(jìn)行抽象,從而使得我們能夠?qū)崿F(xiàn)更加高級的功能。抽象,是一種人類的高級思維活動,是一種充滿著智慧的思維活動。匯編語言抽象了機(jī)器語言,而C語言則進(jìn)一步抽象了匯編語言:其將匯編語言中的各種加法指令,抽象成了一個簡單的加號;將各種寄存器操作,抽象成了形參和實(shí)參...抽象思維是如此的普遍與自然,以至于我們往往甚至忽略了這種思維的存在。
但是,C語言并沒有針對類型進(jìn)行抽象的能力,C語言不知道,也沒有能力表達(dá)“int和double都是數(shù)字”這一語義。而這,直接導(dǎo)致了這個“看起來非常簡單的功能”難以完美的實(shí)現(xiàn)。
針對類型的抽象是如此重要,以至于編程語言世界出現(xiàn)了與C語言這樣的“靜態(tài)類型語言”完全不一樣的“動態(tài)類型語言”。正如開頭所示,在Python這樣的動態(tài)類型語言中,我們根本就不需要為每個變量提供類型,從而似乎“從根本上解決了問題”。但是,“出來混,遲早要還的”,這種看似完美的動態(tài)類型語言,犧牲的卻是極大的運(yùn)行時效率!我們不禁陷入了沉思:真的沒有既不損失效率,又能對類型進(jìn)行抽象的方案了嗎?
正當(dāng)我們一籌莫展,甚至感到些許絕望之時,C++的模板,為我們照亮了前行的道路。
模板,即C++中用以實(shí)現(xiàn)泛型編程思想的語法組分。模板是什么?一言以蔽之:類型也可以是“變量”的東西。這樣的“東西”,在C++中有二:函數(shù)模板和類模板。
通過在普通的函數(shù)定義和類定義中前置template <...>,即可定義一個模板,讓我們以上文中的Plus函數(shù)進(jìn)行說明。請看以下示例:
此為函數(shù)模板:
template <typename T>T Plus(T lhs, T rhs){ return lhs + rhs;}int main(){ cout << Plus(1, 2) << endl; // 3,正確! cout << Plus(1.999, 2.999) << endl; // 4.998,同樣正確!}
此為類模板:
template <typename T>struct Plus{ T operator()(T lhs, T rhs) { return lhs + rhs; }};int main(){ cout << Plus<int>()(1, 2) << endl; // 3,正確! cout << Plus<double>()(1.999, 2.999) << endl; // 4.998,同樣正確!}
顯然,模板的出現(xiàn),使得我們輕而易舉的就實(shí)現(xiàn)了類型抽象,并且沒有(像動態(tài)類型語言那樣)引入任何因?yàn)榇朔N抽象帶來的額外代價。
請看以下示例:
template <typename T>struct Plus{ T operator()(T lhs, T rhs) { return lhs + rhs; }};int main(){ cout << Plus<int>()(1, 2) << endl; cout << Plus<double>()(1.999, 2.999) << endl;}
上例中,typename T中的T,稱為模板形參;而Plus<int>中的int,則稱為模板實(shí)參。在這里,模板實(shí)參是一個類型。
事實(shí)上,模板的形參與實(shí)參既可以是類型,也可以是值,甚至可以是“模板的模板”;并且,模板形參也可以具有默認(rèn)值(就和函數(shù)形參一樣)。請看以下示例:
template <typename T, int N, template <typename U, typename = allocator<U>> class Container = vector>class MyArray{ Container<T> __data[N];};int main(){ MyArray<int, 3> _;}
上例中,我們聲明了三個模板參數(shù):
什么叫“模板的模板參數(shù)”?這里需要明確的是:模板、類型和值,是三個完全不一樣的語法組分。模板能夠“創(chuàng)造”類型,而類型能夠“創(chuàng)造”值。請參考以下示例以進(jìn)行辨析:
vector<int> v;
此例中,vector是一個模板,vector<int>是一個類型,而v是一個值。
所以,一個“模板的模板參數(shù)”,就是一個需要提供給其一個模板作為實(shí)參的參數(shù)。對于上文中的聲明,Container是一個“模板的模板參數(shù)”,其需要接受一個模板作為實(shí)參 。需要怎樣的模板呢?這個模板應(yīng)具有兩個模板形參,且第二形參具有默認(rèn)值allocator<U>;同時,Container具有默認(rèn)值vector,這正是一個符合要求的模板。這樣,Container在類定義中,便可被當(dāng)作一個模板使用(就像vector那樣)。
模板,代表了一種泛化的語義。顯然,既然有泛化語義,就應(yīng)當(dāng)有特化語義。特化,使得我們能為某些特定的類型專門提供一份特殊實(shí)現(xiàn),以達(dá)到某些目的。
特化分為全特化與偏特化。所謂全特化,即一個“披著空空如也的template <>的普通函數(shù)或類”,我們還是以上文中的Plus函數(shù)為例:
// 不管T是什么類型,都將使用此定義...template <typename T>T Plus(T lhs, T rhs){ return lhs + rhs;}// ...但是,當(dāng)T為int時,將使用此定義template <> // 空空如也的template <>int Plus(int lhs, int rhs){ return lhs + rhs;}int main(){ Plus(1., 2.); // 使用泛型版本 Plus(1, 2); // 使用特化版本}
那么,偏特化又是什么呢?除了全特化以外的特化,都稱為偏特化。這句話雖然簡短,但意味深長,讓我們來仔細(xì)分析一下:首先,“除了全特化以外的...”,代表了template關(guān)鍵詞之后的“<>”不能為空,否則就是全特化,這顯而易見;其次,“...的特化”,代表了偏特化也必須是一個特化。什么叫“是一個特化”呢?只要特化版本比泛型版本更特殊,那么此版本就是一個特化版本。請看以下示例:
// 泛化版本template <typename T, typename U>struct _ {};// 這個版本的特殊之處在于:僅當(dāng)兩個類型一樣的時候,才會且一定會使用此版本template <typename T>struct _<T, T> {};// 這個版本的特殊之處在于:僅當(dāng)兩個類型都是指針的時候,才會且一定會使用此版本template <typename T, typename U>struct _<T *, U *> {};// 這個版本“換湯不換藥”,沒有任何特別之處,所以不是一個特化,而是錯誤的重復(fù)定義template <typename A, typename B>struct _<A, B> {};
由此可見,“更特殊”是一個十分寬泛的語義,這賦予了模板極大的表意能力,我們將在下面的章節(jié)中不斷的見到特化所帶來的各種技巧。
函數(shù)模板不是函數(shù),而是一個可以生成函數(shù)的語法組分;同理,類模板也不是類,而是一個可以生成類的語法組分。我們稱通過函數(shù)模板生成函數(shù),或通過類模板生成類的過程為模板實(shí)例化。
模板實(shí)例化具有一個非常重要的特征:惰性。這種惰性主要體現(xiàn)在類模板上。請看以下示例:
template <typename T>struct Test{ void Plus(const T &val) { val + val; } void Minus(const T &val) { val - val; }};int main(){ Test<string>().Plus('abc'); Test<int>().Minus(0);}
上例中,Minus函數(shù)顯然是不適用于string類型的。也就是說,Test類對于string類型而言,并不是“100%完美的”。當(dāng)遇到這種情況時,C++的做法十分寬松:不完美?不要緊,只要不調(diào)用那些“不完美的函數(shù)”就行了。在編譯器層面,編譯器只會實(shí)例化真的被使用的函數(shù),并對其進(jìn)行語法檢查,而根本不會在意那些根本沒有被用到的函數(shù)。也就是說,在上例中,編譯器實(shí)際上只實(shí)例化出了兩個函數(shù):string版本的Plus,以及int版本的Minus。
在這里,“懶惰即美德”占了上風(fēng)。
在C++中,“::”表達(dá)“取得”語義。顯然,“::”既可以取得一個值,也可以取得一個類型。這在非模板場景下是沒有任何問題的,并不會引起接下來即將將要討論的“取得的是一個類型還是一個值”的語義混淆,因?yàn)榫幾g器知道“::”左邊的語法組分的定義。但在模板中,如果“::”左邊的語法組分并不是一個確切類型,而是一個模板參數(shù)的話,語義將不再是確定的。請看以下示例:
struct A { typedef int TypeOrValue; };struct B { static constexpr int TypeOrValue = 0; };template <typename T>struct C{ T::TypeOrValue; // 這是什么?};
上例中,如果T是A,則T::TypeOrValue是一個類型;而如果T是B,則T::TypeOrValue是一個數(shù)。我們稱這種含有模板參數(shù)的,無法立即確定語義的名稱為“依賴型名稱”。所謂“依賴”,意即此名稱的確切語義依賴于模板參數(shù)的實(shí)際類型。
對于依賴型名稱,C++規(guī)定:默認(rèn)情況下,編譯器應(yīng)認(rèn)為依賴型名稱不是一個類型;如果需要編譯器將依賴型名稱視為一個類型,則需要前置typename關(guān)鍵詞。請看以下示例以進(jìn)行辨析:
T::TypeOrValue * N; // T::TypeOrValue是一個值,這是一個乘法表達(dá)式typename T::TypeOrValue * N; // typename T::TypeOrValue是一個類型,聲明了一個這樣類型的指針
可變參數(shù)模板是C++11引入的一個極為重要的語法。這里對其進(jìn)行簡要介紹。
可變參數(shù)模板表達(dá)了“參數(shù)數(shù)量,以及每個參數(shù)的類型都未知且各不相同”這一語義。如果我們希望實(shí)現(xiàn)一個簡單的print函數(shù),其能夠傳入任意數(shù)量,且類型互不相同的參數(shù),并依次打印這些參數(shù)值,此時就需要使用可變參數(shù)模板。
可變參數(shù)模板的語法由以下組分構(gòu)成:
接下來,我們就基于可變參數(shù)模板,實(shí)現(xiàn)這一print函數(shù)。請看以下示例:
// 遞歸終點(diǎn)void print() {}// 分解出一個val + 剩下的所有val// 相當(dāng)于:void print(const T &val, const Types1 &Args1, const Types2 &Args2, const Types3 &Args3, ...)template <typename T, typename... Types>void print(const T &val, const Types &... Args){ // 每次打印一個val cout << val << endl; // 相當(dāng)于:print(Args1, Args2, Args3, ...); // 遞歸地繼續(xù)分解... print(Args...);}int main(){ print(1, 2., '3', '4');}
上例中,我們實(shí)現(xiàn)了一對重載的print函數(shù)。第一個print函數(shù)是一個空函數(shù),其將在“Args...”是空的時候被調(diào)用,以作為遞歸終點(diǎn);而第二個print函數(shù)接受一個val以及余下的所有val作為參數(shù),其將打印val,并使用余下的所有val繼續(xù)遞歸調(diào)用自己。不難發(fā)現(xiàn),第二版本的print函數(shù)具有不斷打印并分解Args的能力,直到Args被完全分解。
“sizeof?這有什么可討論的?”也許你會想。只要你學(xué)過C語言,那么對此必不陌生。那么為什么我們還需要為sizeof這一“平淡無奇”的語法單獨(dú)安排一節(jié)來討論呢?這是因?yàn)閟izeof有兩個對于泛型編程而言極為重要的特性:
上述第一點(diǎn)很好理解,因?yàn)閟izeof所考察的是類型,而類型(當(dāng)然也包含其所占用的內(nèi)存大小),一定是一個編譯期就知道的量(因?yàn)镃++作為一門靜態(tài)類型語言,任何的類型都絕不會延遲到運(yùn)行時才知道,這是動態(tài)類型語言才具有的特性),故sizeof的結(jié)果是一個編譯期常量也就不足為奇了。
上述第二點(diǎn)意味深長。利用此特性,我們可以實(shí)現(xiàn)出一些非常特殊的功能。請看下一節(jié)。
讓我們以一個問題引出這一節(jié)的內(nèi)容:
如何實(shí)現(xiàn):判定類型A是否能夠基于隱式類型轉(zhuǎn)換轉(zhuǎn)為B類型?
乍看之下,這是個十分棘手的問題。此時我們應(yīng)當(dāng)思考的是:如何引導(dǎo)(請注意“引導(dǎo)”一詞的含義)編譯器,在A到B的隱式類型轉(zhuǎn)換可行時,走第一條路,否則,走第二條路?
請看以下示例:
template <typename A, typename B>class IsCastable{private: // 定義兩個內(nèi)存大小不一樣的類型,作為“布爾值” typedef char __True; typedef struct { char _[2]; } __False; // 稻草人函數(shù) static A __A(); // 只要A到B的隱式類型轉(zhuǎn)換可用,重載確定的結(jié)果就是此函數(shù)... static __True __Test(B); // ...否則,重載確定的結(jié)果才是此函數(shù)(“...”參數(shù)的重載確定優(yōu)先級低于其他一切可行的重載版本) static __False __Test(...);public: // 根據(jù)重載確定的結(jié)果,就能夠判定出隱式類型轉(zhuǎn)換是否能夠發(fā)生 static constexpr bool Value = sizeof(__Test(__A())) == sizeof(__True);};
上例比較復(fù)雜,我們依次進(jìn)行討論。
首先,我們聲明了兩個大小不同的類型,作為假想的“布爾值”。也許你會有疑問,這里為什么不使用int或double之類的類型作為False?這是由于C語言并未規(guī)定“int、double必須比char大”,故為了“強(qiáng)行滿足標(biāo)準(zhǔn)”(你完全可以認(rèn)為這是某種“教條主義或形式主義”),這里采用了“兩個char一定比一個char大一倍”這一簡單道理,定義了False。
然后,我們聲明了一個所謂的“稻草人函數(shù)”,這個看似毫無意義的函數(shù)甚至沒有函數(shù)體(因?yàn)椴⒉恍枰?,且接下來的兩個函數(shù)也沒有函數(shù)體,與此函數(shù)同理)。這個函數(shù)唯一的目的就是“獲得”一個A類型的值“給sizeof看”。由于sizeof的不求值特性,此函數(shù)也就不需要(我們也無法提供)函數(shù)體了。那么,為什么不直接使用形如“T()”這樣的寫法,而需要聲明一個“稻草人函數(shù)”呢?我想,不用我說你就已經(jīng)明白原因了:這是因?yàn)椴⒉皇撬械腡都具有默認(rèn)構(gòu)造函數(shù),而如果T沒有默認(rèn)構(gòu)造函數(shù),那么“T()”就是錯誤的。
接下來是最關(guān)鍵的部分,我們聲明了一對重載函數(shù),這兩個函數(shù)的區(qū)別有二:
也就是說,如果我們給這一對重載函數(shù)傳入一個A類型的值時,由于“...”參數(shù)的重載確定優(yōu)先級低于其他一切可行的重載版本,只要A到B的隱式類型轉(zhuǎn)換能夠發(fā)生,重載確定的結(jié)果就一定是調(diào)用第一個版本的函數(shù),返回值為__True;否則,只有當(dāng)A到B的隱式類型轉(zhuǎn)換真的不可行時,編譯器才會“被迫”選擇那個編譯器“最不喜歡的版本”,從而使得返回值為__False。返回值的不同,就能夠直接體現(xiàn)在sizeof的結(jié)果不同上。所以,只需要判定sizeof(__Test(__A()))是多少,就能夠達(dá)到我們最終的目的了。下面請看使用示例:
int main(){ cout << IsCastable<int, double>::Value << endl; // true cout << IsCastable<int, string>::Value << endl; // false}
可以看出,輸出結(jié)果完全符合我們的預(yù)期。
SFINAE(Substitution Failure Is Not An Error,替換失敗并非錯誤)是一個高級模板技巧。首先,讓我們來分析這一拗口的詞語:“替換失敗并非錯誤”。
什么是“替換”?這里的替換,實(shí)際上指的正是模板實(shí)例化;也就是說,當(dāng)模板實(shí)例化失敗時,編譯器并不認(rèn)為這是一個錯誤。這句話看上去似乎莫名其妙,也許你會有疑問:那怎么樣才認(rèn)為是一個錯誤?我們又為什么要討論一個“錯誤的東西”呢?讓我們以一個問題引出這一技巧的意義:
如何判定一個類型是否是一個類類型?
“哇!這個問題似乎比上一個問題更難?。 币苍S你會這么想。不過有了上一個問題的鋪墊,這里我們依然要思考的是:一個類類型,有什么獨(dú)一無二的東西是非類類型所沒有的?(這樣我們似乎就能讓編譯器在“喜歡和不喜歡”之間做出抉擇)
也許你將恍然大悟:類的成員指針。
請看以下示例:
template <typename T>class IsClass{private: // 定義兩個內(nèi)存大小不一樣的類型,作為“布爾值” typedef char __True; typedef struct { char _[2]; } __False; // 僅當(dāng)T是一個類類型時,“int T::*”才是存在的,從而這個泛型函數(shù)的實(shí)例化才是可行的 // 否則,就將觸發(fā)SFINAE template <typename U> static __True __Test(int U::*); // 僅當(dāng)觸發(fā)SFINAE時,編譯器才會“被迫”選擇這個版本 template <typename U> static __False __Test(...);public: // 根據(jù)重載確定的結(jié)果,就能夠判定出T是否為類類型 static constexpr bool Value = sizeof(__Test<T>(0)) == sizeof(__True);};
同樣,我們首先定義了兩個內(nèi)存大小一定不一樣的類型,作為假想的“布爾值”。然后,我們聲明了兩個重載模板,其分別以兩個“布爾值”作為返回值。這里的關(guān)鍵在于,重載模板的參數(shù),一個是類成員指針,另一個是“...”。顯然,當(dāng)編譯器拿到一個T,并準(zhǔn)備生成一個“T::*”時,僅當(dāng)T是一個類類型時,這一生成才是正確的,合乎語法的;否則,這個函數(shù)簽名將根本無法被生成出來,從而進(jìn)一步的使得編譯器“被迫”選擇那個“最不喜歡的版本”進(jìn)行調(diào)用(而不是認(rèn)為這個“根本無法被生成出來”的模板是一個錯誤)。所以,通過sizeof對__Test的返回值大小進(jìn)行判定,就能夠達(dá)到我們最終的目的了。下面請看使用示例:
int main(){ cout << IsClass<double>::Value << endl; // false cout << IsClass<string>::Value << endl; // true}
可以看出,輸出結(jié)果完全符合我們的預(yù)期。
sizeof,作為一個C語言的“入門級”語法,其“永不求值”的特性往往被我們所忽略。本章中,我們充分利用了sizeof的這種“永不求值”的特性,做了很多“表面工程”,僅僅是為了“給sizeof看”;同理,SFINAE技術(shù)似乎也只是在“找編譯器的麻煩,拿編譯器尋開心”。但正是這些“表面工程、找麻煩、尋開心”,讓我們得以實(shí)現(xiàn)了一些非常不可思議的功能。
Traits,中文翻譯為“特性”,Type Traits,即為“類型的特性”。這是個十分奇怪的翻譯,故很多書籍對這個詞選擇不譯,也有書籍將其翻譯為“類型萃取器”,十分生動形象。
Type Traits的定義較為模糊,其大致代表了這樣的一系列技術(shù):通過一個類型T,取得另一個基于T進(jìn)行加工后的類型,或?qū)基于某一標(biāo)準(zhǔn)進(jìn)行分類,得到分類結(jié)果。
本章中,我們以幾個經(jīng)典的Type Traits應(yīng)用,來見識一番此技術(shù)的精妙。
第一個例子較為簡單:我們需要得到T的指針類型,即:得到“T *”。此時,只需要將“T *”通過typedef變?yōu)門ype Traits類的結(jié)果即可。請看以下示例:
template <typename T>struct AddStar { typedef T *Type; };template <typename T>struct AddStar<T *> { typedef T *Type; };int main(){ cout << typeid(AddStar<int>::Type).name() << endl; // int * cout << typeid(AddStar<int *>::Type).name() << endl; // int *}
這段代碼十分簡單,但似乎我們寫了兩遍“一模一樣”的代碼?認(rèn)真觀察和思考即可發(fā)現(xiàn):特化版本是為了防止一個已經(jīng)是指針的類型發(fā)生“升級”而存在的。如果T已經(jīng)是一個指針類型,則Type就是T本身,否則,Type才是“T *”。
上一節(jié),我們實(shí)現(xiàn)了一個能夠?yàn)門“添加星號”的Traits,這一節(jié),我們將實(shí)現(xiàn)一個功能與之相反的Traits:為T“去除星號”。
“簡單!”也許你會想,并很快給出了以下實(shí)現(xiàn):
template <typename T>struct RemoveStar { typedef T Type; };template <typename T>struct RemoveStar<T *> { typedef T Type; };int main(){ cout << typeid(RemoveStar<int>::Type).name() << endl; // int cout << typeid(RemoveStar<int *>::Type).name() << endl; // int}
似乎完成了?不幸的是,這一實(shí)現(xiàn)并不完美。請看以下示例:
int main(){ cout << typeid(RemoveStar<int **>::Type).name() << endl; // int *,哦不!}
可以看到,我們的上述實(shí)現(xiàn)只能去除一個星號,當(dāng)傳入一個多級指針時,并不能得到我們想要的結(jié)果。
這該如何是好?我們不禁想到:如果能夠?qū)崿F(xiàn)一個“while循環(huán)”,就能去除所有的星號了。雖然模板沒有while循環(huán),但我們知道:遞歸正是循環(huán)的等價形式。請看以下示例:
// 遞歸終點(diǎn),此時T真的不是指針了template <typename T>struct RemoveStar { typedef T Type; };// 當(dāng)T是指針時,Type應(yīng)該是T本身(已經(jīng)去除了一個星號)繼續(xù)RemoveStar的結(jié)果template <typename T>struct RemoveStar<T *> { typedef typename RemoveStar<T>::Type Type; };
上述實(shí)現(xiàn)中,當(dāng)發(fā)現(xiàn)T選擇了特化版本(即T本身是指針時),就會遞歸地對T進(jìn)行去星號,直到T不再選擇特化版本,從而抵達(dá)遞歸終點(diǎn)為止。這樣,就能在面對多級指針時,也能夠得到正確的Type。下面請看使用示例:
int main(){ cout << typeid(RemoveStar<int **********>::Type).name() << endl; // int}
可以看出,輸出結(jié)果完全符合我們的預(yù)期。
顯然,使用這樣的Traits是具有潛在的較大代價的。例如上例中,為了去除一個十級指針的星號,編譯器竟然需要實(shí)例化出11個類!但好在這一切均發(fā)生在編譯期,對運(yùn)行效率不會產(chǎn)生任何影響。
讓我們繼續(xù)討論前言中的Plus函數(shù),以引出本節(jié)所要討論的話題。目前我們給出的“最好實(shí)現(xiàn)”如下:
template <typename T>T Plus(T lhs, T rhs){ return lhs + rhs;}int main(){ cout << Plus(1, 2) << endl; // 3,正確!}
但是,只要在上述代碼中添加一個“.”,就立即發(fā)生了問題:
int main(){ cout << Plus(1, 2.) << endl; // 二義性錯誤!T應(yīng)該是int還是double?}
上例中,由于Plus模板只使用了單一的一個模板參數(shù),故要求兩個實(shí)參的類型必須一致,否則,編譯器就不知道T應(yīng)該是什么類型,從而引發(fā)二義性錯誤。但顯然,任何的兩種“數(shù)”之間都應(yīng)該是可以做加法的,所以不難想到,我們應(yīng)該使用兩個而不是一個模板參數(shù),分別作為lhs與rhs的類型,但是,我們立即就遇到了新的問題。請看以下示例:
template <typename T1, typename T2>/* 這里應(yīng)該寫什么?*/ Plus(T1 lhs, T2 rhs){ return lhs + rhs;}
應(yīng)該寫T1?還是T2?顯然都不對。我們應(yīng)該尋求一種方法,其能夠獲取到T1與T2之間的“更強(qiáng)大類型”,并將此“更強(qiáng)大類型”作為返回值。進(jìn)一步的,我們可以以此為基礎(chǔ),實(shí)現(xiàn)出一個能夠獲取到任意數(shù)量的類型之中的“最強(qiáng)大類型”的方法。
應(yīng)該怎么做呢?事實(shí)上,這個問題的解決方案,確實(shí)是難以想到的。請看以下示例:
template <typename A, typename B>class StrongerType{private: // 稻草人函數(shù) static A __A(); static B __B();public: // 3目運(yùn)算符表達(dá)式的類型就是“更強(qiáng)大類型” typedef decltype(true ? __A() : __B()) Type;};int main(){ cout << typeid(StrongerType<int, char>::Type).name() << endl; // int cout << typeid(StrongerType<int, double>::Type).name() << endl; // double}
上例中,我們首先定義了兩個“稻草人函數(shù)”,用以分別“獲取”類型為A或B的值“給decltype看”。然后,我們使用了decltype探測三目運(yùn)算符表達(dá)式的類型,不難發(fā)現(xiàn),decltype也具有sizeof的“不對表達(dá)式進(jìn)行求值”的特性。由于三目運(yùn)算符表達(dá)式從理論上可能返回兩個值中的任意一個,故表達(dá)式的類型就是我們所尋求的“更強(qiáng)大類型”。隨后的用例也證實(shí)了這一點(diǎn)。
有了獲取兩個類型之間的“更強(qiáng)大類型”的Traits以后,我們不難想到:N個類型之中的“最強(qiáng)大類型”,就是N - 1個類型之中的“最強(qiáng)大類型”與第N個類型之間的“更強(qiáng)大類型”。請看以下示例:
// 原型// 通過typename StrongerType<Types...>::Type獲取Types...中的“最強(qiáng)大類型”template <typename... Types>class StrongerType;// 只有一個類型template <typename T>class StrongerType<T>{ // 我自己就是“最強(qiáng)大的” typedef T Type;};// 只有兩個類型template <typename A, typename B>class StrongerType<A, B>{private: // 稻草人函數(shù) static A __A(); static B __B();public: // 3目運(yùn)算符表達(dá)式的類型就是“更強(qiáng)大類型” typedef decltype(true ? __A() : __B()) Type;};// 不止兩個類型template <typename T, typename... Types>class StrongerType<T, Types...>{public: // T和typename StrongerType<Types...>::Type之間的“更強(qiáng)大類型”就是“最強(qiáng)大類型” typedef typename StrongerType<T, typename StrongerType<Types...>::Type>::Type Type;};int main(){ cout << typeid(StrongerType<char, int>::Type).name() << endl; // int cout << typeid(StrongerType<int, double>::Type).name() << endl; // double cout << typeid(StrongerType<char, int, double>::Type).name() << endl; // double}
通過遞歸,我們使得所有的類型共同參與了“打擂臺”,這里的“擂臺”,就是我們已經(jīng)實(shí)現(xiàn)了的StrongerType的雙類型版本,而“打擂臺的最后大贏家”,則正是我們所尋求的“最強(qiáng)大類型”。
有了StrongerType這一Traits后,我們就可以實(shí)現(xiàn)上文中的雙類型版本的Plus函數(shù)了。請看以下示例:
// Plus函數(shù)的返回值應(yīng)該是T1與T2之間的“更強(qiáng)大類型”template <typename T1, typename T2>typename StrongerType<T1, T2>::Type Plus(T1 lhs, T2 rhs){ return lhs + rhs;}int main(){ Plus(1, 2.); // 完美!}
至此,我們“終于”實(shí)現(xiàn)了一個最完美的Plus函數(shù)。
本章所實(shí)現(xiàn)的三個小工具,都是STL的type_traits庫的一部分。值得一提的是我們最后實(shí)現(xiàn)的獲取“最強(qiáng)大類型”的工具:這一工具所解決的問題,實(shí)際上是一個非常經(jīng)典的問題,其多次出現(xiàn)在多部著作中。由于decltype(以及可變參數(shù)模板)是C++11的產(chǎn)物,故很多較老的書籍對此問題給出了“無解”的結(jié)論,或只能給出一些較為牽強(qiáng)的解決方案。
值也能成為模板參數(shù)的一部分,而模板參數(shù)是編譯期常量,這二者的結(jié)合使得通過模板進(jìn)行(較復(fù)雜的)編譯期計(jì)算成為了可能。由于編譯器本就不是“計(jì)算器”,故標(biāo)題中使用了“壓榨”一詞,以表達(dá)此技術(shù)的“高昂的編譯期代價”以及“較大的局限性”的特點(diǎn);同時,合理的利用編譯期計(jì)算技術(shù),能夠極大地提高程序的效率,故“壓榨”也有“壓榨性能”之意。
本章中,我們以一小一大兩個示例,來討論編譯期計(jì)算這一巧妙技術(shù)的應(yīng)用。
編譯期計(jì)算階乘是編譯期計(jì)算技術(shù)的經(jīng)典案例,許多書籍對此均有討論(往往作為“模板元編程”一章的首個案例)。那么首先,讓我們來看看一個普通的階乘函數(shù)的實(shí)現(xiàn):
int Factorial(int N){ return N == 1 ? 1 : N * Factorial(N - 1);}
這個實(shí)現(xiàn)很簡單,這里就不對其進(jìn)行詳細(xì)討論了。下面,我們來看看如何將這個函數(shù)“翻譯”為一個編譯期就進(jìn)行計(jì)算并得到結(jié)果的“函數(shù)”。請看以下示例:
// 遞歸起點(diǎn)template <int N>struct Factorial{ static constexpr int Value = N * Factorial<N - 1>::Value;};// 遞歸終點(diǎn)template <>struct Factorial<1>{ static constexpr int Value = 1;};int main(){ cout << Factorial<4>::Value; // 編譯期就能獲得結(jié)果}
觀察上述代碼,不難總結(jié)出我們的“翻譯”規(guī)則:
上述四點(diǎn)“翻譯”規(guī)則幾乎就是編譯期計(jì)算的全部技巧了!接下來,就讓我們以一個更復(fù)雜的例子來繼續(xù)討論這一技術(shù)的精彩之處:編譯期分?jǐn)?shù)的實(shí)現(xiàn)。
分?jǐn)?shù),由分子和分母組成。有了上一節(jié)的鋪墊,我們不難發(fā)現(xiàn):分?jǐn)?shù)正是一個可以使用編譯期計(jì)算技術(shù)的極佳場合。所以首先,我們需要實(shí)現(xiàn)一個編譯期分?jǐn)?shù)類。編譯期分?jǐn)?shù)類的實(shí)現(xiàn)非常簡單,我們只需要通過一個“構(gòu)造函數(shù)”將模板參數(shù)保留下來,作為靜態(tài)數(shù)據(jù)成員即可。請看以下示例:
template <long long __Numerator, long long __Denominator>struct Fraction{ // “構(gòu)造函數(shù)” static constexpr long long Numerator = __Numerator; static constexpr long long Denominator = __Denominator; // 將編譯期分?jǐn)?shù)轉(zhuǎn)為編譯期浮點(diǎn)數(shù) template <typename T = double> static constexpr T Eval() { return static_cast<T>(Numerator) / static_cast<T>(Denominator); }};int main(){ // 1/2 typedef Fraction<1, 2> OneTwo; // 0.5 cout << OneTwo::Eval<>();}
由使用示例可見:編譯期分?jǐn)?shù)的“實(shí)例化”只需要一個typedef即可;并且,我們也能通過一個編譯期分?jǐn)?shù)得到一個編譯期浮點(diǎn)數(shù)。
讓我們繼續(xù)討論下一個問題:如何實(shí)現(xiàn)約分和通分?
顯然,約分和通分需要“求得兩個數(shù)的最大公約數(shù)和最小公倍數(shù)”的算法。所以,我們首先來看看這兩個算法的“普通”實(shí)現(xiàn):
// 求得兩個數(shù)的最大公約數(shù)long long GreatestCommonDivisor(long long lhs, long long rhs){ return rhs == 0 ? lhs : GreatestCommonDivisor(rhs, lhs % rhs);}// 求得兩個數(shù)的最小公倍數(shù)long long LeastCommonMultiple(long long lhs, long long rhs){ return lhs * rhs / GreatestCommonDivisor(lhs, rhs);}
根據(jù)上一節(jié)的“翻譯規(guī)則”,我們不難翻譯出以下代碼:
// 對應(yīng)于“return rhs == 0 ? ... : GreatestCommonDivisor(rhs, lhs % rhs)”部分template <long long LHS, long long RHS>struct __GreatestCommonDivisor{ static constexpr long long __Value = __GreatestCommonDivisor<RHS, LHS % RHS>::__Value;};// 對應(yīng)于“return rhs == 0 ? lhs : ...”部分template <long long LHS>struct __GreatestCommonDivisor<LHS, 0>{ static constexpr long long __Value = LHS;};// 對應(yīng)于“return lhs * rhs / GreatestCommonDivisor(lhs, rhs)”部分template <long long LHS, long long RHS>struct __LeastCommonMultiple{ static constexpr long long __Value = LHS * RHS / __GreatestCommonDivisor<LHS, RHS>::__Value;};
有了上面的這兩個工具,我們就能夠?qū)崿F(xiàn)出通分和約分了。首先,我們可以改進(jìn)一開始的Fraction類,在“構(gòu)造函數(shù)”中加入“自動約分”功能。請看以下示例:
template <long long __Numerator, long long __Denominator>struct Fraction{ // 具有“自動約分”功能的“構(gòu)造函數(shù)” static constexpr long long Numerator = __Numerator / __GreatestCommonDivisor<__Numerator, __Denominator>::__Value; static constexpr long long Denominator = __Denominator / __GreatestCommonDivisor<__Numerator, __Denominator>::__Value;};int main(){ // 2/4 => 1/2 typedef Fraction<2, 4> OneTwo;}
可以看出,我們只需在“構(gòu)造函數(shù)”中添加對分子、分母同時除以其最大公約數(shù)的運(yùn)算,就能夠?qū)崿F(xiàn)“自動約分”了。
接下來,我們來實(shí)現(xiàn)分?jǐn)?shù)的四則運(yùn)算功能。顯然,分?jǐn)?shù)的四則運(yùn)算的結(jié)果還是一個分?jǐn)?shù),故我們只需要通過using,將“四則運(yùn)算模板”與“等價的結(jié)果分?jǐn)?shù)模板”連接起來即可實(shí)現(xiàn)。請看以下示例:
// FractionAdd其實(shí)就是一個特殊的編譯期分?jǐn)?shù)模板template <typename LHS, typename RHS>using FractionAdd = Fraction< // 將通分后的分子相加 LHS::Numerator * __CommonPoints<LHS::Denominator, RHS::Denominator>::__LValue + RHS::Numerator * __CommonPoints<LHS::Denominator, RHS::Denominator>::__RValue, // 通分后的分母 __LeastCommonMultiple<LHS::Denominator, RHS::Denominator>::__Value // 自動約分>;// FractionMinus其實(shí)也是一個特殊的編譯期分?jǐn)?shù)模板template <typename LHS, typename RHS>using FractionMinus = Fraction< // 將通分后的分子相減 LHS::Numerator * __CommonPoints<LHS::Denominator, RHS::Denominator>::__LValue - RHS::Numerator * __CommonPoints<LHS::Denominator, RHS::Denominator>::__RValue, // 通分后的分母 __LeastCommonMultiple<LHS::Denominator, RHS::Denominator>::__Value // 自動約分>;// FractionMultiply其實(shí)也是一個特殊的編譯期分?jǐn)?shù)模板template <typename LHS, typename RHS>using FractionMultiply = Fraction< // 分子與分子相乘 LHS::Numerator * RHS::Numerator, // 分母與分母相乘 LHS::Denominator * RHS::Denominator // 自動約分>;// FractionDivide其實(shí)也是一個特殊的編譯期分?jǐn)?shù)模板template <typename LHS, typename RHS>using FractionDivide = Fraction< // 分子與分母相乘 LHS::Numerator * RHS::Denominator, // 分母與分子相乘 LHS::Denominator * RHS::Numerator // 自動約分>;int main(){ // 1/2 typedef Fraction<1, 2> OneTwo; // 2/3 typedef Fraction<2, 3> TwoThree; // 2/3 + 1/2 => 7/6 typedef FractionAdd<TwoThree, OneTwo> TwoThreeAddOneTwo; // 2/3 - 1/2 => 1/6 typedef FractionMinus<TwoThree, OneTwo> TwoThreeMinusOneTwo; // 2/3 * 1/2 => 1/3 typedef FractionMultiply<TwoThree, OneTwo> TwoThreeMultiplyOneTwo; // 2/3 / 1/2 => 4/3 typedef FractionDivide<TwoThree, OneTwo> TwoThreeDivideOneTwo;}
由此可見,所謂的四則運(yùn)算,實(shí)際上就是一個針對Fraction的using(模板不能使用typedef,只能使用using)罷了。
最后,我們實(shí)現(xiàn)分?jǐn)?shù)的比大小功能。這非常簡單:只需要先對分母通分,再對分子進(jìn)行比大小即可。而比大小的結(jié)果,就是“比大小模板”的一個數(shù)據(jù)成員。請看以下示例:
// 這六個模板都進(jìn)行“先通分,再比較”運(yùn)算,唯一的區(qū)別就在于比較操作符的不同// “operator==”template <typename LHS, typename RHS>struct FractionEqual{ static constexpr bool Value = LHS::Numerator * __CommonPoints<LHS::Denominator, RHS::Denominator>::__LValue == RHS::Numerator * __CommonPoints<LHS::Denominator, RHS::Denominator>::__RValue;};// “operator!=”template <typename LHS, typename RHS>struct FractionNotEqual{ static constexpr bool Value = LHS::Numerator * __CommonPoints<LHS::Denominator, RHS::Denominator>::__LValue != RHS::Numerator * __CommonPoints<LHS::Denominator, RHS::Denominator>::__RValue;};// “operator<”template <typename LHS, typename RHS>struct FractionLess{ static constexpr bool Value = LHS::Numerator * __CommonPoints<LHS::Denominator, RHS::Denominator>::__LValue < RHS::Numerator * __CommonPoints<LHS::Denominator, RHS::Denominator>::__RValue;};// “operator<=”template <typename LHS, typename RHS>struct FractionLessEqual{ static constexpr bool Value = LHS::Numerator * __CommonPoints<LHS::Denominator, RHS::Denominator>::__LValue <= RHS::Numerator * __CommonPoints<LHS::Denominator, RHS::Denominator>::__RValue;};// “operator>”template <typename LHS, typename RHS>struct FractionGreater{ static constexpr bool Value = LHS::Numerator * __CommonPoints<LHS::Denominator, RHS::Denominator>::__LValue > RHS::Numerator * __CommonPoints<LHS::Denominator, RHS::Denominator>::__RValue;};// “operato>=”template <typename LHS, typename RHS>struct FractionGreaterEqual{ static constexpr bool Value = LHS::Numerator * __CommonPoints<LHS::Denominator, RHS::Denominator>::__LValue >= RHS::Numerator * __CommonPoints<LHS::Denominator, RHS::Denominator>::__RValue;};int main(){ // 1/2 typedef Fraction<1, 2> OneTwo; // 2/3 typedef Fraction<2, 3> TwoThree; // 1/2 == 2/3 => false cout << FractionEqual<OneTwo, TwoThree>::Value << endl; // 1/2 != 2/3 => true cout << FractionNotEqual<OneTwo, TwoThree>::Value << endl; // 1/2 < 2/3 => true cout << FractionLess<OneTwo, TwoThree>::Value << endl; // 1/2 <= 2/3 => true cout << FractionLessEqual<OneTwo, TwoThree>::Value << endl; // 1/2 > 2/3 => false cout << FractionGreater<OneTwo, TwoThree>::Value << endl; // 1/2 >= 2/3 => false cout << FractionGreaterEqual<OneTwo, TwoThree>::Value << endl;}
至此,編譯期分?jǐn)?shù)的全部功能就都實(shí)現(xiàn)完畢了。不難發(fā)現(xiàn),在編譯期分?jǐn)?shù)的使用過程中,我們?nèi)淌褂玫亩际莟ypedef,并沒有真正的構(gòu)造任何一個分?jǐn)?shù),一切計(jì)算都已經(jīng)在編譯期完成了。
讀完本章,也許你會恍然大悟:“哦!原來模板也能夠表達(dá)形參、if、while、return等語義!”,進(jìn)而,也許你會有疑問:“那既然這樣,豈不是所有的計(jì)算函數(shù)都能換成編譯期計(jì)算了?”。
很可惜,答案是否定的。
我們通過對編譯期計(jì)算這一技術(shù)的優(yōu)缺點(diǎn)進(jìn)行總結(jié),從而回答這個問題。編譯期計(jì)算的目的,是為了完全消除運(yùn)行時代價,從而在高性能計(jì)算場合極大的提高效率;但此技術(shù)的缺點(diǎn)也是很多且很明顯的:首先,僅僅為了進(jìn)行一次編譯期計(jì)算,就有可能進(jìn)行很多次的模板實(shí)例化(比如,為了計(jì)算10的階乘,就要實(shí)例化出10個Factorial類),這是一種極大的潛在的編譯期代價;其次,并不是任何類型的值都能作為模板參數(shù),如浮點(diǎn)數(shù)(雖然我們可以使用編譯期分?jǐn)?shù)間接的規(guī)避這一限制)、以及任何的類類型值等均不可以,這就使得編譯期計(jì)算的應(yīng)用幾乎被限定在只需要使用整型和布爾類型的場合中;最后,“遞歸實(shí)例化”在所有的編譯器中都是有最大深度限制的(不過幸運(yùn)的是,在現(xiàn)代編譯器中,允許的最大深度其實(shí)是比較大的)。但即使如此,由于編譯期計(jì)算技術(shù)使得我們可以進(jìn)行“搶跑”,在程序還未開始運(yùn)行時,就計(jì)算出各種復(fù)雜的結(jié)果,從而極大的提升程序的效率,故此技術(shù)當(dāng)然也是瑕不掩瑜的。
注:本文中的部分程序已完整實(shí)現(xiàn)于本文作者的Github上,列舉如下:
本篇完,敬請期待下篇