前面的博文介紹了模板的基礎,深入模板特性,模板和設計的一些內容。從這篇開始,我們介紹一些高級模板設計,開發某些相對較小、並且互相獨立的功能,而且對於這些簡單功能而言,模板是最好的實現方法:
(1)一個用於類型區分的框架;
(2)智能指針
(3)tuple
(4)仿函數
------------------------------------------------------------------------------------------------------------
第19章 類型區分
本章主要介紹用模板實現對類型的辨識,判斷其是內建類型、指針類型、class類型或者其他類型中的哪一種。
------------------------------------------------------------------------------------------------------------
19.1 辨別基本類型
缺省情況下,我們一方面假定一個類型不是一個基本類型,另一方面我們為所有的基本類型都特化給模板:
// types/type1.hpp // 基本模板:一般情況下T不是基本類型 template <typename T> class IsFundaT { public: enum { Yes = 0, No = 1 }; }; // 用於特化基本類型的宏 #define MK_FUNDA_TYPE(T) \ template<> class IsFundaT<T> { \ public: \ enum { Yes = 1, No = 0 }; \ }; MK_FUNDA_TYPE(void) MK_FUNDA_TYPE(bool) MK_FUNDA_TYPE(char) MK_FUNDA_TYPE(signed char) MK_FUNDA_TYPE(unsigned char) MK_FUNDA_TYPE(wchar_t) MK_FUNDA_TYPE(signed short) MK_FUNDA_TYPE(unsigned short) MK_FUNDA_TYPE(signed int) MK_FUNDA_TYPE(unsigned int) MK_FUNDA_TYPE(signed long) MK_FUNDA_TYPE(unsigned long) #if LONGLONG_EXISTS MK_FUNDA_TYPE(signed long long) MK_FUNDA_TYPE(unsigned long long) #endif // LONGLONG_EXISTS MK_FUNDA_TYPE(float) MK_FUNDA_TYPE(double) MK_FUNDA_TYPE(long double) #undef MK_FUNDA_TYPE
19.2 辨別組合類型
組合類型是指一些構造自其他類型的類型。簡單的組合類型包括:普通類型、指針類型、引用類型和數組類型。它們都是構造自單一的基本類型。同時,class類型和函數類型也是組合類型,但這些組合類型通常都會涉及到多種類型(例如參數或者成員的類型)。在此,我們先考慮簡單的組合類型;另外,我們還將使用局部特化對簡單的組合類型進行區分。接下來,我們將定義一個trait類,用於描述簡單的組合類型;而class類型和枚舉類型將在最后考慮。
// types/type2.hpp template <typename T> class CompoundT // 基本模板 { public: enum { IsPtrT = 0, IsRefT = 0, IsArrayT = 0, IsFuncT = 0, IsPtrMemT = 0 }; typedef T BaseT; typedef T BottomT; typedef CompoundT<void> ClassT; };
成員類型BaseT指的是:用於構造模板參數類型T的(直接)類型;而BottomT指的是最終去除指針、引用和數組之后的、用於構造T的原始類型。例如,如果T是int**,那么BaseT將是int*,而BottomT將會是int類型。對於成員指針類型,BaseT將會是成員的類型,而ClassT將會是成員所屬的類的類型。例如,如果T是一個類型為int(X::*)()的成員函數指針,那么BaseT將會是函數類型int(),而ClassT的類型則為X。如果T不是成員指針類型,那么ClassT將會是CompoundT<void>(這個選擇並不是必須的,也可以使用一個noclass來作為ClassT)。
其中,針對指針和引用的局部特化是相當直接的:
// types/type3.hpp template <typename T> class CompoundT<T&> { public: enum { IsPtrT = 0, IsRefT = 1, IsArrayT = 0, IsFuncT = 0, IsPtrMemT = 0 }; typedef T BaseT; typedef typename CompoundT<T>::BottomT BottomT; typedef CompoundT<void> ClassT; }; template <typename T> class CompoundT<T*> { public: enum { IsPtrT = 1, IsRefT = 0, IsArrayT = 0, IsFuncT = 0, IsPtrMemT = 0 }; typedef T BaseT; typedef typename CompoundT<T>::BottomT BottomT; typedef CompoundT<void> ClassT; };
對於成員指針和數組,我們可能會使用同樣的技術來處理。但是,在下面的代碼中我們將發現,與基本模板相比,這些局部特化將會涉及到更多的模板參數:
// types/type4.hpp #include <stddef.h> template<typename T, size_t N> class CompoundT<T[N]> { public: enum { IsPtrT = 0, IsRefT = 0, IsArrayT = 1, IsFuncT = 0, IsPtrMemT = 0 }; typedef T BaseT; typedef typename CompoundT<T>::BottomT BottomT; typedef CompoundT<void> ClassT; }; template<typename T> class CompoundT<T[]> { public: enum { IsPtrT = 0, IsRefT = 0, IsArrayT = 1, IsFuncT = 0, IsPtrMemT = 0 }; typedef T BaseT; typedef typename CompoundT<T>::BottomT BottomT; typedef CompoundT<void> ClassT; }; template<typename T> class CompoundT<T C::*> { public: enum { IsPtrT = 0, IsRefT = 0, IsArrayT = 0, IsFuncT = 0, IsPtrMemT = 1 }; typedef T BaseT; typedef typename CompoundT<T>::BottomT BottomT; typedef C ClassT; };
19.3 辨別函數類型
書中提供了兩種辨識函數類型的方法,這里只介紹第2種:
method2:使用SFINAE原則的解決方案:
一個重載函數模板的后面可以是一些顯式模板實參;而且對於某些重載函數類型而言,該實參是有效的,但對於其他的重載函數類型,該實參則可能是無效的。實際上,后面使用重載解析對枚舉類型進行辨別的技術也使用到了這種方法。SFINAE原則在這里的主要用處是:(1)找到一種構造,該構造對函數類型是無效的,但是對於其他類型都是有效的;或者完全相反。由於前面我們已經能夠辨別出幾種類型了,所以我們在此可以不再考慮這些(已經可以辨別的)類型。(2)因此,針對上面這種要求,數組類型就是一種有效的構造;因為數組的元素是不能為void值、引用或者函數的。故而可以編寫如下代碼:
template <typename T> class IsFunctionT { private: typedef char One; typedef struct { char a[2]; } Two; template <typename U> static One test ( ... ); template <typename U> static Two test ( U (*)[1] ); // 不理解,下面的IsFunctionT<T>::test<T>(0)怎么匹配? public: enum { Yes = sizeof(IsFunctionT<T>::test<T>(0)) == 1 }; enum { No = !Yes }; };
借助於上面這個模板定義,只有對於那些不能作為數組元素類型的類型,IsFunctionT::Yes才是非零值(即為1)。另外,我們應該知道該方法也有一個不足之處;並非只有函數類型不能作為數組元素類型,引用類型和void類型同樣也不能作為數組元素類型。(3)幸運的是,我們可以通過為引用類型提供局部特化,以及為void類型提供顯式特化,來解決這個不足:
template <typename T> class IsFunctionT<T&> { public: enum { Yes = 0 }; enum { No = !Yes }; }; template <> class IsFunctionT<void> { public: enum { Yes = 0 }; enum { No = !Yes }; }; template <> class IsFunctionT<void const> { public: enum { Yes = 0 }; enum { No = !Yes }; };
至此,我們可以重新改寫基本的CompoundT模板如下:
// types/type6.hpp template <typename T> class IsFunctionT { private: typedef char One; typedef struct { char a[2]; } Two; template <typename U> static One test ( ... ); template <typename U> static Two test ( U (*)[1] ); public: enum { Yes = sizeof(IsFunctionT<T>::test<T>(0)) == 1 }; enum { No = !Yes }; }; template <typename T> class IsFunctionT<T&> { public: enum { Yes = 0 }; enum { No = !Yes }; }; template <> class IsFunctionT<void> { public: enum { Yes = 0 }; enum { No = !Yes }; }; template <> class IsFunctionT<void const> { public: enum { Yes = 0 }; enum { No = !Yes }; }; // 對於void volatile 和 void const volatile類型也是一樣的 ... template <typename T> class CompoundT // 基本模板 { public: enum { IsPtrT = 0, IsRefT = 0, IsArrayT = 0, IsFuncT = IsFunctionT<T>::Yes, IsPtrMemT = 0 }; typedef T BaseT; typedef T BottomT; typedef CompoundT<void> ClassT; };
19.4 運用重載解析辨別枚舉類型
重載解析是一個過程,它會根據函數參數的類型,在多個同名函數中選擇出一個合適的函數。接下來我們將看到,即使沒有進行實際的函數調用,我們也能夠利用重載解析來確定所需要的結果。總之,對於測試某個特殊的隱式轉型是否存在的情況,這種(利用重載解析的)方法是相當有用的。在此,我們將要利用從枚舉類型到整型的隱式轉型:它能夠幫助我們分辨枚舉類型。
先看實現:
// types/type7.hpp struct SizeOverOne { char c[2]; }; template<typename T, bool convert_possible = !CompoundT<T>::IsFuncT && !CompoundT<T>::IsArrayT> class ConsumeUDC { public: //在ConsumeUDC模板中已經強制定義了一個到T的自定義轉型 operator T() const; }; // 到函數類型的轉型是不允許的 // 如果由基本模板得到的convert_possible為false,則匹配此特化;不轉型-->無自定義轉型操作 template<typename T> class ConsumeUDC<T, false> { }; // 到void類型的轉型是不允許的 template <bool convert_possible> class ConsumeUDC<void, convert_possible> { }; char enum_check(bool); char enum_check(char); char enum_check(signed char); char enum_check(unsigned char); char enum_check(wchar_t); char enum_check(signed short); char enum_check(unsigned short); char enum_check(signed int); char enum_check(unsigned int); char enum_check(signed long); char enum_check(unsigned long); #if LONGLONG_EXISTS char enum_check(signed long long); char enum_check(unsigned long long); #endif // LONGLONG_EXISTS // 避免從float到int的意外轉型 char enum_check(float); char enum_check(double); char enum_check(long double); SizeOverOne enum_check( ... ); // 捕獲剩余所有情況 template<typename T> class IsEnumT { public: enum { Yes = IsFundaT<T>::No && !CompoundT<T>::IsRefT && !CompoundT<T>::IsPtrT && !CompoundT<T>::IsPtrMemT && sizeof(enum_check(ConsumeUDC<T>())) == 1 } enum { No = !Yes }; };
上面代碼的核心在於后面的一個sizeof表達式,它的參數是一個函數調用。也就是說,該sizeof表達式將會返回函數調用返回值的類型的大小;其中,將應用重載解析原則來處理enum_check()調用;但另一方面,我們並不需要函數定義,因為實際上並沒有真正調用該函數。在上面的例子中,如果實參可以轉型為一個整型,那么enum_check()將返回一個char值,其大小為1。對於其他的所有類型,我們使用了一個省略號函數(即enum_check( ... ) ),然而,根據重載解析原則的優先順序,省略號函數將會是最后的選擇。在此,我們對enum_check()的省略號版本進行了特殊的處理,讓它返回一個大小大於一個字節的類型(即SizeOverOne)。
對於函數enum_check的調用實參,我們必須仔細地考慮。首先,我們並不知道T是如何構造的,或許將會調用一個特殊的構造函數。為了解決這個問題,我們可以聲明一個返回類型為T的函數,然后通過調用這個函數來創建一個T。由於處於sizeof表達式內部,因此該函數實際上並不需要具有函數定義。事實上,更加巧妙的是:對於一個class類型T,重載解析是有可能選擇一個針對整型的enum_check()聲明的,但前提是該class必須定義一個到整型的自定義轉型(有時也稱為UDC)函數。到此,問題已經解決了。因為我們在ConsumeUDC模板中已經強制定義了一個到T的自定義轉型,該轉型運算符同時也為sizeof運算符生成了一個類型為T的實參。下面我們詳細分析下:
(1)最開始的實參是一個臨時的ConsumeUDC<T>對象;
(2)如果T是一個基本整型,那么將會借助於(ConsumeUDC的)轉型運算符來創建一個enum_check()的匹配,該enum_check()以T為實參;
(3)如果T是一個枚舉類型,那么將會借助於(ConsumeUDC的)轉型運算符,先把類型轉化為T,然后調用(從枚舉類型到整型的)類型提升,從而能夠匹配一個接收轉型參數的enum_check()函數(通常而言是enum_check(int));
(4)如果T是一個class類型,而且已經為該class自定義了一個到整型的轉型運算符,那么這個轉型運算符將不會被考慮。因為對於以匹配為目的的自定義轉型而言,最多只能調用一次;而且在前面已經使用了一個從ConsumeUDC<T>到T的自定義轉型,所以也就不允許再次調用自定義轉型。也就是說,對enum_check()函數而言,class類型最終還是未能轉型為整型。
(5)如果最終還是不能讓類型T於整型互相匹配,那么將會選擇enum_check()函數的省略號版本。
最后,由於我們這里只是為了辨別枚舉類型,而不是基本類型或者指針類型,所有我們使用了前面已經開放的IsFundaT和CompoundT類型,從而能夠排除這些令IsEnumT<T>::Yes成為非零的其他類型,最后使得只有枚舉類型的IsEnumT::Yes才等於1。
19.5 辨別class類型
使用排除原理:如果一個類型不是一個基本類型,也不是枚舉類型和組合類型,那么該類型就只能是class類型。
template <typename T> class IsClassT { public: enum { Yes = IsFundaT<T>::No && IsEnumT<T>::No && !CompoundT<T>::IsPtrT && !CompoundT<T>::IsRefT && !CompoundT<T>::IsArrayT && !CompoundT<T>::IsPtrMemT && !CompoundT<T>::IsFuncT }; enum { No = !Yes }; };