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

面試題】即可獲取