C++ 模板沉思錄(上)
??“Python貓” ,一個(gè)值得加星標(biāo)的公眾號(hào)

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

??掃碼贊賞她

櫻雨樓原創(chuàng)文章索引:
對(duì)比 C++ 和 Python,談?wù)勚羔樑c引用
