C++ template —— 類型區分(十一)


前面的博文介紹了模板的基礎,深入模板特性,模板和設計的一些內容。從這篇開始,我們介紹一些高級模板設計,開發某些相對較小、並且互相獨立的功能,而且對於這些簡單功能而言,模板是最好的實現方法:
(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 };
};

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM