0 論抽象——前言
故事要從一個看起來非常簡單的功能開始:
請計算兩個數的和。
如果你對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函式介面強行的要求兩個實參以及返回值的型別都必須是int,或是能夠發生隱式型別轉換到int的型別。此時,如果實參並不是int型別,其結果往往就是錯誤的。請看以下示例:
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;
}
// ...
這種方案的缺點也很明顯:其使得程式碼寫起來像“組合語言”(movl,movq,...)。我們需要針對不同的型別呼叫不同名稱的函式(是的,C語言也不支援函式過載),這太可怕了。
- 使用巨集
#define Plus(lhs, rhs) (lhs + rhs)
這種方案似乎很不錯,甚至“程式碼看上去和Python一樣”。但正如許許多多的書籍都討論過的那樣,巨集,不僅“拋棄”了型別,甚至“拋棄”了程式碼。是的,巨集不是C語言程式碼,其只是交付於前處理器執行的“複製貼上”的標記。一旦預處理完成,巨集已然不再存在。可想而知,在功能變得複雜後,巨集的缺點將會越來越大:程式碼晦澀,無法除錯,“莫名其妙”的報錯...
看到這裡,也許你會覺得:“哇!C語言真爛!居然連這麼簡單的功能都無法實現!”。但請想一想,為什麼會出現這些問題呢?讓我們回到故事的起點:
請計算兩個數的和。
仔細分析這句話:“請計算...的和”,意味著“加法”語義,這在C語言中可以通過“+”實現(也許你會聯想到組合語言中的加法實現);而“兩個”,則意味著形參的數量是2(也許你會聯想到組合語言中的ESS、ESP、EBP等暫存器);那麼,“數”,意味著什麼語義?C語言中,具有“數”這一語義的型別有十幾種:int、double、unsigned,等等,甚至char也具有“數”的語義。那麼,“加法”和“+”,“兩個”和“形參的數量是2”,以及“數”和int、double、unsigned等等之間的關係是什麼?
是抽象。
高階語言的目的,就是對比其更加低階的語言進行抽象,從而使得我們能夠實現更加高階的功能。抽象,是一種人類的高階思維活動,是一種充滿著智慧的思維活動。組合語言抽象了機器語言,而C語言則進一步抽象了組合語言:其將組合語言中的各種加法指令,抽象成了一個簡單的加號;將各種暫存器操作,抽象成了形參和實參...抽象思維是如此的普遍與自然,以至於我們往往甚至忽略了這種思維的存在。
但是,C語言並沒有針對型別進行抽象的能力,C語言不知道,也沒有能力表達“int和double都是數字”這一語義。而這,直接導致了這個“看起來非常簡單的功能”難以完美的實現。
針對型別的抽象是如此重要,以至於程式語言世界出現了與C語言這樣的“靜態型別語言”完全不一樣的“動態型別語言”。正如開頭所示,在Python這樣的動態型別語言中,我們根本就不需要為每個變數提供型別,從而似乎“從根本上解決了問題”。但是,“出來混,遲早要還的”,這種看似完美的動態型別語言,犧牲的卻是極大的執行時效率!我們不禁陷入了沉思:真的沒有既不損失效率,又能對型別進行抽象的方案了嗎?
正當我們一籌莫展,甚至感到些許絕望之時,C++的模板,為我們照亮了前行的道路。
1 新手村——模板基礎
1.1 函式模板與類别範本
模板,即C++中用以實現泛型程式設計思想的語法組分。模板是什麼?一言以蔽之:型別也可以是“變數”的東西。這樣的“東西”,在C++中有二:函式模板和類别範本。
通過在普通的函式定義和類定義中前置template <...>,即可定義一個模板,讓我們以上文中的Plus函式進行說明。請看以下示例:
此為函式模板:
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,同樣正確!
}
顯然,模板的出現,使得我們輕而易舉的就實現了型別抽象,並且沒有(像動態型別語言那樣)引入任何因為此種抽象帶來的額外代價。
1.2 模板形參、模板實參與預設值
請看以下示例:
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,則稱為模板實參。在這裡,模板實參是一個型別。
事實上,模板的形參與實參既可以是型別,也可以是值,甚至可以是“模板的模板”;並且,模板形參也可以具有預設值(就和函式形參一樣)。請看以下示例:
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> _;
}
上例中,我們宣告瞭三個模板引數:
- typename T:一個普通的型別引數
- int N:一個整型引數
- template <typename U, typename = allocator<U>> class Container = vector:一個“模板的模板引數”
什麼叫“模板的模板引數”?這裡需要明確的是:模板、型別和值,是三個完全不一樣的語法組分。模板能夠“創造”型別,而型別能夠“創造”值。請參考以下示例以進行辨析:
vector<int> v;
此例中,vector是一個模板,vector<int>是一個型別,而v是一個值。
所以,一個“模板的模板引數”,就是一個需要提供給其一個模板作為實參的引數。對於上文中的宣告,Container是一個“模板的模板引數”,其需要接受一個模板作為實參 。需要怎樣的模板呢?這個模板應具有兩個模板形參,且第二形參具有預設值allocator<U>;同時,Container具有預設值vector,這正是一個符合要求的模板。這樣,Container在類定義中,便可被當作一個模板使用(就像vector那樣)。
1.3 特化與偏特化
模板,代表了一種泛化的語義。顯然,既然有泛化語義,就應當有特化語義。特化,使得我們能為某些特定的型別專門提供一份特殊實現,以達到某些目的。
特化分為全特化與偏特化。所謂全特化,即一個“披著空空如也的template <>的普通函式或類”,我們還是以上文中的Plus函式為例:
// 不管T是什麼型別,都將使用此定義...
template <typename T>
T Plus(T lhs, T rhs)
{
return lhs + rhs;
}
// ...但是,當T為int時,將使用此定義
template <> // 空空如也的template <>
int Plus(int lhs, int rhs)
{
return lhs + rhs;
}
int main()
{
Plus(1., 2.); // 使用泛型版本
Plus(1, 2); // 使用特化版本
}
那麼,偏特化又是什麼呢?除了全特化以外的特化,都稱為偏特化。這句話雖然簡短,但意味深長,讓我們來仔細分析一下:首先,“除了全特化以外的...”,代表了template關鍵詞之後的“<>”不能為空,否則就是全特化,這顯而易見;其次,“...的特化”,代表了偏特化也必須是一個特化。什麼叫“是一個特化”呢?只要特化版本比泛型版本更特殊,那麼此版本就是一個特化版本。請看以下示例:
// 泛化版本
template <typename T, typename U>
struct _ {};
// 這個版本的特殊之處在於:僅當兩個型別一樣的時候,才會且一定會使用此版本
template <typename T>
struct _<T, T> {};
// 這個版本的特殊之處在於:僅當兩個型別都是指標的時候,才會且一定會使用此版本
template <typename T, typename U>
struct _<T *, U *> {};
// 這個版本“換湯不換藥”,沒有任何特別之處,所以不是一個特化,而是錯誤的重複定義
template <typename A, typename B>
struct _<A, B> {};
由此可見,“更特殊”是一個十分寬泛的語義,這賦予了模板極大的表意能力,我們將在下面的章節中不斷的見到特化所帶來的各種技巧。
1.4 惰性例項化
函式模板不是函式,而是一個可以生成函式的語法組分;同理,類别範本也不是類,而是一個可以生成類的語法組分。我們稱通過函式模板生成函式,或通過類别範本生成類的過程為模板例項化。
模板例項化具有一個非常重要的特徵:惰性。這種惰性主要體現在類别範本上。請看以下示例:
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函式顯然是不適用於string型別的。也就是說,Test類對於string型別而言,並不是“100%完美的”。當遇到這種情況時,C++的做法十分寬鬆:不完美?不要緊,只要不呼叫那些“不完美的函式”就行了。在編譯器層面,編譯器只會例項化真的被使用的函式,並對其進行語法檢查,而根本不會在意那些根本沒有被用到的函式。也就是說,在上例中,編譯器實際上只例項化出了兩個函式:string版本的Plus,以及int版本的Minus。
在這裡,“懶惰即美德”佔了上風。
1.5 依賴型名稱
在C++中,“::”表達“取得”語義。顯然,“::”既可以取得一個值,也可以取得一個型別。這在非模板場景下是沒有任何問題的,並不會引起接下來即將將要討論的“取得的是一個型別還是一個值”的語義混淆,因為編譯器知道“::”左邊的語法組分的定義。但在模板中,如果“::”左邊的語法組分並不是一個確切型別,而是一個模板引數的話,語義將不再是確定的。請看以下示例:
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是一個數。我們稱這種含有模板引數的,無法立即確定語義的名稱為“依賴型名稱”。所謂“依賴”,意即此名稱的確切語義依賴於模板引數的實際型別。
對於依賴型名稱,C++規定:預設情況下,編譯器應認為依賴型名稱不是一個型別;如果需要編譯器將依賴型名稱視為一個型別,則需要前置typename關鍵詞。請看以下示例以進行辨析:
T::TypeOrValue * N; // T::TypeOrValue是一個值,這是一個乘法表示式
typename T::TypeOrValue * N; // typename T::TypeOrValue是一個型別,宣告瞭一個這樣型別的指標
1.6 可變引數模板
可變引數模板是C++11引入的一個極為重要的語法。這裡對其進行簡要介紹。
可變引數模板表達了“引數數量,以及每個引數的型別都未知且各不相同”這一語義。如果我們希望實現一個簡單的print函式,其能夠傳入任意數量,且型別互不相同的引數,並依次列印這些引數值,此時就需要使用可變引數模板。
可變引數模板的語法由以下組分構成:
- typename...:宣告一個可變引數模板形參
- sizeof...:獲取引數包內引數的數量
- Pattern...:以某一模式展開引數包
接下來,我們就基於可變引數模板,實現這一print函式。請看以下示例:
// 遞迴終點
void print() {}
// 分解出一個val + 剩下的所有val
// 相當於: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;
// 相當於:print(Args1, Args2, Args3, ...);
// 遞迴地繼續分解...
print(Args...);
}
int main()
{
print(1, 2., '3', "4");
}
上例中,我們實現了一對過載的print函式。第一個print函式是一個空函式,其將在“Args...”是空的時候被呼叫,以作為遞迴終點;而第二個print函式接受一個val以及餘下的所有val作為引數,其將列印val,並使用餘下的所有val繼續遞迴呼叫自己。不難發現,第二版本的print函式具有不斷列印並分解Args的能力,直到Args被完全分解。
2 平淡無奇卻暗藏玄機的語法——sizeof與SFINAE
2.1 sizeof
“sizeof?這有什麼可討論的?”也許你會想。只要你學過C語言,那麼對此必不陌生。那麼為什麼我們還需要為sizeof這一“平淡無奇”的語法單獨安排一節來討論呢?這是因為sizeof有兩個對於泛型程式設計而言極為重要的特性:
- sizeof的求值結果是編譯期常量(從而可以作為模板實參使用)
- 在任何情況下,sizeof都不會引發對其引數的求值或類似行為(如函式呼叫,甚至函式定義!等),因為並不需要
上述第一點很好理解,因為sizeof所考察的是型別,而型別(當然也包含其所佔用的記憶體大小),一定是一個編譯期就知道的量(因為C++作為一門靜態型別語言,任何的型別都絕不會延遲到執行時才知道,這是動態型別語言才具有的特性),故sizeof的結果是一個編譯期常量也就不足為奇了。
上述第二點意味深長。利用此特性,我們可以實現出一些非常特殊的功能。請看下一節。
2.2 稻草人函式
讓我們以一個問題引出這一節的內容:
如何實現:判定型別A是否能夠基於隱式型別轉換轉為B型別?
乍看之下,這是個十分棘手的問題。此時我們應當思考的是:如何引導(請注意“引導”一詞的含義)編譯器,在A到B的隱式型別轉換可行時,走第一條路,否則,走第二條路?
請看以下示例:
template <typename A, typename B>
class IsCastable
{
private:
// 定義兩個記憶體大小不一樣的型別,作為“布林值”
typedef char __True;
typedef struct { char _[2]; } __False;
// 稻草人函式
static A __A();
// 只要A到B的隱式型別轉換可用,過載確定的結果就是此函式...
static __True __Test(B);
// ...否則,過載確定的結果才是此函式(“...”引數的過載確定優先順序低於其他一切可行的過載版本)
static __False __Test(...);
public:
// 根據過載確定的結果,就能夠判定出隱式型別轉換是否能夠發生
static constexpr bool Value = sizeof(__Test(__A())) == sizeof(__True);
};
上例比較複雜,我們依次進行討論。
首先,我們宣告瞭兩個大小不同的型別,作為假想的“布林值”。也許你會有疑問,這裡為什麼不使用int或double之類的型別作為False?這是由於C語言並未規定“int、double必須比char大”,故為了“強行滿足標準”(你完全可以認為這是某種“教條主義或形式主義”),這裡採用了“兩個char一定比一個char大一倍”這一簡單道理,定義了False。
然後,我們宣告瞭一個所謂的“稻草人函式”,這個看似毫無意義的函式甚至沒有函式體(因為並不需要,且接下來的兩個函式也沒有函式體,與此函式同理)。這個函式唯一的目的就是“獲得”一個A型別的值“給sizeof看”。由於sizeof的不求值特性,此函式也就不需要(我們也無法提供)函式體了。那麼,為什麼不直接使用形如“T()”這樣的寫法,而需要宣告一個“稻草人函式”呢?我想,不用我說你就已經明白原因了:這是因為並不是所有的T都具有預設建構函式,而如果T沒有預設建構函式,那麼“T()”就是錯誤的。
接下來是最關鍵的部分,我們宣告瞭一對過載函式,這兩個函式的區別有二:
- 返回值不同,一個是sizeof的結果為1的值,而另一個是sizeof的結果為2的值
- 形參不同,一個是B,一個是“...”
也就是說,如果我們給這一對過載函式傳入一個A型別的值時,由於“...”引數的過載確定優先順序低於其他一切可行的過載版本,只要A到B的隱式型別轉換能夠發生,過載確定的結果就一定是呼叫第一個版本的函式,返回值為__True;否則,只有當A到B的隱式型別轉換真的不可行時,編譯器才會“被迫”選擇那個編譯器“最不喜歡的版本”,從而使得返回值為__False。返回值的不同,就能夠直接體現在sizeof的結果不同上。所以,只需要判定sizeof(__Test(__A()))是多少,就能夠達到我們最終的目的了。下面請看使用示例:
int main()
{
cout << IsCastable<int, double>::Value << endl; // true
cout << IsCastable<int, string>::Value << endl; // false
}
可以看出,輸出結果完全符合我們的預期。
2.3 SFINAE
SFINAE(Substitution Failure Is Not An Error,替換失敗並非錯誤)是一個高階模板技巧。首先,讓我們來分析這一拗口的詞語:“替換失敗並非錯誤”。
什麼是“替換”?這裡的替換,實際上指的正是模板例項化;也就是說,當模板例項化失敗時,編譯器並不認為這是一個錯誤。這句話看上去似乎莫名其妙,也許你會有疑問:那怎麼樣才認為是一個錯誤?我們又為什麼要討論一個“錯誤的東西”呢?讓我們以一個問題引出這一技巧的意義:
如何判定一個型別是否是一個類型別?
“哇!這個問題似乎比上一個問題更難啊!”也許你會這麼想。不過有了上一個問題的鋪墊,這裡我們依然要思考的是:一個類型別,有什麼獨一無二的東西是非類型別所沒有的?(這樣我們似乎就能讓編譯器在“喜歡和不喜歡”之間做出抉擇)
也許你將恍然大悟:類的成員指標。
請看以下示例:
template <typename T>
class IsClass
{
private:
// 定義兩個記憶體大小不一樣的型別,作為“布林值”
typedef char __True;
typedef struct { char _[2]; } __False;
// 僅當T是一個類型別時,“int T::*”才是存在的,從而這個泛型函式的例項化才是可行的
// 否則,就將觸發SFINAE
template <typename U>
static __True __Test(int U::*);
// 僅當觸發SFINAE時,編譯器才會“被迫”選擇這個版本
template <typename U>
static __False __Test(...);
public:
// 根據過載確定的結果,就能夠判定出T是否為類型別
static constexpr bool Value = sizeof(__Test<T>(0)) == sizeof(__True);
};
同樣,我們首先定義了兩個記憶體大小一定不一樣的型別,作為假想的“布林值”。然後,我們宣告瞭兩個過載模板,其分別以兩個“布林值”作為返回值。這裡的關鍵在於,過載模板的引數,一個是類成員指標,另一個是“...”。顯然,當編譯器拿到一個T,並準備生成一個“T:?”時,僅當T是一個類型別時,這一生成才是正確的,合乎語法的;否則,這個函式簽名將根本無法被生成出來,從而進一步的使得編譯器“被迫”選擇那個“最不喜歡的版本”進行呼叫(而不是認為這個“根本無法被生成出來”的模板是一個錯誤)。所以,通過sizeof對__Test的返回值大小進行判定,就能夠達到我們最終的目的了。下面請看使用示例:
int main()
{
cout << IsClass<double>::Value << endl; // false
cout << IsClass<string>::Value << endl; // true
}
可以看出,輸出結果完全符合我們的預期。
2.4 本章後記
sizeof,作為一個C語言的“入門級”語法,其“永不求值”的特性往往被我們所忽略。本章中,我們充分利用了sizeof的這種“永不求值”的特性,做了很多“表面工程”,僅僅是為了“給sizeof看”;同理,SFINAE技術似乎也只是在“找編譯器的麻煩,拿編譯器尋開心”。但正是這些“表面工程、找麻煩、尋開心”,讓我們得以實現了一些非常不可思議的功能。
3 型別萃取器——Type Traits
Traits,中文翻譯為“特性”,Type Traits,即為“型別的特性”。這是個十分奇怪的翻譯,故很多書籍對這個詞選擇不譯,也有書籍將其翻譯為“型別萃取器”,十分生動形象。
Type Traits的定義較為模糊,其大致代表了這樣的一系列技術:通過一個型別T,取得另一個基於T進行加工後的型別,或對T基於某一標準進行分類,得到分類結果。
本章中,我們以幾個經典的Type Traits應用,來見識一番此技術的精妙。
3.1 為T“新增星號”
第一個例子較為簡單:我們需要得到T的指標型別,即:得到“T *”。此時,只需要將“T *”通過typedef變為Type Traits類的結果即可。請看以下示例:
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 *
}
這段程式碼十分簡單,但似乎我們寫了兩遍“一模一樣”的程式碼?認真觀察和思考即可發現:特化版本是為了防止一個已經是指標的型別發生“升級”而存在的。如果T已經是一個指標型別,則Type就是T本身,否則,Type才是“T *”。
3.2 為T“去除星號”
上一節,我們實現了一個能夠為T“新增星號”的Traits,這一節,我們將實現一個功能與之相反的Traits:為T“去除星號”。
“簡單!”也許你會想,並很快給出了以下實現:
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
}
似乎完成了?不幸的是,這一實現並不完美。請看以下示例:
int main()
{
cout << typeid(RemoveStar<int **>::Type).name() << endl; // int *,哦不!
}
可以看到,我們的上述實現只能去除一個星號,當傳入一個多級指標時,並不能得到我們想要的結果。
這該如何是好?我們不禁想到:如果能夠實現一個“while迴圈”,就能去除所有的星號了。雖然模板沒有while迴圈,但我們知道:遞迴正是迴圈的等價形式。請看以下示例:
// 遞迴終點,此時T真的不是指標了
template <typename T>
struct RemoveStar { typedef T Type; };
// 當T是指標時,Type應該是T本身(已經去除了一個星號)繼續RemoveStar的結果
template <typename T>
struct RemoveStar<T *> { typedef typename RemoveStar<T>::Type Type; };
上述實現中,當發現T選擇了特化版本(即T本身是指標時),就會遞迴地對T進行去星號,直到T不再選擇特化版本,從而抵達遞迴終點為止。這樣,就能在面對多級指標時,也能夠得到正確的Type。下面請看使用示例:
int main()
{
cout << typeid(RemoveStar<int **********>::Type).name() << endl; // int
}
可以看出,輸出結果完全符合我們的預期。
顯然,使用這樣的Traits是具有潛在的較大代價的。例如上例中,為了去除一個十級指標的星號,編譯器竟然需要例項化出11個類!但好在這一切均發生在編譯期,對執行效率不會產生任何影響。
3.3 尋找“最強大型別”
讓我們繼續討論前言中的Plus函式,以引出本節所要討論的話題。目前我們給出的“最好實現”如下:
template <typename T>
T Plus(T lhs, T rhs)
{
return lhs + rhs;
}
int main()
{
cout << Plus(1, 2) << endl; // 3,正確!
}
但是,只要在上述程式碼中新增一個“.”,就立即發生了問題:
int main()
{
cout << Plus(1, 2.) << endl; // 二義性錯誤!T應該是int還是double?
}
上例中,由於Plus模板只使用了單一的一個模板引數,故要求兩個實參的型別必須一致,否則,編譯器就不知道T應該是什麼型別,從而引發二義性錯誤。但顯然,任何的兩種“數”之間都應該是可以做加法的,所以不難想到,我們應該使用兩個而不是一個模板引數,分別作為lhs與rhs的型別,但是,我們立即就遇到了新的問題。請看以下示例:
template <typename T1, typename T2>
/* 這裡應該寫什麼?*/ Plus(T1 lhs, T2 rhs)
{
return lhs + rhs;
}
應該寫T1?還是T2?顯然都不對。我們應該尋求一種方法,其能夠獲取到T1與T2之間的“更強大型別”,並將此“更強大型別”作為返回值。進一步的,我們可以以此為基礎,實現出一個能夠獲取到任意數量的型別之中的“最強大型別”的方法。
應該怎麼做呢?事實上,這個問題的解決方案,確實是難以想到的。請看以下示例:
template <typename A, typename B>
class StrongerType
{
private:
// 稻草人函式
static A __A();
static B __B();
public:
// 3目運算子表示式的型別就是“更強大型別”
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
}
上例中,我們首先定義了兩個“稻草人函式”,用以分別“獲取”型別為A或B的值“給decltype看”。然後,我們使用了decltype探測三目運算子表示式的型別,不難發現,decltype也具有sizeof的“不對錶達式進行求值”的特性。由於三目運算子表示式從理論上可能返回兩個值中的任意一個,故表示式的型別就是我們所尋求的“更強大型別”。隨後的用例也證實了這一點。
有了獲取兩個型別之間的“更強大型別”的Traits以後,我們不難想到:N個型別之中的“最強大型別”,就是N - 1個型別之中的“最強大型別”與第N個型別之間的“更強大型別”。請看以下示例:
// 原型
// 通過typename StrongerType<Types...>::Type獲取Types...中的“最強大型別”
template <typename... Types>
class StrongerType;
// 只有一個型別
template <typename T>
class StrongerType<T>
{
// 我自己就是“最強大的”
typedef T Type;
};
// 只有兩個型別
template <typename A, typename B>
class StrongerType<A, B>
{
private:
// 稻草人函式
static A __A();
static B __B();
public:
// 3目運算子表示式的型別就是“更強大型別”
typedef decltype(true ? __A() : __B()) Type;
};
// 不止兩個型別
template <typename T, typename... Types>
class StrongerType<T, Types...>
{
public:
// T和typename StrongerType<Types...>::Type之間的“更強大型別”就是“最強大型別”
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
}
通過遞迴,我們使得所有的型別共同參與了“打擂臺”,這裡的“擂臺”,就是我們已經實現了的StrongerType的雙型別版本,而“打擂臺的最後大贏家”,則正是我們所尋求的“最強大型別”。
有了StrongerType這一Traits後,我們就可以實現上文中的雙型別版本的Plus函式了。請看以下示例:
// Plus函式的返回值應該是T1與T2之間的“更強大型別”
template <typename T1, typename T2>
typename StrongerType<T1, T2>::Type Plus(T1 lhs, T2 rhs)
{
return lhs + rhs;
}
int main()
{
Plus(1, 2.); // 完美!
}
至此,我們“終於”實現了一個最完美的Plus函式。
3.4 本章後記
本章所實現的三個小工具,都是STL的type_traits庫的一部分。值得一提的是我們最後實現的獲取“最強大型別”的工具:這一工具所解決的問題,實際上是一個非常經典的問題,其多次出現在多部著作中。由於decltype(以及可變引數模板)是C++11的產物,故很多較老的書籍對此問題給出了“無解”的結論,或只能給出一些較為牽強的解決方案。
4 “壓榨”編譯器——編譯期計算
值也能成為模板引數的一部分,而模板引數是編譯期常量,這二者的結合使得通過模板進行(較複雜的)編譯期計算成為了可能。由於編譯器本就不是“計算器”,故標題中使用了“壓榨”一詞,以表達此技術的“高昂的編譯期代價”以及“較大的侷限性”的特點;同時,合理的利用編譯期計算技術,能夠極大地提高程式的效率,故“壓榨”也有“壓榨效能”之意。
本章中,我們以一小一大兩個示例,來討論編譯期計算這一巧妙技術的應用。
4.1 編譯期計算階乘
編譯期計算階乘是編譯期計算技術的經典案例,許多書籍對此均有討論(往往作為“模板超程式設計”一章的首個案例)。那麼首先,讓我們來看看一個普通的階乘函式的實現:
int Factorial(int N)
{
return N == 1 ? 1 : N * Factorial(N - 1);
}
這個實現很簡單,這裡就不對其進行詳細討論了。下面,我們來看看如何將這個函式“翻譯”為一個編譯期就進行計算並得到結果的“函式”。請看以下示例:
// 遞迴起點
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; // 編譯期就能獲得結果
}
觀察上述程式碼,不難總結出我們的“翻譯”規則:
- 形參N(執行時值)變為了模板引數N(編譯期值)
- “N == 1”這樣的“if語句”變為了模板特化
- 遞迴變為了創造一個新的模板(Factorial<N - 1>),這也意味著迴圈也可以通過此種方式實現
- “return”變為了一個static constexpr變數
上述四點“翻譯”規則幾乎就是編譯期計算的全部技巧了!接下來,就讓我們以一個更復雜的例子來繼續討論這一技術的精彩之處:編譯期分數的實現。
4.2 編譯期分數
分數,由分子和分母組成。有了上一節的鋪墊,我們不難發現:分數正是一個可以使用編譯期計算技術的極佳場合。所以首先,我們需要實現一個編譯期分數類。編譯期分數類的實現非常簡單,我們只需要通過一個“建構函式”將模板引數保留下來,作為靜態資料成員即可。請看以下示例:
template <long long __Numerator, long long __Denominator>
struct Fraction
{
// “建構函式”
static constexpr long long Numerator = __Numerator;
static constexpr long long Denominator = __Denominator;
// 將編譯期分數轉為編譯期浮點數
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<>();
}
由使用示例可見:編譯期分數的“例項化”只需要一個typedef即可;並且,我們也能通過一個編譯期分數得到一個編譯期浮點數。
讓我們繼續討論下一個問題:如何實現約分和通分?
顯然,約分和通分需要“求得兩個數的最大公約數和最小公倍數”的演算法。所以,我們首先來看看這兩個演算法的“普通”實現:
// 求得兩個數的最大公約數
long long GreatestCommonDivisor(long long lhs, long long rhs)
{
return rhs == 0 ? lhs : GreatestCommonDivisor(rhs, lhs % rhs);
}
// 求得兩個數的最小公倍數
long long LeastCommonMultiple(long long lhs, long long rhs)
{
return lhs * rhs / GreatestCommonDivisor(lhs, rhs);
}
根據上一節的“翻譯規則”,我們不難翻譯出以下程式碼:
// 對應於“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;
};
// 對應於“return rhs == 0 ? lhs : ...”部分
template <long long LHS>
struct __GreatestCommonDivisor<LHS, 0>
{
static constexpr long long __Value = LHS;
};
// 對應於“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;
};
有了上面的這兩個工具,我們就能夠實現出通分和約分了。首先,我們可以改進一開始的Fraction類,在“建構函式”中加入“自動約分”功能。請看以下示例:
template <long long __Numerator, long long __Denominator>
struct Fraction
{
// 具有“自動約分”功能的“建構函式”
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;
}
可以看出,我們只需在“建構函式”中新增對分子、分母同時除以其最大公約數的運算,就能夠實現“自動約分”了。
接下來,我們來實現分數的四則運算功能。顯然,分數的四則運算的結果還是一個分數,故我們只需要通過using,將“四則運算模板”與“等價的結果分數模板”連線起來即可實現。請看以下示例:
// FractionAdd其實就是一個特殊的編譯期分數模板
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其實也是一個特殊的編譯期分數模板
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其實也是一個特殊的編譯期分數模板
template <typename LHS, typename RHS>
using FractionMultiply = Fraction<
// 分子與分子相乘
LHS::Numerator * RHS::Numerator,
// 分母與分母相乘
LHS::Denominator * RHS::Denominator
// 自動約分
>;
// FractionDivide其實也是一個特殊的編譯期分數模板
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)罷了。
最後,我們實現分數的比大小功能。這非常簡單:只需要先對分母通分,再對分子進行比大小即可。而比大小的結果,就是“比大小模板”的一個資料成員。請看以下示例:
// 這六個模板都進行“先通分,再比較”運算,唯一的區別就在於比較操作符的不同
// “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;
}
至此,編譯期分數的全部功能就都實現完畢了。不難發現,在編譯期分數的使用過程中,我們全程使用的都是typedef,並沒有真正的構造任何一個分數,一切計算都已經在編譯期完成了。
4.3 本章後記
讀完本章,也許你會恍然大悟:“哦!原來模板也能夠表達形參、if、while、return等語義!”,進而,也許你會有疑問:“那既然這樣,豈不是所有的計算函式都能換成編譯期計算了?”。
很可惜,答案是否定的。
我們通過對編譯期計算這一技術的優缺點進行總結,從而回答這個問題。編譯期計算的目的,是為了完全消除執行時代價,從而在高效能運算場合極大的提高效率;但此技術的缺點也是很多且很明顯的:首先,僅僅為了進行一次編譯期計算,就有可能進行很多次的模板例項化(比如,為了計算10的階乘,就要例項化出10個Factorial類),這是一種極大的潛在的編譯期代價;其次,並不是任何型別的值都能作為模板引數,如浮點數(雖然我們可以使用編譯期分數間接的規避這一限制)、以及任何的類型別值等均不可以,這就使得編譯期計算的應用幾乎被限定在只需要使用整型和布林型別的場合中;最後,“遞迴例項化”在所有的編譯器中都是有最大深度限制的(不過幸運的是,在現代編譯器中,允許的最大深度其實是比較大的)。但即使如此,由於編譯期計算技術使得我們可以進行“搶跑”,在程式還未開始執行時,就計算出各種複雜的結果,從而極大的提升程式的效率,故此技術當然也是瑕不掩瑜的。
5 神奇的“多功能”函式——編譯期分派
本章旨在討論這樣的一個問題:
如何實現一個“多功能函式”,使得單一的“多功能函式”在面對不同型別的引數時,能夠自動選擇針對當前型別的最佳方案或獨特功能?
本章亦將通過一小一大兩個案例,來討論這一問題。
5.1 迭代器的advance函式
STL中的advance函式,可用於將迭代器向前(後)推進N步。顯然,不同的迭代器類別對前進這一動作的支援是不一樣的:如前向迭代器,只能前進,不能後退;雙向迭代器,可雙向移動;而隨機訪問迭代器,不僅可以雙向移動,還可以“大跨步地”移動。為了討論方便,我們首先用數字的加減模擬出這三種不同類別的“迭代器”。請看以下示例:
// 模擬的“前向迭代器”
template <typename T>
class ForwardIterator
{
public:
// 建構函式
ForwardIterator(const T &val);
// operator*
T &operator*() { return __val; }
// 只能++
void operator++(int) { __val++; }
private:
// 資料成員
T __val;
};
// 模擬的“雙向迭代器”
template <typename T>
class BidirectionalIterator
{
public:
// 建構函式
BidirectionalIterator(const T &val);
// operator*
T &operator*() { return __val; }
// 可以++和--
void operator++(int) { __val++; }
void operator--(int) { __val--; }
private:
// 資料成員
T __val;
};
// 模擬的“隨機訪問迭代器”
template <typename T>
class RandomAccessIterator
{
public:
// 建構函式
RandomAccessIterator(const T &val);
// operator*
T &operator*() { return __val; }
// 不僅可以++和--,還可以直接+=、-=
void operator++(int) { __val++; }
void operator--(int) { __val--; }
void operator+=(int N) { __val += N; }
void operator-=(int N) { __val -= N; }
private:
// 資料成員
T __val;
};
template <typename T>
ForwardIterator<T>::ForwardIterator(const T &val): __val(val) {}
template <typename T>
BidirectionalIterator<T>::BidirectionalIterator(const T &val): __val(val) {}
template <typename T>
RandomAccessIterator<T>::RandomAccessIterator(const T &val): __val(val) {}
此時,我們就可以實現出一個簡單的advance函式了。請看以下示例:
template <typename Iterator>
void Advance(Iterator &iter, int N)
{
for (int _ = 0; _ < N; _++) iter++;
}
int main()
{
ForwardIterator<int> forwardIterator(0);
BidirectionalIterator<int> bidirectionalIterator(0);
RandomAccessIterator<int> randomAccessIterator(0);
Advance(forwardIterator, 10);
Advance(bidirectionalIterator, 10);
Advance(randomAccessIterator, 10);
cout << *forwardIterator << endl; // 10
cout << *bidirectionalIterator << endl; // 10
cout << *randomAccessIterator << endl; // 10
}
上述實現似乎執行正常,但稍加分析不難發現,這一實現具有以下兩個主要缺點:
- N必須為正數,即使對於(允許N為負數的)雙向迭代器和隨機訪問迭代器而言也是一樣
- 隨機訪問迭代器並不需要像此實現這樣“步進”,從而造成了效率的浪費
怎麼樣才能既提供單一介面,又能夠讓介面有能力分辨出不同的迭代器類別呢?首先,我們需要將“迭代器的類別”這一語義表達出來,可以通過“強行編造”一些空類實現這一語義。請看以下示例:
// “強行編造”的三種不同的迭代器類別
struct ForwardIteratorTag {};
struct BidirectionalIteratorTag {};
struct RandomAccessIteratorTag {};
然後,我們可以為每個迭代器新增一個typedef,從而表示出這個迭代器的類別。請看以下示例:
template <typename T>
class ForwardIterator
{
public:
// 前向迭代器的類別是ForwardIteratorTag
typedef ForwardIteratorTag IteratorCategory;
// ...
};
template <typename T>
class BidirectionalIterator
{
public:
// 雙向迭代器的類別是BidirectionalIteratorTag
typedef BidirectionalIteratorTag IteratorCategory;
// ...
};
template <typename T>
class RandomAccessIterator
{
public:
// 隨機訪問迭代器的類別是RandomAccessIteratorTag
typedef RandomAccessIteratorTag IteratorCategory;
// ...
};
此時,當我們拿到一個迭代器型別Iterator時,我們就可以通過typename Iterator::IteratorCategory來獲取到當前迭代器的類別了。
同時,我們可以以迭代器類別作為Advance函式的第三引數,從而過載出多個不同版本的Advance函式。請看以下示例:
// 適用於前向迭代器的版本
template <typename Iterator>
void __Advance(Iterator &iter, int N, ForwardIteratorTag)
{
for (int _ = 0; _ < N; _++) iter++;
}
// 適用於雙向迭代器的版本
template <typename Iterator>
void __Advance(Iterator &iter, int N, BidirectionalIteratorTag)
{
if (N > 0)
{
for (int _ = 0; _ < N; _++) iter++;
}
else
{
for (int _ = 0; _ < -N; _++) iter--;
}
}
// 適用於隨機訪問迭代器的版本
template <typename Iterator>
void __Advance(Iterator &iter, int N, RandomAccessIteratorTag)
{
iter += N;
}
此時,我們已經擁有了兩組工具:
- 可以通過typename Iterator::IteratorCategory來獲取到當前迭代器的類別
- 實現了3個__Advance函式,分別適用於三個迭代器類別
此時,只要我們使用迭代器類別(作為第三引數)去呼叫__Advance函式,編譯器就將根據過載確定規則,選擇適用於當前迭代器類別的__Advance函式進行呼叫了。請看以下示例:
int main()
{
ForwardIterator<int> forwardIterator(0);
BidirectionalIterator<int> bidirectionalIterator(0);
RandomAccessIterator<int> randomAccessIterator(0);
// 過載確定至__Advance(Iterator &iter, int N, ForwardIteratorTag)版本
__Advance(forwardIterator, 10, typename ForwardIterator<int>::IteratorCategory());
// 過載確定至__Advance(Iterator &iter, int N, BidirectionalIteratorTag)版本
__Advance(bidirectionalIterator, 10, typename BidirectionalIterator<int>::IteratorCategory());
// 過載確定至__Advance(Iterator &iter, int N, RandomAccessIteratorTag)版本
__Advance(randomAccessIterator, 10, typename RandomAccessIterator<int>::IteratorCategory());
cout << *forwardIterator << endl; // 10
cout << *bidirectionalIterator << endl; // 10
cout << *randomAccessIterator << endl; // 10
}
這就結束了嗎?怎麼感覺函式的呼叫這麼麻煩呢?顯然還未結束。
不難發現,在任何一個函式的實現程式碼中,我們都不僅擁有引數值,還擁有每個引數的型別。所以,我們可以再實現一個只有兩個引數的函式,並在其內部構造出“typename Iterator::IteratorCategory()”,並呼叫三個引數的__Advance函式。而這,就是基於過載確定的編譯期分派技術。請看以下示例:
// 最終的“多功能函式”
template <typename Iterator>
void Advance(Iterator &iter, int N)
{
// 根據typename Iterator::IteratorCategory()進行基於過載確定的編譯期分派
__Advance(iter, N, typename Iterator::IteratorCategory());
}
int main()
{
ForwardIterator<int> forwardIterator(0);
BidirectionalIterator<int> bidirectionalIterator(0);
RandomAccessIterator<int> randomAccessIterator(0);
// 過載確定至__Advance(Iterator &iter, int N, ForwardIteratorTag)版本
Advance(forwardIterator, 10);
// 過載確定至__Advance(Iterator &iter, int N, BidirectionalIteratorTag)版本
Advance(bidirectionalIterator, 10);
// 過載確定至__Advance(Iterator &iter, int N, RandomAccessIteratorTag)版本
Advance(randomAccessIterator, 10);
cout << *forwardIterator << endl; // 10
cout << *bidirectionalIterator << endl; // 10
cout << *randomAccessIterator << endl; // 10
}
至此,我們就完成了Advance這個“多功能函式”的實現。但最後,我們還有一個重要問題需要解決:指標也是迭代器,那麼指標的迭代器型別(當然是隨機訪問迭代器)怎麼獲取?
也許不用我說,你就已經知道答案了,解決方案就是“加中間層可解決一切問題”定理。我們可以為“獲取迭代器型別”這一操作新增一箇中間層,並在此中間層中,對指標型別進行特化。請看以下示例:
// 迭代器的迭代器型別
template <typename Iterator>
struct IteratorTraits
{
typedef typename Iterator::IteratorCategory IteratorCategory;
};
// 指標的迭代器型別
template <typename T>
struct IteratorTraits<T *>
{
typedef RandomAccessIteratorTag IteratorCategory;
};
同時,我們還需要將上面的Advance函式中的“簡單粗暴的typename Iterator::IteratorCategory”替換為我們剛剛實現的IteratorTraits方法:
template <typename Iterator>
void Advance(Iterator &iter, int N)
{
// 根據typename IteratorTraits<Iterator>::IteratorCategory()進行基於過載確定的編譯期分派
__Advance(iter, N, typename IteratorTraits<Iterator>::IteratorCategory());
}
至此,Advance函式的實現也就全部完成了。
接下來,我們以一個更為複雜,也更為神奇的功能,繼續討論編譯期分派這一技術。
5.2 “萬能的”列印函式
本節將要實現的功能,其最終使用起來十分簡單:
print(X); // "X"可以是任何值!
沒錯,這是一個可以列印任何值的函式!通過上一節的鋪墊,我們知道:作為實現者,我們不僅可以得到X的值,還能“順便”得到X的型別。所以,我們就可以在X的型別上大做文章,針對不同的型別實現出不同的列印函式,最後,通過這個print函式進行編譯期分派,從而實現出這一神奇的函式。
首先應該實現什麼呢?不難發現,X可以是一個“可以直接cout的值”、(支援迭代器的)容器、Pair、Tuple、Stack、Queue等類別。所以,我們首先需要對X的這些不同的類別進行分類,通過建立很多空的Tag類即可實現此功能。請看以下示例:
// 預設類別
struct __CommonTag {};
// 線性容器類別
struct __SequenceContainerTag {};
// Map容器類別
struct __MapContainerTag {};
// Set容器類別
struct __SetContainerTag {};
// Pair類別
struct __PairTag {};
// Map中的Pair類別
struct __MapPairTag {};
// Tuple類別
struct __TupleTag {};
// Stack類別
struct __StackTag {};
// Queue類別
struct __QueueTag {};
然後,通過建立一系列的Traits,我們就可以將X的型別對映到這些Tag上:
// 如果T不是下面所提及的型別中的任何一種,那麼T的類別就是__CommonTag(即:“可以直接cout的型別”)
template <typename T>
struct __CategoryTraits
{
typedef __CommonTag __Category;
};
// array<T, N>的類別是__SequenceContainerTag
template <typename T, size_t N>
struct __CategoryTraits<array<T, N>>
{
typedef __SequenceContainerTag __Category;
};
// deque<T>的類別是__SequenceContainerTag
template <typename T>
struct __CategoryTraits<deque<T>>
{
typedef __SequenceContainerTag __Category;
};
// forward_list<T>的類別是__SequenceContainerTag
template <typename T>
struct __CategoryTraits<forward_list<T>>
{
typedef __SequenceContainerTag __Category;
};
// list<T>的類別是__SequenceContainerTag
template <typename T>
struct __CategoryTraits<list<T>>
{
typedef __SequenceContainerTag __Category;
};
// vector<T>的類別是__SequenceContainerTag
template <typename T>
struct __CategoryTraits<vector<T>>
{
typedef __SequenceContainerTag __Category;
};
// map<K, V>的類別是__MapContainerTag
template <typename K, typename V>
struct __CategoryTraits<map<K, V>>
{
typedef __MapContainerTag __Category;
};
// multimap<K, V>的類別是__MapContainerTag
template <typename K, typename V>
struct __CategoryTraits<multimap<K, V>>
{
typedef __MapContainerTag __Category;
};
// unordered_map<K, V>的類別是__MapContainerTag
template <typename K, typename V>
struct __CategoryTraits<unordered_map<K, V>>
{
typedef __MapContainerTag __Category;
};
// unordered_multimap<K, V>的類別是__MapContainerTag
template <typename K, typename V>
struct __CategoryTraits<unordered_multimap<K, V>>
{
typedef __MapContainerTag __Category;
};
// set<T>的類別是__SetContainerTag
template <typename T>
struct __CategoryTraits<set<T>>
{
typedef __SetContainerTag __Category;
};
// multiset<T>的類別是__SetContainerTag
template <typename T>
struct __CategoryTraits<multiset<T>>
{
typedef __SetContainerTag __Category;
};
// unordered_set<T>的類別是__SetContainerTag
template <typename T>
struct __CategoryTraits<unordered_set<T>>
{
typedef __SetContainerTag __Category;
};
// unordered_multiset<T>的類別是__SetContainerTag
template <typename T>
struct __CategoryTraits<unordered_multiset<T>>
{
typedef __SetContainerTag __Category;
};
// pair<T1, T2>的類別是__PairTag
template <typename T1, typename T2>
struct __CategoryTraits<pair<T1, T2>>
{
typedef __PairTag __Category;
};
// tuple<Types...>的類別是__TupleTag
template <typename... Types>
struct __CategoryTraits<tuple<Types...>>
{
typedef __TupleTag __Category;
};
// stack<T>的類別是__StackTag
template <typename T>
struct __CategoryTraits<stack<T>>
{
typedef __StackTag __Category;
};
// queue<T>的類別是__QueueTag
template <typename T>
struct __CategoryTraits<queue<T>>
{
typedef __QueueTag __Category;
};
// priority_queue<T>的類別是__StackTag
template <typename T>
struct __CategoryTraits<priority_queue<T>>
{
typedef __StackTag __Category;
};
接下來需要解決的問題是:我們希望面對類似於一維陣列這樣的“較小的值”和類似於高維陣列這樣的“較大的值”時,能夠採用或緊湊,或分散的不同的列印格式。具體什麼是“較小的值”呢?這裡,我們認為:如果X本身的類別就是__CommonTag,或X中的值(前提是X本身的類別不是__CommonTag)的類別是__CommonTag,則認為X是一個“較小的值”,採取緊湊的列印格式,否則,就認為X是一個“較大的值”,採取分散的列印格式。
那麼,我們就又引出了一個問題:對於某些型別,如Pair和Tuple,其中的各個元素的型別是不一樣的,即:各個元素的類別也是不一樣的;同時,很顯然,當我們面對多個類別時,只要其中有一個類別不是__CommonTag,那麼我們就應當認為這些類別的“最強大類別”不是__CommonTag。因此,我們首先需要實現一個獲取“最強大類別”的Traits。請看以下示例:
// 原型
// 通過typename __CategoryPromotionTraits<Tags...>::__Category獲取“最強大類別”
template <typename... Tags>
struct __CategoryPromotionTraits;
// 如果有兩個任意的類別,則隨便選一個類別作為“更強大類別”即可...
template <typename Tag1, typename Tag2>
struct __CategoryPromotionTraits<Tag1, Tag2>
{
typedef Tag1 __Category;
};
// ...但是,如果右類別是__CommonTag,則“更強大類別”需要“敬而遠之”...
template <typename Tag1>
struct __CategoryPromotionTraits<Tag1, __CommonTag>
{
typedef Tag1 __Category;
};
// ...同理,如果左類別是__CommonTag,則“更強大類別”也需要“敬而遠之”...
template <typename Tag2>
struct __CategoryPromotionTraits<__CommonTag, Tag2>
{
typedef Tag2 __Category;
};
// ...只有當“你我都是普通人”時,“更強大類別”才是__CommonTag
template <>
struct __CategoryPromotionTraits<__CommonTag, __CommonTag>
{
typedef __CommonTag __Category;
};
// 面對不止兩個類別時,“最強大類別”應該是Tag1與Tags...的“最強大類別”之間的“更強大類別”
template <typename Tag1, typename... Tags>
struct __CategoryPromotionTraits<Tag1, Tags...>
{
typedef typename __CategoryPromotionTraits<
Tag1,
typename __CategoryPromotionTraits<Tags...>::__Category
>::__Category __Category;
};
接下來,我們就來實現能夠獲取X中的元素的類別(即X的“子類別”)的Traits:
// 原型
// 通過typename __SubCategoryTraits<X的類別, X的型別>::__Category獲取X中的元素的類別
template <typename Tag, typename T>
struct __SubCategoryTraits;
// __CommonTag沒有子類別
template <typename T>
struct __SubCategoryTraits<__CommonTag, T>
{
typedef void __Category;
};
// __SequenceContainerTag的子類別就是T::value_type的類別
template <typename T>
struct __SubCategoryTraits<__SequenceContainerTag, T>
{
typedef typename __CategoryTraits<typename T::value_type>::__Category __Category;
};
// __MapContainerTag的子類別一定是__MapPairTag
template <typename T>
struct __SubCategoryTraits<__MapContainerTag, T>
{
typedef __MapPairTag __Category;
};
// __SetContainerTag的子類別就是T::value_type的類別
template <typename T>
struct __SubCategoryTraits<__SetContainerTag, T>
{
typedef typename __CategoryTraits<typename T::value_type>::__Category __Category;
};
// __MapPairTag的子類別是T::first_type的類別與T::second_type的類別之間的“更強大類別”
template <typename T>
struct __SubCategoryTraits<__MapPairTag, T>
{
typedef typename __CategoryPromotionTraits<
typename __CategoryTraits<typename T::first_type>::__Category,
typename __CategoryTraits<typename T::second_type>::__Category
>::__Category __Category;
};
// 和__MapPairTag一樣
// __PairTag的子類別也是T::first_type的類別與T::second_type的類別之間的“更強大類別”
template <typename T>
struct __SubCategoryTraits<__PairTag, T>
{
typedef typename __CategoryPromotionTraits<
typename __CategoryTraits<typename T::first_type>::__Category,
typename __CategoryTraits<typename T::second_type>::__Category
>::__Category __Category;
};
// __TupleTag的子類別是各個Types的類別之中的“最強大類別”
template <typename... Types>
struct __SubCategoryTraits<__TupleTag, tuple<Types...>>
{
typedef typename __CategoryPromotionTraits<
typename __CategoryTraits<Types>::__Category...
>::__Category __Category;
};
// __StackTag的子類別就是T::value_type的類別
template <typename T>
struct __SubCategoryTraits<__StackTag, T>
{
typedef typename __CategoryTraits<typename T::value_type>::__Category __Category;
};
// __QueueTag的子類別就是T::value_type的類別
template <typename T>
struct __SubCategoryTraits<__QueueTag, T>
{
typedef typename __CategoryTraits<typename T::value_type>::__Category __Category;
};
有了__CategoryTraits和__SubCategoryTraits這兩個工具,我們的準備工作也就基本上完成了。接下來是最後的一些簡單的準備工作:
- 我們需要定義一些控制列印樣式的字元或字串常量。請看以下示例:
// 縮排空白字元
const char __SPACE = ' ';
// 單次縮排長度
const int __INDENTATION_LEN = 4;
// 元素之間的分隔符
const string __VALUE_SPLICE = ", ";
// 行末分隔符
const char __LINE_END = ',';
// 線性容器類別的左右定界符
const char __SEQUENCE_CONTAINER_BEGIN = '[';
const char __SEQUENCE_CONTAINER_END = ']';
// Map容器類別的左右定界符
const char __MAP_CONTAINER_BEGIN = '{';
const char __MAP_CONTAINER_END = '}';
// Set容器類別的左右定界符
const char __SET_CONTAINER_BEGIN = '{';
const char __SET_CONTAINER_END = '}';
// Pair類別的左右定界符
const char __PAIR_BEGIN = '(';
const char __PAIR_END = ')';
// Map中的Pair類別的左右定界符
const char __MAP_PAIR_BEGIN = '(';
const char __MAP_PAIR_END = ')';
// Map中的Pair類別的鍵值對分隔符
const string __MAP_PAIR_SPLICE = ": ";
// Map中的Pair類別的鍵值對行末分隔符
const char __MAP_PAIR_LINE_END = ':';
// Tuple容器類別的左右定界符
const char __TUPLE_BEGIN = '(';
const char __TUPLE_END = ')';
// Stack容器類別的左右定界符
const char __STACK_BEGIN = '[';
const char __STACK_END = ']';
// Queue容器類別的左右定界符
const char __QUEUE_BEGIN = '[';
const char __QUEUE_END = ']';
- 對於Stack<int>,如果我們將1,2,3依次入棧,則取出時只能按3,2,1的順序出棧,這將造成很奇怪的列印效果。所以我們需要一個簡單的函式,用於將Stack反序。請看以下示例:
template <typename T>
T __reverseStack(T oriStack) // 必須使用值傳遞
{
T reverseStack;
while (!oriStack.empty())
{
reverseStack.push(oriStack.top());
oriStack.pop();
}
return reverseStack;
}
終於可以開始著手實現最重要的print函式了!這裡我們通過實現很多個__PrintData類的特化來實現針對不同類別及其子類別組合的編譯期分派。那麼,__PrintData類需要哪些模板引數呢?X的類別、X的子類別、X的型別,顯然都是需要的,此外,我們還需要在模板引數中維護一個整數,用於使每個模板都能夠知道“我在第幾層?”,以實現列印時的縮排控制。請看以下示例:
// 原型
// 通過__PrintData<SelfTag, SubTag, T, N>::__Print(val)列印val
template <typename SelfTag, typename SubTag, typename T, int N>
struct __PrintData;
// 列印__CommonTag類別的值(__CommonTag的子類別一定是void),採用緊湊的列印格式
template <typename T, int N>
struct __PrintData<__CommonTag, void, T, N>
{
static void __Print(const T &val);
};
// 列印__SequenceContainerTag類別 + 任意子類別組合的值,採用寬鬆的列印格式...
template <typename SubTag, typename T, int N>
struct __PrintData<__SequenceContainerTag, SubTag, T, N>
{
static void __Print(const T &val);
};
// ...但是,如果子類別是__CommonTag,則採用緊湊的列印格式
template <typename T, int N>
struct __PrintData<__SequenceContainerTag, __CommonTag, T, N>
{
static void __Print(const T &val);
};
// 列印__MapContainerTag類別 + __MapPairTag子類別組合的值,採用寬鬆的列印格式
template <typename T, int N>
struct __PrintData<__MapContainerTag, __MapPairTag, T, N>
{
static void __Print(const T &val);
};
// 列印__SetContainerTag類別 + 任意子類別組合的值,採用寬鬆的列印格式...
template <typename SubTag, typename T, int N>
struct __PrintData<__SetContainerTag, SubTag, T, N>
{
static void __Print(const T &val);
};
// ...但是,如果子類別是__CommonTag,則採用緊湊的列印格式
template <typename T, int N>
struct __PrintData<__SetContainerTag, __CommonTag, T, N>
{
static void __Print(const T &val);
};
// 列印__PairTag類別 + 任意子類別組合的值,採用寬鬆的列印格式...
template <typename SubTag, typename T, int N>
struct __PrintData<__PairTag, SubTag, T, N>
{
static void __Print(const T &val);
};
// ...但是,如果子類別是__CommonTag,則採用緊湊的列印格式
template <typename T, int N>
struct __PrintData<__PairTag, __CommonTag, T, N>
{
static void __Print(const T &val);
};
// 列印__MapPairTag類別 + 任意子類別組合的值,採用寬鬆的列印格式...
template <typename SubTag, typename T, int N>
struct __PrintData<__MapPairTag, SubTag, T, N>
{
static void __Print(const T &val);
};
// ...但是,如果子類別是__CommonTag,則採用緊湊的列印格式
template <typename T, int N>
struct __PrintData<__MapPairTag, __CommonTag, T, N>
{
static void __Print(const T &val);
};
// 列印__TupleTag類別 + 任意子類別組合的值,採用寬鬆的列印格式...
template <typename SubTag, typename T, int N>
struct __PrintData<__TupleTag, SubTag, T, N>
{
static void __Print(const T &val);
};
// ...但是,如果子類別是__CommonTag,則採用緊湊的列印格式
template <typename T, int N>
struct __PrintData<__TupleTag, __CommonTag, T, N>
{
static void __Print(const T &val);
};
// 列印__StackTag類別 + 任意子類別組合的值,採用寬鬆的列印格式...
template <typename SubTag, typename T, int N>
struct __PrintData<__StackTag, SubTag, T, N>
{
static void __Print(const T &val);
};
// ...但是,如果子類別是__CommonTag,則採用緊湊的列印格式
template <typename T, int N>
struct __PrintData<__StackTag, __CommonTag, T, N>
{
static void __Print(const T &val);
};
// 列印__QueueTag + 任意子類別組合的值,採用寬鬆的列印格式...
template <typename SubTag, typename T, int N>
struct __PrintData<__QueueTag, SubTag, T, N>
{
static void __Print(T val);
};
// ...但是,如果子類別是__CommonTag,則採用緊湊的列印格式
template <typename T, int N>
struct __PrintData<__QueueTag, __CommonTag, T, N>
{
static void __Print(T val);
};
以下是除了Tuple以外的(Tuple的列印函式的實現我們將在稍後討論)各個列印函式的實現:
// 列印__CommonTag類別的值(__CommonTag的子類別一定是void),採用緊湊的列印格式
template <typename T, int N>
void __PrintData<__CommonTag, void, T, N>::__Print(const T &val)
{
// 直接列印縮排與val
cout << string(N * __INDENTATION_LEN, __SPACE) << val;
}
// 列印__SequenceContainerTag類別 + 任意子類別組合的值,採用寬鬆的列印格式...
template <typename SubTag, typename T, int N>
void __PrintData<__SequenceContainerTag, SubTag, T, N>::__Print(const T &val)
{
// 列印縮排與線性容器類別的左定界符,然後換行
cout << string(N * __INDENTATION_LEN, __SPACE) << __SEQUENCE_CONTAINER_BEGIN << endl;
for (auto &subVal: val)
{
// 縮排層數+1,使用線性容器中的元素的__PrintData版本,依次列印線性容器中的每個元素
__PrintData<
SubTag,
typename __SubCategoryTraits<SubTag, typename T::value_type>::__Category,
typename T::value_type,
N + 1
>::__Print(subVal);
// 列印行末分隔符後換行
cout << __LINE_END << endl;
}
// 列印縮排與線性容器類別的右定界符
cout << string(N * __INDENTATION_LEN, __SPACE) << __SEQUENCE_CONTAINER_END;
}
// ...但是,如果子類別是__CommonTag,則採用緊湊的列印格式
template <typename T, int N>
void __PrintData<__SequenceContainerTag, __CommonTag, T, N>::__Print(const T &val)
{
// 列印縮排與線性容器類別的左定界符
cout << string(N * __INDENTATION_LEN, __SPACE) << __SEQUENCE_CONTAINER_BEGIN;
// 不換行,依次列印線性容器中的每個元素,以及元素之間的分隔符
if (!val.empty())
{
cout << val.front();
for (auto iter = next(val.begin()); iter != val.end(); iter++)
{
cout << __VALUE_SPLICE << *iter;
}
}
// 列印線性容器類別的右定界符
cout << __SEQUENCE_CONTAINER_END;
}
// 列印__MapContainerTag類別 + __MapPairTag子類別組合的值,採用寬鬆的列印格式
template <typename T, int N>
void __PrintData<__MapContainerTag, __MapPairTag, T, N>::__Print(const T &val)
{
// 列印縮排與Map容器類別的左定界符,然後換行
cout << string(N * __INDENTATION_LEN, __SPACE) << __MAP_CONTAINER_BEGIN << endl;
for (auto &subVal: val)
{
// 縮排層數+1,直接使用__MapPairTag類別以及Map容器的子類別生成__PrintData,用於列印Map中的Pair
__PrintData<
__MapPairTag,
typename __SubCategoryTraits<__MapPairTag, typename T::value_type>::__Category,
typename T::value_type,
N + 1
>::__Print(subVal);
// 列印行末分隔符後換行
cout << __LINE_END << endl;
}
// 列印縮排與Map容器類別的右定界符
cout << string(N * __INDENTATION_LEN, __SPACE) << __MAP_CONTAINER_END;
}
// 列印__SetContainerTag類別 + 任意子類別組合的值,採用寬鬆的列印格式...
template <typename SubTag, typename T, int N>
void __PrintData<__SetContainerTag, SubTag, T, N>::__Print(const T &val)
{
// 列印縮排與Set容器類別的左定界符,然後換行
cout << string(N * __INDENTATION_LEN, __SPACE) << __SET_CONTAINER_BEGIN << endl;
for (auto &subVal: val)
{
// 縮排層數+1,使用Set容器中的元素的__PrintData版本,依次列印Set容器中的每個元素
__PrintData<
SubTag,
typename __SubCategoryTraits<SubTag, typename T::value_type>::__Category,
typename T::value_type,
N + 1
>::__Print(subVal);
// 列印行末分隔符後換行
cout << __LINE_END << endl;
}
// 列印縮排與Set容器類別的右定界符
cout << string(N * __INDENTATION_LEN, __SPACE) << __SET_CONTAINER_END;
}
// ...但是,如果子類別是__CommonTag,則採用緊湊的列印格式
template <typename T, int N>
void __PrintData<__SetContainerTag, __CommonTag, T, N>::__Print(const T &val)
{
// 列印縮排與Set容器類別的左定界符
cout << string(N * __INDENTATION_LEN, __SPACE) << __SET_CONTAINER_BEGIN;
// 不換行,依次列印Set容器中的每個元素,以及元素之間的分隔符
if (!val.empty())
{
cout << *val.begin();
for (auto iter = next(val.begin()); iter != val.end(); iter++)
{
cout << __VALUE_SPLICE << *iter;
}
}
// 列印Set容器類別的右定界符
cout << __SET_CONTAINER_END;
}
// 列印__PairTag類別 + 任意子類別組合的值,採用寬鬆的列印格式...
template <typename SubTag, typename T, int N>
void __PrintData<__PairTag, SubTag, T, N>::__Print(const T &val)
{
// 列印縮排與Pair類別的左定界符,然後換行
cout << string(N * __INDENTATION_LEN, __SPACE) << __PAIR_BEGIN << endl;
// 縮排層數+1,使用val.first的__PrintData版本列印val.first
__PrintData<
typename __CategoryTraits<typename T::first_type>::__Category,
typename __SubCategoryTraits<
typename __CategoryTraits<typename T::first_type>::__Category,
typename T::first_type
>::__Category,
typename T::first_type,
N + 1
>::__Print(val.first);
// 列印行末分隔符後換行
cout << __LINE_END << endl;
// 縮排層數+1,使用val.second的__PrintData版本列印val.second
__PrintData<
typename __CategoryTraits<typename T::second_type>::__Category,
typename __SubCategoryTraits<
typename __CategoryTraits<typename T::second_type>::__Category,
typename T::second_type
>::__Category,
typename T::second_type,
N + 1
>::__Print(val.second);
// 列印行末分隔符後換行,再列印縮排與Pair類別的右定界符
cout << __LINE_END << endl << string(N * __INDENTATION_LEN, __SPACE) << __PAIR_END;
}
// ...但是,如果子類別是__CommonTag,則採用緊湊的列印格式
template <typename T, int N>
void __PrintData<__PairTag, __CommonTag, T, N>::__Print(const T &val)
{
// 直接依次列印縮排、Pair類別的左定界符、val.first、元素之間的分隔符、
// val.second以及Pair類別的右定界符
cout << string(N * __INDENTATION_LEN, __SPACE) << __PAIR_BEGIN <<
val.first << __VALUE_SPLICE << val.second << __PAIR_END;
}
// 列印__MapPairTag類別 + 任意子類別組合的值,採用寬鬆的列印格式...
// 此版本的實現與__PrintData<__PairTag, SubTag, T, N>版本高度相似
// 唯一的區別在於定界符的選取不同
template <typename SubTag, typename T, int N>
void __PrintData<__MapPairTag, SubTag, T, N>::__Print(const T &val)
{
// 列印縮排與Map中的Pair類別的左定界符,然後換行
cout << string(N * __INDENTATION_LEN, __SPACE) << __MAP_PAIR_BEGIN << endl;
// 縮排層數+1,使用val.first的__PrintData版本列印val.first
__PrintData<
typename __CategoryTraits<typename T::first_type>::__Category,
typename __SubCategoryTraits<
typename __CategoryTraits<typename T::first_type>::__Category,
typename T::first_type
>::__Category,
typename T::first_type,
N + 1
>::__Print(val.first);
// 列印Map中的Pair類別的鍵值對行末分隔符後換行
cout << __MAP_PAIR_LINE_END << endl;
// 縮排層數+1,使用val.second的__PrintData版本列印val.second
__PrintData<
typename __CategoryTraits<typename T::second_type>::__Category,
typename __SubCategoryTraits<
typename __CategoryTraits<typename T::second_type>::__Category,
typename T::second_type
>::__Category,
typename T::second_type,
N + 1
>::__Print(val.second);
// 列印行末分隔符後換行,再列印縮排與Map中的Pair類別的右定界符
cout << __LINE_END << endl << string(N * __INDENTATION_LEN, __SPACE) << __MAP_PAIR_END;
}
// ...但是,如果子類別是__CommonTag,則採用緊湊的列印格式
// 此版本的實現與__PrintData<__PairTag, __CommonTag, T, N>版本高度相似
// 唯一的區別在於定界符的選取不同
template <typename T, int N>
void __PrintData<__MapPairTag, __CommonTag, T, N>::__Print(const T &val)
{
// 直接依次列印縮排、Map中的Pair類別的左定界符、val.first、
// Map中的Pair類別的鍵值對分隔符、val.second以及Map中的Pair類別的右定界符
cout << string(N * __INDENTATION_LEN, __SPACE) << __MAP_PAIR_BEGIN <<
val.first << __MAP_PAIR_SPLICE << val.second << __MAP_PAIR_END;
}
// 列印__StackTag類別 + 任意子類別組合的值,採用寬鬆的列印格式...
template <typename SubTag, typename T, int N>
void __PrintData<__StackTag, SubTag, T, N>::__Print(const T &val)
{
// 得到一個反序的Stack
T reverseVal = __reverseStack(val);
// 列印縮排與Stack容器類別的左定界符,然後換行
cout << string(N * __INDENTATION_LEN, __SPACE) << __STACK_BEGIN << endl;
while (!reverseVal.empty())
{
// 縮排層數+1,使用Stack容器中的元素的__PrintData版本,依次列印Stack容器中的每個元素
__PrintData<
SubTag,
typename __SubCategoryTraits<SubTag, typename T::value_type>::__Category,
typename T::value_type,
N + 1
>::__Print(reverseVal.top());
// 列印行末分隔符後換行
cout << __LINE_END << endl;
// 出棧
reverseVal.pop();
}
// 列印縮排與Stack容器類別的右定界符
cout << string(N * __INDENTATION_LEN, __SPACE) << __STACK_END;
}
// ...但是,如果子類別是__CommonTag,則採用緊湊的列印格式
template <typename T, int N>
void __PrintData<__StackTag, __CommonTag, T, N>::__Print(const T &val)
{
// 得到一個反序的Stack
T reverseVal = __reverseStack(val);
// 列印縮排與Stack容器類別的左定界符
cout << string(N * __INDENTATION_LEN, __SPACE) << __STACK_BEGIN;
// 不換行,依次列印Stack容器中的每個元素,以及元素之間的分隔符
if (!reverseVal.empty())
{
cout << reverseVal.top();
reverseVal.pop();
while (!reverseVal.empty())
{
cout << __VALUE_SPLICE << reverseVal.top();
reverseVal.pop();
}
}
// 列印Stack容器類別的右定界符
cout << __STACK_END;
}
// 列印__QueueTag類別 + 任意子類別組合的值,採用寬鬆的列印格式...
template <typename SubTag, typename T, int N>
void __PrintData<__QueueTag, SubTag, T, N>::__Print(T val)
{
// 列印縮排與Queue容器類別的左定界符,然後換行
cout << string(N * __INDENTATION_LEN, __SPACE) << __QUEUE_BEGIN << endl;
while (!val.empty())
{
// 縮排層數+1,使用Queue容器中的元素的__PrintData版本,依次列印Queue容器中的每個元素
__PrintData<
SubTag,
typename __SubCategoryTraits<SubTag, typename T::value_type>::__Category,
typename T::value_type,
N + 1
>::__Print(val.front());
// 列印行末分隔符後換行
cout << __LINE_END << endl;
// 出佇列
val.pop();
}
// 列印縮排與Queue容器類別的右定界符
cout << string(N * __INDENTATION_LEN, __SPACE) << __QUEUE_END;
}
// ...但是,如果子類別是__CommonTag,則採用緊湊的列印格式
template <typename T, int N>
void __PrintData<__QueueTag, __CommonTag, T, N>::__Print(T val)
{
// 列印縮排與Queue容器類別的左定界符
cout << string(N * __INDENTATION_LEN, __SPACE) << __QUEUE_BEGIN;
// 不換行,依次列印Queue容器中的每個元素,以及元素之間的分隔符
if (!val.empty())
{
cout << val.front();
val.pop();
while (!val.empty())
{
cout << __VALUE_SPLICE << val.front();
val.pop();
}
}
// 列印Queue容器類別的右定界符
cout << __QUEUE_END;
}
怎麼列印Tuple?雖然Tuple不能通過真正的for迴圈進行遍歷,但我們可以使用編譯期的“for迴圈”對Tuple進行“遍歷”。請看以下示例:
// 以緊湊(單行)的列印格式列印Tuple
// 從“索引值”0,向“最大索引值”TopIdx進行“迭代”
template <typename T, int Idx, int TopIdx>
struct __PrintTupleOneLine
{
static void __Print(const T &val);
};
// 當Idx == TopIdx時,“迭代”結束
template <typename T, int TopIdx>
struct __PrintTupleOneLine<T, TopIdx, TopIdx>
{
static void __Print(const T &val) {}
};
// 以寬鬆(多行)的列印格式列印Tuple
// 從“索引值”0,向“最大索引值”TopIdx進行“迭代”
template <typename T, int N, int Idx, int TopIdx>
struct __PrintTupleMultiLine
{
static void __Print(const T &val);
};
// 當Idx == TopIdx時,“迭代”結束
template <typename T, int N, int TopIdx>
struct __PrintTupleMultiLine<T, N, TopIdx, TopIdx>
{
static void __Print(const T &val) {}
};
template <typename T, int Idx, int TopIdx>
void __PrintTupleOneLine<T, Idx, TopIdx>::__Print(const T &val)
{
// 列印“val[Idx]”
cout << get<Idx>(val);
// 如果“val[Idx]”不是Tuple的最後一個元素,則列印元素之間的分隔符
if (Idx + 1 < TopIdx)
{
cout << __VALUE_SPLICE;
}
// 繼續列印“val[Idx + 1]”...
__PrintTupleOneLine<T, Idx + 1, TopIdx>::__Print(val);
}
template <typename T, int N, int Idx, int TopIdx>
void __PrintTupleMultiLine<T, N, Idx, TopIdx>::__Print(const T &val)
{
// 縮排層數+1,使用“val[Idx]”的__PrintData版本列印“val[Idx]”
__PrintData<
typename __CategoryTraits<typename tuple_element<Idx, T>::type>::__Category,
typename __SubCategoryTraits<
typename __CategoryTraits<typename tuple_element<Idx, T>::type>::__Category,
typename tuple_element<Idx, T>::type
>::__Category,
typename tuple_element<Idx, T>::type,
N + 1
>::__Print(get<Idx>(val));
// 列印行末分隔符後換行
cout << __LINE_END << endl;
// 繼續列印“val[Idx + 1]”...
__PrintTupleMultiLine<T, N, Idx + 1, TopIdx>::__Print(val);
}
此時,我們就可以提供用於列印Tuple的__PrintData的定義了:
// 列印__TupleTag類別 + 任意子類別組合的值,採用寬鬆的列印格式...
template <typename SubTag, typename T, int N>
void __PrintData<__TupleTag, SubTag, T, N>::__Print(const T &val)
{
// 列印縮排與Tuple容器類別的左定界符,然後換行
cout << string(N * __INDENTATION_LEN, __SPACE) << __TUPLE_BEGIN << endl;
// 呼叫多行版本的Tuple列印函式,列印val
__PrintTupleMultiLine<T, N, 0, tuple_size<T>::value>::__Print(val);
// 列印Tuple容器類別的右定界符
cout << string(N * __INDENTATION_LEN, __SPACE) << __TUPLE_END;
}
// ...但是,如果子類別是__CommonTag,則採用緊湊的列印格式
template <typename T, int N>
void __PrintData<__TupleTag, __CommonTag, T, N>::__Print(const T &val)
{
// 列印縮排與Tuple容器類別的左定界符
cout << string(N * __INDENTATION_LEN, __SPACE) << __TUPLE_BEGIN;
// 呼叫單行版本的Tuple列印函式,列印val
__PrintTupleOneLine<T, 0, tuple_size<T>::value>::__Print(val);
// 列印Tuple容器類別的右定界符
cout << __TUPLE_END;
}
至此,我們已經實現了print函式所需要的一切底層元件。現在我們需要做的,就是匯聚所有的這些底層元件,最終實現print函式。請看以下示例:
template <typename T>
void print(const T &val)
{
__PrintData<
// T的類別
typename __CategoryTraits<T>::__Category,
// T的子類別
typename __SubCategoryTraits<
typename __CategoryTraits<T>::__Category,
T
>::__Category,
// val的型別
T,
// 縮排層數從0開始
0
>::__Print(val);
cout << endl;
}
讓我們立即來試試這個print函式的效果:
int main()
{
// 普通值
int sampleInt = 123;
double *samplePtr = nullptr;
string sampleStr = "abc";
print(sampleInt); // 123
print(samplePtr); // 0
print(sampleStr); // abc
// 線性容器
array<int, 3> sampleArray {1, 2, 3};
vector<string> sampleVector {"abc", "def", "ghi"};
list<deque<forward_list<string>>> sampleComplexContainer {{{"abc", "def"}, {"ghi", "jkl"}}, {{"mno", "pqr"}, {"stu", "vwx"}}};
print(sampleArray); // [1, 2, 3]
print(sampleVector); // [abc, def, ghi]
/*
[
[
[abc, def],
[ghi, jkl],
],
[
[mno, pqr],
[stu, vwx],
],
]
*/
print(sampleComplexContainer);
// Map容器
map<int, string> sampleMap {{1, "abc"}, {2, "def"}, {3, "ghi"}};
multimap<int, vector<string>> sampleComplexMap {{1, {"abc", "def"}}, {2, {"ghi", "jkl"}}, {3, {"mno", "pqu"}}};
/*
{
(1: abc),
(2: def),
(3: ghi),
}
*/
print(sampleMap);
/*
{
(
1:
[abc, def],
),
(
2:
[ghi, jkl],
),
(
3:
[mno, pqu],
),
}
*/
print(sampleComplexMap);
// Set容器
set<int> sampleSet {1, 2, 3};
multiset<vector<bool>> sampleComplexSet {{true, false}, {false, true}, {true, false, false, true}};
print(sampleSet); // {1, 2, 3}
/*
{
[0, 1],
[1, 0],
[1, 0, 0, 1],
}
*/
print(sampleComplexSet);
// Pair
pair<int, string> samplePair {1, "abc"};
pair<int, vector<string>> sampleComplexPair {1, {"abc", "def", "ghi"}};
print(samplePair); // (1, abc)
/*
(
1,
[abc, def, ghi],
)
*/
print(sampleComplexPair);
// Tuple容器
tuple<int, double, char, string> sampleTuple {1, 2., 'a', "abc"};
tuple<int, double, char, string, vector<string>> sampleComplexTuple {1, 2., 'a', "abc", {"abc", "def", "ghi"}};
print(sampleTuple); // (1, 2, a, abc)
/*
(
1,
2,
a,
abc,
[abc, def, ghi],
)
*/
print(sampleComplexTuple);
// Stack容器
stack<int> sampleStack;
sampleStack.push(1);
sampleStack.push(2);
sampleStack.push(3);
stack<vector<string>> sampleComplexStack;
sampleComplexStack.push({"abc", "def"});
sampleComplexStack.push({"ghi", "jkl"});
sampleComplexStack.push({"mno", "pqr"});
/*
棧底 --------> 棧頂
[1, 2, 3]
*/
print(sampleStack);
/*
棧底
[ |
[abc, def], |
[ghi, jkl], |
[mno, pqr], |
] v
棧頂
*/
print(sampleComplexStack);
// Queue容器
queue<int> sampleQueue;
sampleQueue.push(1);
sampleQueue.push(2);
sampleQueue.push(3);
priority_queue<vector<string>> sampleComplexPriorityQueue;
sampleComplexPriorityQueue.push({"abc", "def"});
sampleComplexPriorityQueue.push({"ghi", "jkl"});
sampleComplexPriorityQueue.push({"mno", "pqr"});
/*
佇列首 <-------- 佇列尾
[1, 2, 3]
*/
print(sampleQueue);
/*
佇列首
[ ^
[mno, pqr], |
[ghi, jkl], |
[abc, def], |
] |
佇列尾
*/
print(sampleComplexPriorityQueue);
}
至此,print函式的實現也就全部完成了。
5.3 本章後記
本章,我們首先通過一個簡單的STL advance函式,討論了編譯期分派技術。這一函式的實現過程能夠帶給我們兩點思考:
- 為什麼我們能在使用者無感知的情況下實現分派呢?不難發現:你,作為一個函式的實現者,有一樣東西是使用者所不具有的,那就是型別。當使用者使用一個值來呼叫函式時,你不僅能得到這個值,還能“順便”得到這個值的型別,而正是因為有了這多出來的型別,我們就能在型別上大做文章,實現出很多“神奇”的介面
- 事實上,並沒有真正的“多功能函式”。但我們可以通過“新增中間層可解決一切問題”這一“經典定理”,使得單一的介面函式,根據傳入的實參型別的不同,“多功能的”呼叫多個底層的實現函式,從而達到“多功能函式”的效果
緊接著,我們實現了一個程式碼更為複雜的print函式。觀其輸出結果,不禁讓我們感慨:一個小小的T,在經過我們的“大做文章”之後,竟能夠表現出如此豐富的多樣性!這,就是編譯期分派的強大威力所在!
6 “突破極限”的容器——Tuple
Tuple是一種非常特殊且高階的資料結構,其能夠容納和取用數量、型別都不定的一組值,你也可以將Tuple理解為某種“匿名結構體”。乍看之下,“數量、型別都不定”和模板中“什麼都是已經確定的編譯期常量”從語法上就是完全相悖的,和容器的“所有元素的型別必須相同”的原則也是完全相悖的,似乎,Tuple是一種“突破極限”的容器。可事實真的是如此嗎?
6.1 可遞迴Pair
首先,請看下面這段“平淡無奇”的程式碼:
template <typename T1, typename T2>
struct __RecursionPair
{
public:
// 資料成員
T1 __first;
T2 __second;
// 建構函式
__RecursionPair();
__RecursionPair(const T1 &first, const T2 &second);
};
template <typename T1, typename T2>
__RecursionPair<T1, T2>::__RecursionPair() = default;
template <typename T1, typename T2>
__RecursionPair<T1, T2>::__RecursionPair(const T1 &first, const T2 &second):
__first(first), __second(second) {}
// 針對只有一個值的Pair的特化
template <typename T1>
struct __RecursionPair<T1, void>
{
public:
// 資料成員
T1 __first;
// 建構函式
__RecursionPair();
explicit __RecursionPair(const T1 &first);
};
template <typename T1>
__RecursionPair<T1, void>::__RecursionPair() = default;
template <typename T1>
__RecursionPair<T1, void>::__RecursionPair(const T1 &first): __first(first) {}
int main()
{
__RecursionPair<int, double> _(1, 2.);
__RecursionPair<int, void> __(1);
}
“這不就是STL的Pair嗎?”,你一定會有這樣的疑問。沒錯,這確實就是STL的Pair,但請你繼續看:
__RecursionPair<int, __RecursionPair<double, __RecursionPair<char, string>>> multiPair; // 還有這種操作???
沒錯!就是有這樣的操作。此時,也許你已經意識到了,只要“無限堆疊”這樣的Pair,理論上就能實現一個任意數量+任意型別的容器了。我們稱這樣的Pair為“可遞迴Pair”。
故事結束了嗎?不,這才只是個開始。我們可以立即發現,這種“無限堆疊”產生的“千層餅”,是一個非常反人類的東西,不僅沒有“索引值”,甚至為了取得第10個值,竟然需要連著寫9遍“.second”!這也太反人類了!請不要著急,接著往下看。
6.2 為可遞迴Pair的型別新增“索引值”
接下來,我們需要解決似乎很棘手的一個問題:如何根據“索引值”獲取可遞迴Pair的某個位置的型別呢?要知道,可遞迴Pair裡面可是根本沒有“索引值”這一概念啊。
讓我們先試著邁出第一步:獲取__RecursionPair<T1, void>的T1。請看以下示例:
// 原型
// 通過typename __RecursionPairType<T, N>::__ValueType獲取“T[N]”的型別
template <int N, typename T>
struct __RecursionPairType;
// 獲取__RecursionPair<T1, void>的第0個型別(相當於“T[0]”的型別)
template <typename T1>
struct __RecursionPairType<0, __RecursionPair<T1, void>>
{
// __RecursionPair<T1, void>的第0個型別顯然就是T1
typedef T1 __ValueType;
};
似乎很順利對不對?讓我們繼續:獲取__RecursionPair<T1, T2>的T1和T2。請看以下示例:
// 獲取__RecursionPair<T1, T2>的第0個型別(相當於“T[0]”的型別)
template <typename T1, typename T2>
struct __RecursionPairType<0, __RecursionPair<T1, T2>>
{
// __RecursionPair<T1, T2>的第0個型別顯然就是T1
typedef T1 __ValueType;
};
// 獲取__RecursionPair<T1, T2>的第1個型別(相當於“T[1]”的型別)
template <typename T1, typename T2>
struct __RecursionPairType<1, __RecursionPair<T1, T2>>
{
// __RecursionPair<T1, T2>的第1個型別顯然就是T2
typedef T2 __ValueType;
};
接下來,我們就要面對真正的難題了,它就是:
__RecursionPair<T1, __RecursionPair<T2, T3>>
仔細分析這一型別不難發現:T1和T2一定不會繼續是一個__RecursionPair型別(因為我們人為地“預設”了可遞迴Pair只有second可以進行遞迴,實際上first也可以進行遞迴,但是這樣的程式碼看上去比較“彆扭”)。所以,我們立即可以給出以下實現:
// 獲取__RecursionPair<T1, __RecursionPair<T2, T3>>的第0個型別(相當於“T[0]”的型別)
template <typename T1, typename T2, typename T3>
struct __RecursionPairType<0, __RecursionPair<T1, __RecursionPair<T2, T3>>>
{
// 因為T1一定不會繼續是一個__RecursionPair型別
// 所以__RecursionPair<T1, __RecursionPair<T2, T3>>的第0個型別應該就是T1
typedef T1 __ValueType;
};
// 獲取__RecursionPair<T1, __RecursionPair<T2, T3>>的第1個型別(相當於“T[1]”的型別)
template <typename T1, typename T2, typename T3>
struct __RecursionPairType<1, __RecursionPair<T1, __RecursionPair<T2, T3>>>
{
// 因為T2一定不會繼續是一個__RecursionPair型別
// 所以__RecursionPair<T1, __RecursionPair<T2, T3>>的第1個型別應該就是T2
typedef T2 __ValueType;
};
那麼,如果N大於1,要怎麼辦呢?此時,雖然我們自己已經無能為力(因為我們並沒有能力“拆分”T3),但是我們可以“寄希望於”遞迴。請看以下示例:
// 獲取__RecursionPair<T1, __RecursionPair<T2, T3>>的第N(N > 1)個型別(相當於“T[N]”的型別)
template <int N, typename T1, typename T2, typename T3>
struct __RecursionPairType<N, __RecursionPair<T1, __RecursionPair<T2, T3>>>
{
// 如果N大於1,那麼“T[N]”的型別應該是__RecursionPair<T2, T3>的第N - 1個型別
typedef typename __RecursionPairType<N - 1, __RecursionPair<T2, T3>>::__ValueType __ValueType;
};
至此,我們就完整實現了根據“索引值”獲取可遞迴Pair的某個位置的型別這一工具。讓我們來看看效果:
int main()
{
typedef __RecursionPair<int, __RecursionPair<double, __RecursionPair<char, string>>> Type;
cout << typeid(__RecursionPairType<3, Type>::__ValueType).name(); // string
}
可以看出,輸出結果完全符合我們的預期。
看到這裡,也許你會覺得上述實現中的第4個和第5個特化(即__RecursionPairType<0, __RecursionPair<T1, __RecursionPair<T2, T3>>>和__RecursionPairType<1, __RecursionPair<T1, __RecursionPair<T2, T3>>>版本)似乎是多餘的?你可以去掉這些特化,然後編譯試試看。
6.3 為可遞迴Pair的值新增“索引值”
有了上文中__RecursionPairType的鋪墊,根據“索引值”獲取可遞迴Pair的某個位置的值這一功能似乎也可以“依葫蘆畫瓢”進行實現了。同樣,讓我們先邁出第一步:
// 原型
// 通過__RecursionPairValue<N, T>::__Get(pairObj)獲取“pairObj[N]”的值
template <int N, typename T>
struct __RecursionPairValue;
// 獲取__RecursionPair<T1, void>的“pairObj[0]”的值
template <typename T1>
struct __RecursionPairValue<0, __RecursionPair<T1, void>>
{
// __Get函式的引數型別顯然就是__RecursionPair<T1, void>
typedef __RecursionPair<T1, void> __PairType;
// __Get函式的返回值型別顯然就是T1
typedef T1 __ValueType;
// 實際的返回值顯然就是pairObj.__first
static __ValueType &__Get(__PairType &pairObj) { return pairObj.__first; }
static const __ValueType &__Get(const __PairType &pairObj) { return pairObj.__first; }
};
讓我們繼續。接下來實現__RecursionPair<T1, T2>的取值:
// 獲取__RecursionPair<T1, T2>的“pairObj[0]”的值
template <typename T1, typename T2>
struct __RecursionPairValue<0, __RecursionPair<T1, T2>>
{
// __Get函式的引數型別顯然就是__RecursionPair<T1, T2>
typedef __RecursionPair<T1, T2> __PairType;
// __Get函式的返回值型別顯然就是T1
typedef T1 __ValueType;
// 實際的返回值顯然就是pairObj.__first
static __ValueType &__Get(__PairType &pairObj) { return pairObj.__first; }
static const __ValueType &__Get(const __PairType &pairObj) { return pairObj.__first; }
};
// 獲取__RecursionPair<T1, T2>的“pairObj[1]”的值
template <typename T1, typename T2>
struct __RecursionPairValue<1, __RecursionPair<T1, T2>>
{
// __Get函式的引數型別顯然就是__RecursionPair<T1, T2>
typedef __RecursionPair<T1, T2> __PairType;
// __Get函式的返回值型別顯然就是T2
typedef T2 __ValueType;
// 實際的返回值顯然就是pairObj.__second
static __ValueType &__Get(__PairType &pairObj) { return pairObj.__second; }
static const __ValueType &__Get(const __PairType &pairObj) { return pairObj.__second; }
};
讓我們繼續。接下來實現__RecursionPair<T1, __RecursionPair<T2, T3>>的取值:
// 獲取__RecursionPair<T1, __RecursionPair<T2, T3>>的“pairObj[0]”的值
template <typename T1, typename T2, typename T3>
struct __RecursionPairValue<0, __RecursionPair<T1, __RecursionPair<T2, T3>>>
{
// __Get函式的引數型別顯然就是__RecursionPair<T1, __RecursionPair<T2, T3>>
typedef __RecursionPair<T1, __RecursionPair<T2, T3>> __PairType;
// __Get函式的返回值型別顯然就是T1
typedef T1 __ValueType;
// 實際的返回值顯然就是pairObj.__first
static __ValueType &__Get(__PairType &pairObj) { return pairObj.__first; }
static const __ValueType &__Get(const __PairType &pairObj) { return pairObj.__first; }
};
// 獲取__RecursionPair<T1, __RecursionPair<T2, T3>>的“pairObj[1]”的值
template <typename T1, typename T2, typename T3>
struct __RecursionPairValue<1, __RecursionPair<T1, __RecursionPair<T2, T3>>>
{
// __Get函式的引數型別顯然就是__RecursionPair<T1, __RecursionPair<T2, T3>>
typedef __RecursionPair<T1, __RecursionPair<T2, T3>> __PairType;
// __Get函式的返回值型別顯然就是T2
typedef T2 __ValueType;
// 實際的返回值顯然就是pairObj.__second.__first
static __ValueType &__Get(__PairType &pairObj) { return pairObj.__second.__first; }
static const __ValueType &__Get(const __PairType &pairObj) { return pairObj.__second.__first; }
};
那麼,如果N大於1,要怎麼辦呢?我們需要解決兩個問題:
- Get函式的返回值型別是什麼?
- 怎麼得到“pairObj[N]”的值?
第一個問題的解決方案不言而喻:我們已經實現了可以獲取到可遞迴Pair的任意位置的型別的工具,這當然可以在這裡為我們所用;對於第二個問題,我們同樣可以基於遞迴,對pairObj.second進行“拆分”,直至N降至1為止。請看以下示例:
// 獲取__RecursionPair<T1, __RecursionPair<T2, T3>>的“pairObj[N]”的值
template <int N, typename T1, typename T2, typename T3>
struct __RecursionPairValue<N, __RecursionPair<T1, __RecursionPair<T2, T3>>>
{
// __Get函式的引數型別顯然就是__RecursionPair<T1, __RecursionPair<T2, T3>>
typedef __RecursionPair<T1, __RecursionPair<T2, T3>> __PairType;
// __Get函式的返回值型別需要依賴我們前面已經實現的__RecursionPairType獲取
typedef typename __RecursionPairType<N, __PairType>::__ValueType __ValueType;
// 我們並沒有能力“拆分”pairObj.__second.__second,但是我們可以“寄希望於”遞迴
static __ValueType &__Get(__PairType &pairObj)
{
return __RecursionPairValue<N - 1, __RecursionPair<T2, T3>>::__Get(pairObj.__second);
}
// 同上
static const __ValueType &__Get(const __PairType &pairObj)
{
return __RecursionPairValue<N - 1, __RecursionPair<T2, T3>>::__Get(pairObj.__second);
}
};
至此,我們就完整實現了根據“索引值”獲取可遞迴Pair的某個位置的值這一工具。讓我們來看看效果:
int main()
{
__RecursionPair<int, __RecursionPair<double, __RecursionPair<char, string>>> testPair;
__RecursionPairValue<3, decltype(testPair)>::__Get(testPair) = "abc";
cout << __RecursionPairValue<3, decltype(testPair)>::__Get(testPair); // abc
}
同樣,如果你覺得上述實現中的第4個和第5個特化(即__RecursionPairValue<0, __RecursionPair<T1, __RecursionPair<T2, T3>>>和__RecursionPairValue<1, __RecursionPair<T1, __RecursionPair<T2, T3>>>版本)是多餘的,你可以去掉這些特化,然後編譯試試看。
6.4 將“千層餅”擀成“單層餅”
本節將會是整個Tuple的實現中最為精彩的部分!
我們雖然已經實現了針對可遞迴Pair的取型別和取值工具,但我們還是沒有實現出一個“扁平的”真正的Tuple(沒錯,終於又看到Tuple這個詞了)。接下來,我們就開始著手考慮,如何將可遞迴Pair這張“千層餅”擀平,變成一張“單層餅”Tuple。
如何實現“擀平”這一操作呢?稍加思考不難發現,可遞迴Pair和Tuple之間似乎存在著這樣的一一對應關係:
- 含有一個元素的Tuple,就是一個__RecursionPair<T1, void>
- 含有兩個元素的Tuple,就是一個__RecursionPair<T1, T2>
- 含有三個元素的Tuple,就是一個__RecursionPair<T1, __RecursionPair<T2, T3>>
- 含有四個元素的Tuple,就是一個__RecursionPair<T1, __RecursionPair<T2, __RecursionPair<T3, T4>>>
- ...
如何描述這種“是一個”的語義?哦!是繼承!
請看以下示例:
// 原型
// 通過Tuple<Types...>構造一個Tuple
template <typename... Types>
struct Tuple;
// 含有一個元素的Tuple,就是一個__RecursionPair<T1, void>
template <typename T1>
struct Tuple<T1>: __RecursionPair<T1, void>
{
// 我是一個怎樣的__RecursionPair?當然是繼承的那個!
typedef __RecursionPair<T1, void> __PairType;
// 建構函式(待實現)
Tuple();
Tuple(const T1 &first);
};
// 含有兩個元素的Tuple,就是一個__RecursionPair<T1, T2>
template <typename T1, typename T2>
struct Tuple<T1, T2>: __RecursionPair<T1, T2>
{
// 我是一個怎樣的__RecursionPair?當然也是繼承的那個!
typedef __RecursionPair<T1, T2> __PairType;
// 建構函式(待實現)
Tuple();
Tuple(const T1 &first, const T2 &second);
};
// 預設建構函式
template <typename T1>
Tuple<T1>::Tuple() = default;
// 只需要呼叫Tuple的“可遞迴Pair形態”(即父類)的建構函式即可
template <typename T1>
Tuple<T1>::Tuple(const T1 &first): __PairType(first) {}
// 預設建構函式
template <typename T1, typename T2>
Tuple<T1, T2>::Tuple() = default;
// 同樣,只需要呼叫Tuple的“可遞迴Pair形態”(即父類)的建構函式即可
template <typename T1, typename T2>
Tuple<T1, T2>::Tuple(const T1 &first, const T2 &second): __PairType(first, second) {}
那麼,含有不止兩個元素的Tuple,是哪個可遞迴Pair呢?如果你已經注意到了上面的兩個Tuple實現中的“看似無用”的typedef,那麼問題就能迎刃而解。這些typedef,儲存了當前Tuple所對應的“可遞迴Pair形態”。從可遞迴Pair的角度去思考,不難找到以下規律:
- Tuple<T1, T2, T3>的“可遞迴Pair形態”是__RecursionPair<T1, typename Tuple<T2, T3>::__PairType>,即:將T1,與Tuple<T2, T3>的“可遞迴Pair形態”放入一個__RecursionPair中,最終得到的結果是__RecursionPair<T1, __RecursionPair<T2, T3>>
- Tuple<T1, T2, T3, T4>的“可遞迴Pair形態”是__RecursionPair<T1, typename Tuple<T2, T3, T4>::__PairType>,即:將T1,與Tuple<T2, T3, T4>的“可遞迴Pair形態”放入一個__RecursionPair中(Tuple<T2, T3, T4>的“可遞迴Pair形態”就是上面已經得到的Tuple<T1, T2, T3>的“可遞迴Pair形態”),最終得到的結果是__RecursionPair<T1, __RecursionPair<T2, __RecursionPair<T3, T4>>>
- ...
- Tuple<T1, Types...>的“可遞迴Pair形態”是__RecursionPair<T1, typename Tuple<Types...>::__PairType>,即:將T1,與Tuple<Types...>的“可遞迴Pair形態”放入一個__RecursionPair中,最終得到的結果是__RecursionPair<T1, __RecursionPair<T2, __RecursionPair<T3, ..., __RecursionPair<T(N - 1), T(N)>>>...>
找到了這一規律,程式碼實現也就輕而易舉了。請看以下示例:
// Tuple<T1, Types...>的“可遞迴Pair形態”是:將T1,與typename Tuple<Types...>::__PairType
//(即Tuple<Types...>的“可遞迴Pair形態”)放入一個__RecursionPair中
template <typename T1, typename... Types>
struct Tuple<T1, Types...>: __RecursionPair<T1, typename Tuple<Types...>::__PairType>
{
// 我是一個怎樣的__RecursionPair?同樣也是繼承的那個!
typedef __RecursionPair<T1, typename Tuple<Types...>::__PairType> __PairType;
// 建構函式(待實現)
Tuple();
Tuple(const T1 &first, const Types &... Args);
};
那麼,這樣的一個含有多個元素的Tuple,其建構函式又該如何實現呢?通過上文的討論,我們不難發現:不管是什麼樣的Tuple(從只含有一個元素的Tuple到含有很多個元素的Tuple),其父類都是一個可遞迴Pair,而可遞迴Pair也是Pair,其建構函式永遠只需要兩個值(不管是多麼複雜的可遞迴Pair)。所以,我們仍然可以通過直接呼叫父類的建構函式來對任意的Tuple進行構造。我們需要哪兩個值來呼叫複雜的可遞迴Pair的建構函式呢?讓我們繼續進行“找規律”:
- 為了構造一個Tuple<T1, T2, T3>(Arg1, Arg2, Arg3),我們需要構造一個typename Tuple<T1, T2, T3>::__PairType,即一個__RecursionPair<T1, __RecursionPair<T2, T3>>,其中,T1來自於Arg1,而__RecursionPair<T2, T3>可以通過構造一個Tuple<T2, T3>(Arg2, Arg3)得到(因為一個Tuple<T2, T3>就是一個__RecursionPair<T2, T3>)
- 為了構造一個Tuple<T1, T2, T3, T4>(Arg1, Arg2, Arg3, Arg4),我們需要構造一個typename Tuple<T1, T2, T3, T4>::__PairType,即一個__RecursionPair<T1, __RecursionPair<T2, __RecursionPair<T3, T4>>>,其中,T1同樣來自於Arg1,而__RecursionPair<T2, __RecursionPair<T3, T4>>可以通過構造一個Tuple<T2, T3, T4>(Arg2, Arg3, Arg4)得到(因為一個Tuple<T2, T3, T4>就是一個__RecursionPair<T2, __RecursionPair<T3, T4>>)
- ...
- 為了構造一個Tuple<T1, Types...>(first, Args...),我們需要構造一個typename Tuple<T1, Types...>::__PairType,即一個__RecursionPair<T1, typename Tuple<Types...>::__PairType>,其中,T1來自於first,而typename Tuple<Types...>::__PairType可以通過構造一個Tuple<Types...>(Args...)得到(因為一個Tuple<Types...>就是一個typename Tuple<Types...>::__PairType)
我們再一次通過找規律的方法得到了結論!接下來就可以進行程式碼實現了。請看以下示例:
// 預設建構函式
template <typename T1, typename... Types>
Tuple<T1, Types...>::Tuple() = default;
// 只需要呼叫Tuple的“可遞迴Pair形態”(即父類)的建構函式即可
// 構造__PairType的兩個引數分別來自於first與構造Tuple<Types...>(Args...)所得到的一個__RecursionPair
template <typename T1, typename... Types>
Tuple<T1, Types...>::Tuple(const T1 &first, const Types &... Args):
__PairType(first, Tuple<Types...>(Args...)) {}
至此,Tuple的實現中最重要的部分:Tuple的建構函式,也就全部實現完畢了。讓我們立即來試用一下。請看以下示例:
int main()
{
Tuple<int, double, char, string> sampleTuple(1, 2., '3', "4");
cout << __RecursionPairValue<0, decltype(sampleTuple)::__PairType>::__Get(sampleTuple) << endl; // 1
cout << __RecursionPairValue<1, decltype(sampleTuple)::__PairType>::__Get(sampleTuple) << endl; // 2
cout << __RecursionPairValue<2, decltype(sampleTuple)::__PairType>::__Get(sampleTuple) << endl; // 3
cout << __RecursionPairValue<3, decltype(sampleTuple)::__PairType>::__Get(sampleTuple) << endl; // 4
}
由此可見,Tuple的建構函式工作正常(雖然我們暫時還只能通過“可遞迴Pair時代”的工具獲取到其內部的值)。
6.5 Tuple的其他功能的實現
最後,就是一些簡單的周邊功能的實現了。
首先是MakeTuple快捷函式,此函式只需要使用一個可變引數模板封裝Tuple的建構函式即可。請看以下示例:
template <typename... Types>
inline Tuple<Types...> MakeTuple(const Types &... Args)
{
return Tuple<Types...>(Args...);
}
int main()
{
auto sampleTuple = MakeTuple(1, 2., '3');
}
然後是根據“索引值”獲取Tuple的某個位置的型別的類,實現時只需要將全部操作直接委託給我們已經實現的__RecursionPairType即可。請看以下示例:
// 原型
// 通過typename TupleType<N, T>::Type獲取“T[N]”的型別
template <int N, typename T>
struct TupleType;
// 僅當T是一個Tuple時,此類才有意義
template <int N, typename... Types>
struct TupleType<N, Tuple<Types...>>
{
// 使用__RecursionPairType作用於Tuple<Types...>的“可遞迴Pair形態”上,就能獲取“Tuple<Types...>[N]”的型別
typedef typename __RecursionPairType<N, typename Tuple<Types...>::__PairType>::__ValueType Type;
};
int main()
{
Tuple<int, double, char, string> sampleTuple(1, 2., '3', "4");
cout << typeid(TupleType<0, decltype(sampleTuple)>::Type).name() << endl; // int
cout << typeid(TupleType<1, decltype(sampleTuple)>::Type).name() << endl; // double
cout << typeid(TupleType<2, decltype(sampleTuple)>::Type).name() << endl; // char
cout << typeid(TupleType<3, decltype(sampleTuple)>::Type).name() << endl; // string
}
然後是根據“索引值”獲取Tuple的某個位置的值的函式,實現時只需要將全部操作直接委託給我們已經實現的TupleType以及__RecursionPairValue即可。請看以下示例:
// 函式的返回值就是typename TupleType<N, Tuple<Types...>>::Type
template <int N, typename... Types>
inline typename TupleType<N, Tuple<Types...>>::Type &Get(Tuple<Types...> &tupleObj)
{
// 使用__RecursionPairValue作用於Tuple<Types...>的“可遞迴Pair形態”上,就能獲取“tupleObj[N]”的值
return __RecursionPairValue<N, typename Tuple<Types...>::__PairType>::__Get(tupleObj);
}
// 同上
template <int N, typename... Types>
inline const typename TupleType<N, Tuple<Types...>>::Type &Get(const Tuple<Types...> &tupleObj)
{
return __RecursionPairValue<N, typename Tuple<Types...>::__PairType>::__Get(tupleObj);
}
int main()
{
Tuple<int, double, char, string> sampleTuple(1, 2., '3', "4");
cout << Get<0>(sampleTuple) << endl; // 1
cout << Get<1>(sampleTuple) << endl; // 2
cout << Get<2>(sampleTuple) << endl; // 3
cout << Get<3>(sampleTuple) << endl; // 4
}
最後是獲取Tuple的長度的類,直接使用sizeof...(Types)即可。請看以下示例:
// 原型
template <typename T>
struct TupleSize;
// 僅當T是一個Tuple時,此類才有意義
template <typename... Types>
struct TupleSize<Tuple<Types...>>
{
// Tuple的長度顯然就是Tuple的可變模板引數的數量
static constexpr int Size = sizeof...(Types);
};
int main()
{
cout << TupleSize<Tuple<int, double, char, string>>::Size << endl; // 4
}
至此,Tuple的實現也就全部完成了。
6.6 本章後記
Tuple,作為一個看起來已然“突破極限”的高階容器,其背後的核心竟然只是一個“平淡無奇”的Pair,這不得不令人驚訝於基於模板的高階抽象的威力。在Tuple的實現過程中,我們充分利用了模板偏特化,用以描繪出各種不同“形態”的可遞迴Pair;我們也使用了繼承,用以描繪出Tuple與可遞迴Pair的一一對應關係。在這裡,模板與繼承,這兩個“不同世界的產物”,被巧妙的結合在了一起,最終為我們帶來了一場十分精彩的二重奏!
7 模板與高效能運算的極佳配合——表示式模板
表示式模板?什麼?你沒聽說過?那就對了!通過本章的討論,你就會了解到:模板是如何在使用者無感知的前提下,將高效能運算引入我們的程式中的。
7.1 表示式的困境
讓我們從一個看似很簡單的問題開始:
如何實現向量的加法運算?
如果使用STL的array表示向量,不難做出以下實現:
template <typename T, size_t N>
array<T, N> operator+(const array<T, N> &lhs, const array<T, N> &rhs)
{
array<T, N> resArray;
for (int idx = 0; idx < N; idx++) resArray[idx] = lhs[idx] + rhs[idx];
return resArray;
}
int main()
{
array<int, 3> lhs {1, 2, 3}, rhs {4, 5, 6}, res;
res = lhs + rhs;
for (auto val: res) cout << val << endl; // 5 7 9
}
這個實現有什麼問題呢?請看以下示例:
lhs + rhs + lhs + rhs + lhs + rhs + lhs + rhs + lhs + rhs; // 哦!大量的冗餘計算!
在上面這個“10連加”表示式中,operator+函式一共被呼叫了9次,這也就意味著:函式體內的resArray臨時變數被建立、return了9次(假設沒有NRV優化),for迴圈也被執行了9次,而這還只是一次“10連加”所造成的結果。可想而知,在計算量變得越來越大時,這是多麼大的時間耗費!此時,我們不難想到,上述的“10連加”表示式實際上能夠被優化為如下實現:
// 實際上只需要一次函式呼叫
template <typename T, size_t N>
array<T, N> operator+(const array<T, N> &lhs, const array<T, N> &rhs)
{
array<T, N> resArray;
// 實際上也只需要一次迴圈
for (int idx = 0; idx < N; idx++)
{
// 只需要在迴圈體內執行“10連加”即可
resArray[idx] = lhs[idx] + rhs[idx] + lhs[idx] + rhs[idx] + lhs[idx] +
rhs[idx] + lhs[idx] + rhs[idx] + lhs[idx] + rhs[idx];
}
return resArray;
}
可問題是,編譯器就算有能力優化成這樣的實現,其也不能優化成這樣。這是由於C++的表示式語義本就是“積極主動的”,當編譯器看到lhs + rhs...時,其就必須遵守C++的語義規定,立即計算此加法,而“暫且不顧”後續表示式。
看來,編譯器優化是徹底不可能幫得上忙了。這讓我們陷入了困境之中。
7.2 一個天馬行空的想法
既然編譯器幫不上忙,那我們是否能通過某種技術,“繞過”編譯器的這種主動計算呢?如果你的想象力足夠豐富,也許你會有這樣的想法:
能否將表示式看作某種“字串”,這樣,加法就相當於“字串的拼接”呢?而當我們真的需要表示式的結果時,我們可以實現一個對“表示式字串”進行求值的函式來進行求值。
這是一個天馬行空的想法,但基於模板,這個想法是真的可以實現的!這就是本章將要討論的表示式模板技術。
7.3 向量類的實現
首先,讓我們實現一個Array類,用於存放一個向量。請看以下示例:
template <typename T, int N>
class __Array
{
public:
// 建構函式
__Array();
explicit __Array(const T &val);
__Array(initializer_list<T> initializerList);
// operator[]
T &operator[](int idx) { return __data[idx]; }
const T &operator[](int idx) const { return __data[idx]; }
private:
// 一個C語言陣列,用於存放向量
T __data[N];
};
template <typename T, int N>
__Array<T, N>::__Array() = default;
template <typename T, int N>
__Array<T, N>::__Array(const T &val)
{
for (int idx = 0; idx < N; idx++)
{
__data[idx] = val;
}
}
template <typename T, int N>
__Array<T, N>::__Array(initializer_list<T> initializerList)
{
int idx = 0;
for (auto &val: initializerList)
{
__data[idx++] = val;
}
}
int main()
{
__Array<int, 3> lhs {1, 2, 3};
for (int idx = 0; idx < 3; idx++) cout << lhs[idx] << endl; // 1 2 3
}
我們為這個Array實現了預設建構函式,Fill建構函式,initializer_list建構函式,以及operator[]過載。看上去平淡無奇,不是嗎?
7.4 “表示式字串”的實現
接下來,我們就來實現上文中的“表示式字串”(當然,我們不是真的去實現一個特殊的字串)。一個“表示式字串”,如“lhs + rhs”,是由哪幾部分組成的呢?顯然,其是由lhs、“+”以及rhs組成,其中,lhs與rhs代表的是某個值,而“+”代表的是一個動作。如果我們使用兩個變數分別存放lhs與rhs,並使用一個函式表達“+”這一動作,我們就能夠實現出一個“表示式字串”了。而將這些內容封裝進一個模板中,我們也就得到了一個“表示式模板”。請看以下示例:
// 加法表示式模板
template <typename T, typename LExpr, typename RExpr>
class __Plus
{
public:
// 建構函式
__Plus(const LExpr &lhs, const RExpr &rhs);
// 當對這個表示式模板進行[...]運算的時候,就能得到這個表示式模板在某個“索引值”位置上的加法計算的結果
// 也就是說,表示式模板也是某種從外觀上看和向量別無二致的東西
T operator[](int idx) const { return __lhs[idx] + __rhs[idx]; }
private:
// 用於儲存LExpr與RExpr的引用的資料成員
const LExpr &__lhs;
const RExpr &__rhs;
};
template <typename T, typename LExpr, typename RExpr>
__Plus<T, LExpr, RExpr>::__Plus(const LExpr &lhs, const RExpr &rhs):
__lhs(lhs), __rhs(rhs) {}
__Plus的模板引數包含加法的返回值型別T,以及左右值型別LExpr和RExpr;在__Plus中,我們宣告瞭兩個分別指向LExpr和RExpr的引用;而在建構函式中,lhs、rhs被分別繫結至類中的兩個引用上,此時,我們並沒有執行任何加法運算。那麼,什麼時候才執行加法運算呢?從程式碼中不難看出,直到operator[]時,才會真正計算加法。
這是一種利用模板實現的“惰性計算”技術,當加法語義出現時,我們並沒有真的執行加法,而只是執行了一次成本很低的“記錄”操作,我們記錄了執行一次加法所需要的全部資訊:左值、加這個動作、以及右值。僅當真正需要加法的結果時,__Plus才會“在我們強硬的驅使下”計算加法。並且,就算是在這種“強硬的驅使下”,__Plus每次也只會計算一個位置的加法。這就使得__Plus能夠最大程度的規避無意義的加法計算(設想我們進行了一次十萬維向量的加法,但我們只需要知道第五萬維這一個位置的加法結果)。
此外,__Plus在設計上刻意的模仿了__Array的操作,這就使得__Plus也能夠像一個__Array那樣具有“索引值”。這樣做的意義是什麼呢?僅僅是為了方便、美觀嗎?我們將在下一節中揭曉答案。
接下來,讓我們試著使用一下__Plus類,體驗一下這種“惰性計算”技術。請看以下示例:
int main()
{
__Array<int, 3> lhs {1, 2, 3}, rhs {4, 5, 6};
// 儲存了lhs + rhs這個表示式,但不對其進行計算
__Plus<int, __Array<int, 3>, __Array<int, 3>> res(lhs, rhs);
for (int idx = 0; idx < 3; idx++)
{
// 這裡才計算加法
cout << res[idx] << endl;
}
}
看到這裡,也許你會恍然大悟:“哦!這個__Plus和上一章的可遞迴Pair一樣,也是可以遞迴的!”請看以下示例:
int main()
{
__Array<int, 3> lhs {1, 2, 3}, rhs {4, 5, 6};
// 儲存了lhs + rhs + lhs這個表示式,但不對其進行計算
// 可是這也太反人類了吧!
__Plus<int, __Array<int, 3>, __Array<int, 3>> tmp(lhs, rhs);
__Plus<int, __Plus<int, __Array<int, 3>, __Array<int, 3>>, __Array<int, 3>> res(tmp, lhs);
for (int idx = 0; idx < 3; idx++)
{
// 這裡才計算加法
cout << res[idx] << endl;
}
}
我們通過整整兩行的“超長”程式碼,“終於”實現了lhs + rhs + lhs的惰性加法。顯然,這樣的實現是非常“反人類”的,有什麼辦法能對其進行簡化,甚至讓使用者無感知呢?稍加思索就能夠發現,只要使用運算子過載,我們就能把所有這些都隱藏於幕後,只留下lhs + rhs + lhs本身。請看以下示例:
template </* 這裡寫什麼?*/>
/* 這裡寫什麼?*/ operator+(const /* 這裡寫什麼?*/ &lhs, const /* 這裡寫什麼?*/ &rhs)
{
return /* 這裡寫什麼?*/;
}
int main()
{
/* 這裡寫什麼?*/ lhs {1, 2, 3}, rhs {4, 5, 6}, res;
// 最終的效果,太棒了!
res = lhs + rhs + lhs;
for (int idx = 0; idx < 3; idx++)
{
cout << res[idx] << endl;
}
}
對於operator+,其需要同時滿足“__Array + __Array”、“__Array + __Plus”、“__Plus + __Array”、“__Plus + __Plus”等等的“排列組合”(並且,請不要忘了:除了“加”,還有“減乘除”呢!)。這就使得我們難以確定lhs與rhs的型別。難道真的要為每種情況都寫一個operator+過載嗎?請接著往下看。
7.5 再加一層抽象
如何規避這種“排列組合”呢?讓我們開拓一下思維,不難發現:單獨的一個__Array是一個表示式,而__Plus(任意兩個表示式相加的結果)也是一個表示式,並且他們的共性即在於,都是可以基於operator[]進行表示式求值的。至此,解決方案水落石出:我們可以在__Array和__Plus之上再增加一個抽象層,表達“表示式”語義,而__Array和__Plus在此抽象層中並無區別,都是一個可以進行operator[]運算的“表示式”。此時你應該能夠明白:為什麼__Plus要“刻意”模仿__Array的operator[]了。請看以下示例:
template <typename T, int N, typename Expr>
class __Expression
{
public:
// 適用於__Array的建構函式
__Expression();
explicit __Expression(const T &val);
__Expression(initializer_list<T> initializerList);
// 適用於__Plus的建構函式
__Expression(const Expr &expr);
// operator[]直接委託給__expr執行
T &operator[](int idx) { return __expr[idx]; }
T operator[](int idx) const { return __expr[idx]; }
// operator=
template <typename RExpr>
__Expression &operator=(const __Expression<T, N, RExpr> &rhs);
private:
// __expr可能是一個__Array,也可能是一個__Plus
Expr __expr;
};
template <typename T, int N, typename Expr>
__Expression<T, N, Expr>::__Expression() = default;
template <typename T, int N, typename Expr>
__Expression<T, N, Expr>::__Expression(const T &val): __expr(val) {}
template <typename T, int N, typename Expr>
__Expression<T, N, Expr>::__Expression(initializer_list<T> initializerList): __expr(initializerList) {}
template <typename T, int N, typename Expr>
__Expression<T, N, Expr>::__Expression(const Expr &expr): __expr(expr) {}
// operator=直接委託給__expr執行
// 直到operator=發生時,rhs才會真正被計算
template <typename T, int N, typename Expr>
template <typename RhsExpr>
__Expression<T, N, Expr> &__Expression<T, N, Expr>::operator=(
const __Expression<T, N, RhsExpr> &rhs)
{
for (int idx = 0; idx < N; idx++)
{
// 計算rhs[idx]的值,並賦值給左值
__expr[idx] = rhs[idx];
}
return *this;
}
讓我們來分析這一實現:既然我們需要將__Array和__Plus都抽象為一個表示式,那麼我們就可以增加一個模板引數Expr,用以標明這個__Expression到底是什麼(是__Array還是__Plus)。由於Expr既可以是__Array又可以是__Plus,我們就需要實現多個建構函式,使得這兩種型別的值都可以在__Expression中構造。所以,我們實現了三個和__Array的三個建構函式功能一致的建構函式,以及可以使用一個Expr作為引數的建構函式。然後,我們將operator[]和operator=都直接委託給__expr執行。
顯然,當使用者在使用的時候,__Expression的Expr模板引數必須是__Array,所以我們可以宣告一個固定了Expr模板引數的模板,作為面向使用者的Array介面。請看以下示例:
// 最終面向使用者的Array介面
template <typename T, int N>
using Array = __Expression<T, N, __Array<T, N>>;
同時,作為實現者,我們也可以創造一些更復雜的Expr模板引數。所以,就讓我們來實現上一節中未能實現的operator+吧。請看以下示例:
// __Expression<T, N, LExpr> + __Expression<T, N, RExpr>的結果是一個新的__Expression
// 其第一、二模板引數不變,第三模板引數是LExpr + RExpr的結果,即__Plus<T, LExpr, RExpr>
template <typename T, int N, typename LExpr, typename RExpr>
inline __Expression<T, N, __Plus<T, LExpr, RExpr>> operator+(
const __Expression<T, N, LExpr> &lhs, const __Expression<T, N, RExpr> &rhs)
{
// 用lhs與rhs的__expr,構造出一個__Plus
// 再用這個__Plus,構造出一個新的__Expression(使用的是__Expression的第四建構函式)
return __Expression<T, N, __Plus<T, LExpr, RExpr>>(
__Plus<T, LExpr, RExpr>(lhs.__expr, rhs.__expr));
}
看上去很複雜的樣子?讓我們來分析一下。首先,由於我們並不知道lhs和rhs的Expr分別是什麼(二者都可能是__Array,如果這是一個“新的”Array;或__Plus,如果這已經是一個表示式),所以我們需要兩個模板引數LExpr與RExpr,以分別代表lhs和rhs的Expr型別;但同時我們知道,只有相同型別的Array之間可以進行運算,所以我們只需要一套T與N即可。所以,兩個形參分別是const Array<T, N, LExpr> &lhs與const Array<T, N, RExpr> &rhs。
返回值是什麼呢?不難發現,當__Expression<T, N, LExpr>與__Expression<T, N, RExpr>相加後,結果仍然是一個新的__Expression,而真正需要相加的其實是LExpr與RExpr,且相加的結果是__Plus<T, LExpr, RExpr>(事實上,相加的結果也可以就是__Plus<T, __Expression<T, N, LExpr>, __Expression<T, N, RExpr>>,你一定不難想出其中原因。但很明顯,這樣的程式碼也實在是太長,太反人類了),故返回值的型別就是一個Expr模板引數為__Plus<T, LExpr, RExpr>的__Expression,即__Expression<T, N, __Plus<T, LExpr, RExpr>>,而實際的返回值需要先使用lhs與rhs的__expr,構造出一個__Plus,再用這個__Plus,構造出一個新的__Expression。這裡使用的是__Expression的第四建構函式。
現在,讓我們試著使用一下我們剛剛實現的Array(請注意:如果你現在就實際編譯以下這段程式碼,請去除__Expression的__expr的private限定,或為operator+函式進行友元授權)。請看以下示例:
int main()
{
// lhs,rhs,res的實際型別都是__Expression<int, 3, __Array<int, 3>>
Array<int, 3> lhs {1, 2, 3}, rhs {4, 5, 6}, res;
// “什麼都不做”(不會進行實際的運算)
lhs + rhs + lhs;
// lhs + rhs...的型別是__Expression<int, 3, __Plus<__Array<int, 3>, __Array<int, 3>>>
// ... + lhs的型別是__Expression<int, 3, __Plus<__Plus<__Array<int, 3>, __Array<int, 3>>, __Array<int, 3>>>
// 直到operator=發生時,才會進行實際的運算
res = lhs + rhs + lhs;
for (int idx = 0; idx < 3; idx++)
{
// 此時,res[idx]就是一次指標運算
cout << res[idx] << endl;
}
}
觀察上述程式碼不難發現,我們僅僅才做了兩次加法,__Expression的型別就已經“長的沒法看”了。但是事實上,我們根本就不用關心__Expression的型別究竟是什麼,而只需要牢記以下兩點即可:
- 不管進行多少次計算,或一次計算都還沒有進行(即一個Array),我們所操作的都是一個(可能具有很複雜型別的)__Expression
- 對__Expression進行operator[],就相當於對__Expression中的__expr進行operator[];而__expr無非只有兩種情況:其要麼是一個__Array,此時,operator[]就相當於一次陣列取值;要麼是一個__Plus,此時,operator[]就相當於遞迴地呼叫lhs[idx] + rhs[idx],直到lhs與rhs都不再是一個__Plus為止
由此可見,__Expression一方面為使用者提供了對於表示式模板的存在無感知的Array類,另一方面又絲毫不丟失其作為表示式模板的功能,實在是一個優秀的“左右開弓”類;另一方面,由於我們的__Array與__Plus均實現了統一的operator[]介面,這就使得__Expression能夠“自適應地”最終實現對其自身的求值。以上種種,都能夠為我們展現出“抽象”這一思想的精彩之處。
7.6 讓標量也加入進來
在數學中,一個向量不僅可以和另一個向量相加,還可以和一個標量(即一個T型別的值)相加。本節我們就來實現這一功能。
如何讓標量也加入我們的“__Expression大家族”中呢?沒錯,關鍵就在於我們在上幾節已經“老生常談”的operator[]。雖然標量根本就沒有operator[]這一概念,我們也可以“強行的”為其新增這一概念,以使其適配__Expression的需要。請看以下示例:
// 為標量提供的封裝類,從而使得標量也能夠適配__Expression
template <typename T>
class __Scalar
{
public:
// 建構函式
__Scalar(T val);
// 強行為標量提供一個“莫名其妙的”operator[]
// 不管索引值是多少(事實上我們根本就無視了這個索引值),都返回val
T operator[](int) const { return __val; }
private:
// 使用一個T型別的資料成員存放建構函式中的val
T __val;
};
template <typename T>
__Scalar<T>::__Scalar(T val): __val(val) {}
此時,__Expression的Expr模板引數就不僅可以是__Array或__Plus,還可以是__Scalar了。
讓我們繼續,實現適用於__Expression與__Scalar之間的加法的運算子過載。請看以下示例:
// __Expression<T, N, LExpr> + T
// 其結果為__Plus<T, LExpr, __Scalar<T>>
template <typename T, int N, typename LExpr>
inline __Expression<T, N, __Plus<T, LExpr, __Scalar<T>>> operator+(
const __Expression<T, N, LExpr> &lhs, const T &rhs)
{
// 先使用rhs構造出一個__Scalar<T>
// 再使用lhs的__expr和__Scalar<T>構造出一個__Plus
// 最後使用一個__Plus構造出一個新的__Expression
return __Expression<T, N, __Plus<T, LExpr, __Scalar<T>>>(
__Plus<T, LExpr, __Scalar<T>>(lhs.__expr, __Scalar<T>(rhs)));
}
// T + __Expression<T, N, RExpr>
// 其結果為:__Plus<T, __Scalar<T>, RExpr>
template <typename T, int N, typename RExpr>
inline __Expression<T, N, __Plus<T, __Scalar<T>, RExpr>> operator+(
const T &lhs, const __Expression<T, N, RExpr> &rhs)
{
// 先使用lhs構造出一個__Scalar<T>
// 再使用__Scalar<T>和rhs的__expr構造出一個__Plus
// 最後使用一個__Plus構造出一個新的__Expression
return __Expression<T, N, __Plus<T, __Scalar<T>, RExpr>>(
__Plus<T, __Scalar<T>, RExpr>(__Scalar<T>(lhs), rhs.__expr));
}
在我們試用這一功能前,其實還有一件事是沒有完成的。請設想:當我們寫下“lhs + 1”這一表示式時,這裡的“1”顯然是一個臨時量,而如果使用我們現在所實現的__Plus,那麼這個臨時量“1”將被存入一個引用中。這將立即導致“懸掛引用”的發生!所以,我們需要實現一個簡單的Traits類,在面對一個__Scalar時,將__Plus中的資料成員型別,從引用型別自動切換至值型別。這一Traits的實現非常簡單,請看以下示例:
// 不管T是是什麼,都萃取出一個const T &型別...
template <typename T>
struct __ScalarTypeTraits
{
typedef const T &__Type;
};
// ...但是,如果T是一個__Scalar<T>型別,則萃取出一個__Scalar<T>型別
template <typename T>
struct __ScalarTypeTraits<__Scalar<T>>
{
typedef __Scalar<T> __Type;
};
有了這個Traits,我們就可以使用這個Traits改進我們的__Plus類了。請看以下示例:
template <typename T, typename LExpr, typename RExpr>
class __Plus
{
// ...
private:
// 原實現:
// const LExpr &__lhs;
// const RExpr &__rhs;
// 改進後的實現:
// 在LExpr(或RExpr)為__Scalar<LExpr>(或__Scalar<RExpr>)時
// __Type將從引用型別自動切換至值型別
typename __ScalarTypeTraits<LExpr>::__Type __lhs;
typename __ScalarTypeTraits<RExpr>::__Type __rhs;
};
至此,我們就可以將標量也加入到表示式模板中了(請注意:如果你現在就實際編譯以下這段程式碼,請去除__Expression的__expr的private限定,或為operator+函式進行友元授權)。請看以下示例:
int main()
{
Array<int, 3> lhs {1, 2, 3}, rhs {4, 5, 6}, res;
// 加入標量
res = lhs + rhs + lhs + 1;
// 7 10 13
for (int idx = 0; idx < 3; idx++)
{
cout << res[idx] << endl;
}
}
本節中,我們通過一個對標量的簡單的封裝類,使得標量也能夠加入到表示式模板中;同時,為了避免標量臨時量所引發的“懸掛引用”問題,我們又實現了一個簡單的Traits類,用於在面對標量時自動將表示式模板中的引用型別切換為值型別。
至此,表示式模板的全部技術就都討論完畢了。下一節,我們將最終給出表示式模板的完整實現。
7.7 完整的實現
由於本章的程式碼較為分散,且我們仍有很多重複性的程式碼沒有於上文中給出。故本節中,我們將給出表示式模板的完整實現。主要包含以下幾點新增內容:
- 我們不僅需要實現operator+,還要實現operator-、operator*、operator/和operator%。這些實現都是operator+的簡單重複
- 我們需要為所有的operator函式新增友元授權
- 我們需要為__Expression實現operator<<過載(實現方案與operator=一致)
請看以下示例:
// “夢開始的地方”:__Array類
template <typename T, int N>
class __Array
{
public:
// 建構函式
__Array();
explicit __Array(const T &val);
__Array(initializer_list<T> initializerList);
// operator[]
T &operator[](int idx) { return __data[idx]; }
const T &operator[](int idx) const { return __data[idx]; }
private:
// 資料成員
T __data[N];
};
template <typename T, int N>
__Array<T, N>::__Array() = default;
template <typename T, int N>
__Array<T, N>::__Array(const T &val)
{
for (int idx = 0; idx < N; idx++)
{
__data[idx] = val;
}
}
template <typename T, int N>
__Array<T, N>::__Array(initializer_list<T> initializerList)
{
int idx = 0;
for (auto &val: initializerList)
{
__data[idx++] = val;
}
}
// 標量介面卡
template <typename T>
class __Scalar
{
public:
// 建構函式
__Scalar(T val);
// operator[]
T operator[](int) const { return __val; }
private:
// 資料成員
T __val;
};
template <typename T>
__Scalar<T>::__Scalar(T val): __val(val) {}
// 標量值型別萃取器
template <typename T>
struct __ScalarTypeTraits
{
typedef const T &__Type;
};
template <typename T>
struct __ScalarTypeTraits<__Scalar<T>>
{
typedef __Scalar<T> __Type;
};
// 加法表示式模板
template <typename T, typename LExpr, typename RExpr>
class __Plus
{
public:
// 建構函式
__Plus(const LExpr &lhs, const RExpr &rhs);
// operator[]
T operator[](int idx) const { return __lhs[idx] + __rhs[idx]; }
private:
// 資料成員
typename __ScalarTypeTraits<LExpr>::__Type __lhs;
typename __ScalarTypeTraits<RExpr>::__Type __rhs;
};
// 減法表示式模板
template <typename T, typename LExpr, typename RExpr>
class __Minus
{
public:
// 建構函式
__Minus(const LExpr &lhs, const RExpr &rhs);
// operator[]
T operator[](int idx) const { return __lhs[idx] - __rhs[idx]; }
private:
// 資料成員
typename __ScalarTypeTraits<LExpr>::__Type __lhs;
typename __ScalarTypeTraits<RExpr>::__Type __rhs;
};
// 乘法表示式模板
template <typename T, typename LExpr, typename RExpr>
class __Multiplies
{
public:
// 建構函式
__Multiplies(const LExpr &lhs, const RExpr &rhs);
// operator[]
T operator[](int idx) const { return __lhs[idx] * __rhs[idx]; }
private:
// 資料成員
typename __ScalarTypeTraits<LExpr>::__Type __lhs;
typename __ScalarTypeTraits<RExpr>::__Type __rhs;
};
// 除法表示式模板
template <typename T, typename LExpr, typename RExpr>
class __Divides
{
public:
// 建構函式
__Divides(const LExpr &lhs, const RExpr &rhs);
// operator[]
T operator[](int idx) const { return __lhs[idx] / __rhs[idx]; }
private:
// 資料成員
typename __ScalarTypeTraits<LExpr>::__Type __lhs;
typename __ScalarTypeTraits<RExpr>::__Type __rhs;
};
// 取模表示式模板
template <typename T, typename LExpr, typename RExpr>
class __Modulus
{
public:
// 建構函式
__Modulus(const LExpr &lhs, const RExpr &rhs);
// operator[]
T operator[](int idx) const { return __lhs[idx] % __rhs[idx]; }
private:
// 資料成員
typename __ScalarTypeTraits<LExpr>::__Type __lhs;
typename __ScalarTypeTraits<RExpr>::__Type __rhs;
};
template <typename T, typename LExpr, typename RExpr>
__Plus<T, LExpr, RExpr>::__Plus(const LExpr &lhs, const RExpr &rhs): __lhs(lhs), __rhs(rhs) {}
template <typename T, typename LExpr, typename RExpr>
__Minus<T, LExpr, RExpr>::__Minus(const LExpr &lhs, const RExpr &rhs): __lhs(lhs), __rhs(rhs) {}
template <typename T, typename LExpr, typename RExpr>
__Multiplies<T, LExpr, RExpr>::__Multiplies(const LExpr &lhs, const RExpr &rhs): __lhs(lhs), __rhs(rhs) {}
template <typename T, typename LExpr, typename RExpr>
__Divides<T, LExpr, RExpr>::__Divides(const LExpr &lhs, const RExpr &rhs): __lhs(lhs), __rhs(rhs) {}
template <typename T, typename LExpr, typename RExpr>
__Modulus<T, LExpr, RExpr>::__Modulus(const LExpr &lhs, const RExpr &rhs): __lhs(lhs), __rhs(rhs) {}
// __Expression表示式類
template <typename T, int N, typename Expr>
class __Expression
{
public:
// 建構函式
__Expression();
explicit __Expression(const T &val);
__Expression(initializer_list<T> initializerList);
__Expression(const Expr &expr);
// operator[]
T &operator[](int idx) { return __expr[idx]; }
T operator[](int idx) const { return __expr[idx]; }
// operator=
template <typename RExpr>
__Expression &operator=(const __Expression<T, N, RExpr> &rhs);
private:
// 資料成員
Expr __expr;
// 以下均為友元授權
// operator+
template <typename T_, int N_, typename LExpr, typename RExpr>
friend inline __Expression<T_, N_, __Plus<T_, LExpr, RExpr>> operator+(
const __Expression<T_, N_, LExpr> &lhs, const __Expression<T_, N_, RExpr> &rhs);
template <typename T_, int N_, typename LExpr>
friend inline __Expression<T_, N_, __Plus<T_, LExpr, __Scalar<T_>>> operator+(
const __Expression<T_, N_, LExpr> &lhs, const T_ &rhs);
template <typename T_, int N_, typename RExpr>
friend inline __Expression<T_, N_, __Plus<T_, __Scalar<T_>, RExpr>> operator+(
const T_ &lhs, const __Expression<T_, N_, RExpr> &rhs);
// operator-
template <typename T_, int N_, typename LExpr, typename RExpr>
friend inline __Expression<T_, N_, __Minus<T_, LExpr, RExpr>> operator-(
const __Expression<T_, N_, LExpr> &lhs, const __Expression<T_, N_, RExpr> &rhs);
template <typename T_, int N_, typename LExpr>
friend inline __Expression<T_, N_, __Minus<T_, LExpr, __Scalar<T_>>> operator-(
const __Expression<T_, N_, LExpr> &lhs, const T_ &rhs);
template <typename T_, int N_, typename RExpr>
friend inline __Expression<T_, N_, __Minus<T_, __Scalar<T_>, RExpr>> operator-(
const T_ &lhs, const __Expression<T_, N_, RExpr> &rhs);
// operator*
template <typename T_, int N_, typename LExpr, typename RExpr>
friend inline __Expression<T_, N_, __Multiplies<T_, LExpr, RExpr>> operator*(
const __Expression<T_, N_, LExpr> &lhs, const __Expression<T_, N_, RExpr> &rhs);
template <typename T_, int N_, typename LExpr>
friend inline __Expression<T_, N_, __Multiplies<T_, LExpr, __Scalar<T_>>> operator*(
const __Expression<T_, N_, LExpr> &lhs, const T_ &rhs);
template <typename T_, int N_, typename RExpr>
friend inline __Expression<T_, N_, __Multiplies<T_, __Scalar<T_>, RExpr>> operator*(
const T_ &lhs, const __Expression<T_, N_, RExpr> &rhs);
// operator/
template <typename T_, int N_, typename LExpr, typename RExpr>
friend inline __Expression<T_, N_, __Divides<T_, LExpr, RExpr>> operator/(
const __Expression<T_, N_, LExpr> &lhs, const __Expression<T_, N_, RExpr> &rhs);
template <typename T_, int N_, typename LExpr>
friend inline __Expression<T_, N_, __Divides<T_, LExpr, __Scalar<T_>>> operator/(
const __Expression<T_, N_, LExpr> &lhs, const T_ &rhs);
template <typename T_, int N_, typename RExpr>
friend inline __Expression<T_, N_, __Divides<T_, __Scalar<T_>, RExpr>> operator/(
const T_ &lhs, const __Expression<T_, N_, RExpr> &rhs);
// operator%
template <typename T_, int N_, typename LExpr, typename RExpr>
friend inline __Expression<T_, N_, __Modulus<T_, LExpr, RExpr>> operator%(
const __Expression<T_, N_, LExpr> &lhs, const __Expression<T_, N_, RExpr> &rhs);
template <typename T_, int N_, typename LExpr>
friend inline __Expression<T_, N_, __Modulus<T_, LExpr, __Scalar<T_>>> operator%(
const __Expression<T_, N_, LExpr> &lhs, const T_ &rhs);
template <typename T_, int N_, typename RExpr>
friend inline __Expression<T_, N_, __Modulus<T_, __Scalar<T_>, RExpr>> operator%(
const T_ &lhs, const __Expression<T_, N_, RExpr> &rhs);
};
template <typename T, int N, typename Expr>
__Expression<T, N, Expr>::__Expression() = default;
template <typename T, int N, typename Expr>
__Expression<T, N, Expr>::__Expression(const T &val): __expr(val) {}
template <typename T, int N, typename Expr>
__Expression<T, N, Expr>::__Expression(initializer_list<T> initializerList): __expr(initializerList) {}
template <typename T, int N, typename Expr>
__Expression<T, N, Expr>::__Expression(const Expr &expr): __expr(expr) {}
template <typename T, int N, typename Expr>
template <typename RhsExpr>
__Expression<T, N, Expr> &__Expression<T, N, Expr>::operator=(
const __Expression<T, N, RhsExpr> &rhs)
{
for (int idx = 0; idx < N; idx++)
{
__expr[idx] = rhs[idx];
}
return *this;
}
// 運算子過載
// __Expression + __Expression
template <typename T, int N, typename LExpr, typename RExpr>
inline __Expression<T, N, __Plus<T, LExpr, RExpr>> operator+(
const __Expression<T, N, LExpr> &lhs, const __Expression<T, N, RExpr> &rhs)
{
return __Expression<T, N, __Plus<T, LExpr, RExpr>>(
__Plus<T, LExpr, RExpr>(lhs.__expr, rhs.__expr));
}
// __Expression + __Scalar
template <typename T, int N, typename LExpr>
inline __Expression<T, N, __Plus<T, LExpr, __Scalar<T>>> operator+(
const __Expression<T, N, LExpr> &lhs, const T &rhs)
{
return __Expression<T, N, __Plus<T, LExpr, __Scalar<T>>>(
__Plus<T, LExpr, __Scalar<T>>(lhs.__expr, __Scalar<T>(rhs)));
}
// __Scalar + __Expression
template <typename T, int N, typename RExpr>
inline __Expression<T, N, __Plus<T, __Scalar<T>, RExpr>> operator+(
const T &lhs, const __Expression<T, N, RExpr> &rhs)
{
return __Expression<T, N, __Plus<T, __Scalar<T>, RExpr>>(
__Plus<T, __Scalar<T>, RExpr>(__Scalar<T>(lhs), rhs.__expr));
}
// __Expression - __Expression
template <typename T, int N, typename LExpr, typename RExpr>
inline __Expression<T, N, __Minus<T, LExpr, RExpr>> operator-(
const __Expression<T, N, LExpr> &lhs, const __Expression<T, N, RExpr> &rhs)
{
return __Expression<T, N, __Minus<T, LExpr, RExpr>>(
__Minus<T, LExpr, RExpr>(lhs.__expr, rhs.__expr));
}
// __Expression - __Scalar
template <typename T, int N, typename LExpr>
inline __Expression<T, N, __Minus<T, LExpr, __Scalar<T>>> operator-(
const __Expression<T, N, LExpr> &lhs, const T &rhs)
{
return __Expression<T, N, __Minus<T, LExpr, __Scalar<T>>>(
__Minus<T, LExpr, __Scalar<T>>(lhs.__expr, __Scalar<T>(rhs)));
}
// __Scalar - __Expression
template <typename T, int N, typename RExpr>
inline __Expression<T, N, __Minus<T, __Scalar<T>, RExpr>> operator-(
const T &lhs, const __Expression<T, N, RExpr> &rhs)
{
return __Expression<T, N, __Minus<T, __Scalar<T>, RExpr>>(
__Minus<T, __Scalar<T>, RExpr>(__Scalar<T>(lhs), rhs.__expr));
}
// __Expression * __Expression
template <typename T, int N, typename LExpr, typename RExpr>
inline __Expression<T, N, __Multiplies<T, LExpr, RExpr>> operator*(
const __Expression<T, N, LExpr> &lhs, const __Expression<T, N, RExpr> &rhs)
{
return __Expression<T, N, __Multiplies<T, LExpr, RExpr>>(
__Multiplies<T, LExpr, RExpr>(lhs.__expr, rhs.__expr));
}
// __Expression * __Scalar
template <typename T, int N, typename LExpr>
inline __Expression<T, N, __Multiplies<T, LExpr, __Scalar<T>>> operator*(
const __Expression<T, N, LExpr> &lhs, const T &rhs)
{
return __Expression<T, N, __Multiplies<T, LExpr, __Scalar<T>>>(
__Multiplies<T, LExpr, __Scalar<T>>(lhs.__expr, __Scalar<T>(rhs)));
}
// __Scalar * __Expression
template <typename T, int N, typename RExpr>
inline __Expression<T, N, __Multiplies<T, __Scalar<T>, RExpr>> operator*(
const T &lhs, const __Expression<T, N, RExpr> &rhs)
{
return __Expression<T, N, __Multiplies<T, __Scalar<T>, RExpr>>(
__Multiplies<T, __Scalar<T>, RExpr>(__Scalar<T>(lhs), rhs.__expr));
}
// __Expression / __Expression
template <typename T, int N, typename LExpr, typename RExpr>
inline __Expression<T, N, __Divides<T, LExpr, RExpr>> operator/(
const __Expression<T, N, LExpr> &lhs, const __Expression<T, N, RExpr> &rhs)
{
return __Expression<T, N, __Divides<T, LExpr, RExpr>>(
__Divides<T, LExpr, RExpr>(lhs.__expr, rhs.__expr));
}
// __Expression / __Scalar
template <typename T, int N, typename LExpr>
inline __Expression<T, N, __Divides<T, LExpr, __Scalar<T>>> operator/(
const __Expression<T, N, LExpr> &lhs, const T &rhs)
{
return __Expression<T, N, __Divides<T, LExpr, __Scalar<T>>>(
__Divides<T, LExpr, __Scalar<T>>(lhs.__expr, __Scalar<T>(rhs)));
}
// __Scalar / __Expression
template <typename T, int N, typename RExpr>
inline __Expression<T, N, __Divides<T, __Scalar<T>, RExpr>> operator/(
const T &lhs, const __Expression<T, N, RExpr> &rhs)
{
return __Expression<T, N, __Divides<T, __Scalar<T>, RExpr>>(
__Divides<T, __Scalar<T>, RExpr>(__Scalar<T>(lhs), rhs.__expr));
}
// __Expression % __Expression
template <typename T, int N, typename LExpr, typename RExpr>
inline __Expression<T, N, __Modulus<T, LExpr, RExpr>> operator%(
const __Expression<T, N, LExpr> &lhs, const __Expression<T, N, RExpr> &rhs)
{
return __Expression<T, N, __Modulus<T, LExpr, RExpr>>(
__Modulus<T, LExpr, RExpr>(lhs.__expr, rhs.__expr));
}
// __Expression % __Scalar
template <typename T, int N, typename LExpr>
inline __Expression<T, N, __Modulus<T, LExpr, __Scalar<T>>> operator%(
const __Expression<T, N, LExpr> &lhs, const T &rhs)
{
return __Expression<T, N, __Modulus<T, LExpr, __Scalar<T>>>(
__Modulus<T, LExpr, __Scalar<T>>(lhs.__expr, __Scalar<T>(rhs)));
}
// __Scalar % __Expression
template <typename T, int N, typename RExpr>
inline __Expression<T, N, __Modulus<T, __Scalar<T>, RExpr>> operator%(
const T &lhs, const __Expression<T, N, RExpr> &rhs)
{
return __Expression<T, N, __Modulus<T, __Scalar<T>, RExpr>>(
__Modulus<T, __Scalar<T>, RExpr>(__Scalar<T>(lhs), rhs.__expr));
}
// 適用於__Expression的operator<<過載
template <typename T, int N, typename Expr>
ostream &operator<<(ostream &os, const __Expression<T, N, Expr> &expressionObj)
{
os << '[';
if (N)
{
os << expressionObj[0];
for (int idx = 1; idx < N; idx++)
{
os << ", " << expressionObj[idx];
}
}
os << ']';
return os;
}
// 最終供使用者使用的Array類
template <typename T, int N>
using Array = __Expression<T, N, __Array<T, N>>;
int main()
{
// 預設建構函式
Array<int, 3> sampleArrayA; // [?, ?, ?]
// Fill建構函式
Array<int, 3> sampleArrayB(0); // [0, 0, 0]
// initializer_list建構函式
Array<int, 3> sampleArrayC {1, 2, 3}; // [1, 2, 3]
// 拷貝建構函式
Array<int, 3> sampleArrayD(sampleArrayC); // [1, 2, 3]
// 四則運算
Array<int, 3> arrayA {1, 2, 3}, arrayB {4, 5, 6}, resArray;
// operator<<
cout << arrayA << endl; // [1, 2, 3]
cout << arrayB << endl; // [4, 5, 6]
cout << resArray << endl; // [?, ?, ?]
// 惰性計算
arrayA + arrayB; // 什麼都不做!
cout << arrayA + arrayB << endl; // [5, 7, 9]
// operator+
resArray = 2 + arrayA + arrayB + 2;
cout << resArray << endl; // [9, 11, 13]
// operator-
resArray = 2 - arrayA - arrayB - 2;
cout << resArray << endl; // [-5, -7, -9]
// operator*
resArray = 2 * arrayA * arrayB * 2;
cout << resArray << endl; // [16, 40, 72]
// operator/
resArray = 200 / arrayB / arrayA / 2;
cout << resArray << endl; // [25, 10, 5]
// operator%
resArray = 17 % arrayA % arrayB % 5;
cout << resArray << endl; // [0, 1, 2]
}
至此,表示式模板的實現也就全部完成了。
7.8 本章後記
表示式模板,作為一種服務於高效能運算場合的模板技術,被廣泛應用於各種線性代數庫中(如著名的Eigen庫)。表示式模板的精彩之處在於:其充分利用了多級模板抽象所帶來的更大的抽象能力,將表示式模板中產生的重重複雜型別完全隱藏於程式碼實現中,使得使用者既能夠像書寫普通表示式那樣進行公式的編碼,亦能夠享受到表示式模板所帶來的極佳效率。模板在高效能運算領域的這一應用,既為模板技術再添精彩一筆,也為我們的故事畫上了句號...
8 模板是黑魔法嗎?——後記
模板,最早於上世紀90年代被引入至C++,此後的多年內,模板技術迅速發展,促使了大量與之相關的程式設計技術的出現與成熟,並直接導致了STL的出現。在模板出現的幾年後,一份“通過報錯資訊計算質數”的程式程式碼徹底重新整理了人們對於模板的認知,這直接導致了“模板超程式設計”這一概念的出現(本文作者原想以此份程式碼的解讀作為後記前的最後一章,但這份程式碼已年代久遠,已經不能在作者的GCC編譯器上得到理想的效果了,故抱憾作罷)。C++98標準的確立,催生了包括《C++ Templates: The Complete Guide》、《Modern C++ Design》等大量優秀著作的產生。正如《Modern C++ Design》一書的中文譯序中所言,這些書籍所講述的技術,使得我們不再認為模板只是一位“戴上了新帽子”的舊朋友。閱讀這些書籍,一定能讓你對模板這一技術具有更深入,更全面的認知。
模板是黑魔法嗎?類似的問題還有很多(例如:Python的元類是黑魔法嗎?)。如果你是一個狂熱的模板愛好者,你一定會回答:不!模板是很有用的工具!而如果你對模板不是很感興趣,或僅僅是因為在學習模板的過程中感到吃力,也許你會對模板的實用性存疑。人對某一個學術領域,某一項技術的認知,必將隨著學識、心態、技術本身的興衰等因素的變化而不斷的發生著變化。所以這個問題地答案,也就等著讀者你自己去不斷的回答了。
注:本文中的部分程式已完整實現於本文作者的Github上,列舉如下: