在C++中偵測內嵌型別的存在

在C++中偵測內嵌類型的存在(rev#2)

 

By 劉未鵬(pongba)

C++的羅浮宮(http://blog.csdn.net/pongba)

 

動機(Motivation)

假設一所大學的註冊系統提供了一個註冊函數:

 

template<class T>

void Register(T person)

{

Register(person, typename T::person_tag());

};

 

而對於註冊者有以下幾種標識:

 

struct student_tag{};

struct teacher_tag{};

 

還有Register的幾個供內部使用的重載版本:

 

template<class T> void Register(T p, student_tag){...} // 註冊學生

template<class T> void Register(T p, teacher_tag){...} // 註冊教師

 

並規定學生類一定要在內部typedef student_tag person_tag,教師類typedef teacher_tag person_tag,這樣,當傳給起初的那個Register的對象爲學生類對象時,typename T::person_tag()其實就構造了一個student_tag對象,從而激發函數重載,調用Register內部版本的template<class T> void Register(T p, student_tag)版本。其他情況亦均有對應。這是泛型編程裏的常用手法(靜態多態),STL裏屢見不鮮。

 

問題是,現在學校裏假如不止學生教師,還有工人,警衛等其它人員。如果他們不會在類內部typedef任何東西,則Register需要一種機制以確定T內部是否typedef了某個標識符(例如person_tag)。如果沒有,就默認處理。如果有,則再進行更詳細的分類。

 

實現(Implementation)

這個問題可能有兩個實現途徑。

 

一是利用函數重載,具體如下:

 

typedef char (&yes_type)[1]; // sizeof(yes_type)==1

typedef char (&no_type)[2]; // sizeof(no_type)==2

 

以上的兩個typedef用於識別不同的重載函數。char (&)[1]表示對char[1]數組的引用,所以sizeof(char(&)[1])==sizeof(char[1])==1。注意圍繞&符號的一對圓括號,它們是必要的,如果沒有將會導致編譯錯誤,正如char* [1]將被解析爲char*的數組,char& [1]將被解析爲引用的數組,而後者是非法的。將&用圓括號包圍則改變了運算符的結合優先序,這將被解析爲對char[1]數組的引用。

 

template<class T>

struct does_sometypedef_exists

{

template<class U>

static yes_type check(U, typename U::key_type* =0); // #1

static no_type check(...);

static T t;   // 聲明

static const bool value = sizeof(check(t))==sizeof(yes_type);

};

 

注意,#1處,*和=之間的空格是必要的,否則編譯器會將它解析爲operator*=操作符。

 

在我的VC7.0環境下,以下測試是成功的:

 

struct A{};

struct B

{

typedef int key_type;

};

int main()

{

std::cout << does_sometypedef_exists<A>::value<<' '  // 0

<< does_sometypedef_exists<B>::value<<' '  // 1

<< std::endl;

};

 

下面我爲你講解它的原理。

 

當進行重載解析時,編譯器會首先嚐試實例化可以匹配的模板函數並將它們納入到有待進行重載解析的函數的候選單之列,在本例中,當typename T::key_type不存在時,check的第一個模板版本不能實例化(因爲其第二個參數類型typename U::key_type*不存在),所以只能匹配第二個版本。當typename T::key_type存在時,第一個模板函數可以實例化,且可以匹配(注意第二個參數爲缺省參數),所以無疑編譯器會匹配第一個版本,因爲C++標準保證:只有當其它所有重載版本都不能匹配的時候含有任意類型參數列表的版本(在本例中那是no_type check(...))纔會被匹配。

 

一個值得注意的地方是:check的第一個版本只能是模板函數,因爲當編譯器推導類型的過程中發現該模板函數不能實例化時它就不去實例化它,而不是產生編譯錯誤(除非沒有其它可匹配的重載版本)。因爲編譯錯誤只有將代碼編譯的過程中才會產生,而既然模板沒有實例化,那麼該模板實際上並沒有經過編譯。

 

然而,如果它不是模板函數,則隨着does_sometypedef_exists類的實例化。它也會被實例化,然而如果不存在T::key_type,那麼,該函數就成爲非法。

 

還有一個值得注意的地方是:does_sometypedef_exists內部的static T t;只是一個聲明,並不佔用內存空間,更妙的是,因爲是個聲明,所以編譯器根本不會對它初始化,所以它的默認構造函數就根本不會被執行,事實上,編譯器在這種情況下甚至不會去看一看它是否有可用的默認構造函數,它只需要類型信息就足夠了,不是麼?因此,即使由於某些原因(例如,想讓T從堆上創建)T的默認構造函數被禁止(設爲private),那麼以上的traits也不會通不過編譯。“但是,等等!”你彷彿意識到了問題:“check的參數是傳值的!這時如果T的拷貝構造函數是私有的將會發生什麼事情呢?”事實是,根本不用去擔心,在sizeof的世界裏,根本不會發生求值行爲,編譯器只需要有關類型的信息。在編譯器內部蘊涵有一個巨大的類型推導系統。無論sizeof(...)裏的表達式多麼複雜,其類型都會最終在編譯期被正確推導出來。而對於sizeof(check(t)),編譯器有了函數的返回值類型信息就夠了,它並不會去執行函數的代碼,也不會做實際的傳參行爲,所以拷貝構造也就無從發生。

 

但這裏有一個十分怪異的問題(在我的VC7.0環境下存在),假設我們增加一個新類:

 

struct C

{

template<class T>

struct key_type{};  // 請注意這是個模板類

};

 

按理說,這種情況下does_sometypedef_exists<C>::value應該爲false,因爲第一個重載版本的typename U::key_type*不能被推導爲C::key_type* (C::key_type是個模板,它需要模板參數來實例化),然而在我的VC7.0下它通過編譯了,並且結果爲true(就是說重載解析爲第一個check函數)。如果我將check的第一個版本作一點小小的改動,像這樣:

 

template<class U>

static yes_type check(U,

typename U::key_type* = (typename U::key_type*)0);

 

我僅僅加了一個轉換,編譯器就開始抱怨說使用模板類(它指的是C::key_type)需要模板參數了。我作了另外的種種測試(甚至我發現如果將10傳給它的第二個參數,編譯器會說不能將int轉換爲C::key_typ*,是的,這是編譯錯誤的原文,這是否表示編譯器承認C::key_type*爲一種類型呢?我不知道)。結論是隻有當typename U::key_type*作爲模板函數的參數類型時這種情況纔會發生。

 

第二種實現是利用模板偏特化及默認模板參數的規則

 

template<class T,class>

struct check_helper

{

typedef T type;

};

 

template<class T,class =T>

struct does_sometypedef_exists_1

{

static const bool value=false;

};

 

template<class T>

struct does_sometypedef_exists_1<T,

typename check_helper<T, typename T::key_type>::type>

{

static const bool value=true;

};

 

這看起來很小巧,僅僅使用了模板偏特化。但是請耐心聽我解釋。

 

如果typename X::key_type存在(假設X爲任意類),則does_sometypedef_exists_1<X>首先由模板推導將does_sometypedef_exists_1的模板參數T匹配爲X,則其偏特化版本因而被推導爲:

 

struct does_sometypedef_exists_1<X,

typename check_helper<X,typename X::key_type>::type>

 

typename check_helper<X,typename X::key_type>::type根據check_helper的定義其實就是X,所以該偏特化版本其實被推導爲:

 

struct does_sometypedef_exists_1<X,X>

 

所以,如果你這樣測試:does_sometypedef_exists_1<X>::value,根據does_sometypedef_exists_1缺省定義(第二個模板參數默認爲T),你寫的相當於:does_sometypedef_exists_1<X, X>::value。

 

而根據上面的推導,如果typename X::key_type存在,則does_sometypedef_exists_1的偏特化版本也存在且形式爲:

 

struct does_sometypedef_exists_1<X, X>

 

於是編譯器選擇匹配偏特化版本,其中的value值爲true。

 

而如果typename X::key_type不存在,則typename check_helper<X, typenameX::key_type>::type也就隨之不存在,則does_sometypedef_exists_1的偏特化版本也就隨之不存在,於是編譯器會選擇使用缺省定義,其中value值爲false。這正是我們所想要的結果。

 

測試(Test)

現在對我們的兩個實現版本測試一下吧,假設有一下幾個類:

 

// 沒有key_type

struct A{};

 

// typedef

struct B{typedef int key_type;};

 

// key_type爲成員函數

struct C{void key_type(void){}};

 

// key_type爲靜態常量數據成員

struct D{static const bool key_type=false;};

 

// 定義,D裏面的是聲明

const bool D::key_type;

 

// key_type爲模板類

struct E{

template<class>

struct key_type{};

};

 

template<class T>

struct does_typedef_exists

{

typedef does_sometypedef_exists<T> impl_type;

static const bool value = impl_type::value;

};

 

int main()

{

std::cout << does_typedef_exists<A>::value<<' '

<< does_typedef_exists<B>::value<<' '

<< does_typedef_exists<C>::value<<' '

<< does_typedef_exists<D>::value<<' '

<< does_typedef_exists<E>::value<<' '

<< std::endl;

return 0;

};

 

在我的VC7.0編譯平臺上:

 

如果使用第一種實現,這將輸出:0 1 0 0 1

如果使用第二種實現,這將輸出:0 1 0 0 0

 

很顯然,兩種實現對於struct E給出的結果不一樣。事實上,我們希望該traits對E這種情況給出的結果爲1。從這一點講第一種實現在我的編譯器上已經神差鬼使的成功了,而第二種實現還沒有。不管怎樣,我們都必須試圖找到一種方法來實現它。這種方法不可以像實現一那樣依賴與編譯器的可能的“一時糊塗”,它應該是以C++標準的規則爲依據的。Paul Mensonides提供了一種方法,然而在我的VC7.0上編譯不能通過。後面我會介紹它。

 

改進(Improvement)

第一種實現還可以做一點改進,像這樣:

 

template<class T>

struct does_sometypedef_exists

{

template<class U>

static yes_type check(typename U::key_type* );

 

template<class U>

static no_type check(...);

 

static const bool value = sizeof(check<T>(0))==sizeof(yes_type);

};

 

這樣,去掉static T t,和check的第一個參數,會使代碼看上去更簡潔和更可靠一些。

 

封裝(Encapsulation)

現在我們的traits只能偵測typename T::key_type的存在性,我們需要一個擴充的機制,以讓我們能夠偵測任意名稱的內嵌類型的存在性。我們使用宏:

 

#define IMPLEMENT_TYPEDEF_EXISTS(id) /

template<class T> /

struct does_sometypedef_exists_##id /

{ /

private: /

template<class U> /

static yes_type check(typename U::id*); /

template<class U> /

static no_type check(...); /

public: /

static const bool value=sizeof(check<T>(0))==sizeof(yes_type); /

};

 

#define DOES_TYPEDEF_EXISTS(T,id) /

does_sometypedef_exists_##id<T>

 

經過這重封裝,當你要偵測某個名稱的內嵌類型如some_type時,你先在任何函數之外寫這樣的代碼:

 

IMPLEMENT_TYPEDEF_EXISTS(some_type)

 

這將會擴展成一個名爲does_sometypedef_exists_some_type的模板類,然後你這樣使用它:

 

DOES_TYPEDEF_EXISTS(X,some_type)::value;

 

這將偵測類X中有沒有some_type。不將::value直接納入到宏中的原因是爲了保留traits編程的風格。

 

Paul Mensonides對內嵌template的偵測方法

Paul Mensonides是Boost庫的preprocesser部分的設計者,那完全是一個宏的世界,也是Boost庫中的一個十分精巧的部分。我最初是在comp.lang.c++.moderated上看到他關於這個問題的解答的。

 

template<class> struct split;  // 缺省聲明,因爲不會被匹配所以不用定義

 

// 以下是偏特化

template< template<class> class T, class T1 > // T爲模板

struct split< T<T1> > {

struct type { };

};

 

template< template<class, class> class T, class T1, class T2 >

struct split< T<T1, T2> > {

struct type { };

};

 

// etc. :(,後面有支持更多模板參數的版本,從略

 

template<class T> class has_template_key_type

{

private:

template<class U>

static yes_type check(

typename split<

typename U::template key_type<null_t> >::type*

);

 

template<class U>

static yes_type check(

typename split<

typename U::template key_type<null_t, null_t> >::type*

);

 

// etc. :( 後面有支持更多模板參數的版本,從略

 

template<class U> static no_type check(...);

 

public:

static const bool value

            = sizeof(check<T>(0)) == sizeof(yes_type);

};

 

template<class T, bool V = has_template_key_type<T>::value>

class has_key_type

{

private:

template<class U> static yes_type check(typename U::key_type*);

template<class U> static no_type check(...);

 

public:

static const bool value

            = sizeof(check<T>(0)) == sizeof(yes_type);

};

 

template<class T> struct has_key_type<T, true>

{

static const bool value = false;

};

 

Paul Mensonides說它能夠工作,我也覺得根據標準它也該能夠工作,但事實是在我的VC7.0上編譯器有一大堆抱怨。我試了其它各種方法,結果總是類似的編譯錯誤將我擋住。我希望它在你的編譯器上能夠工作。

 

這裏的原理是這樣的,如果類型X有內嵌模板類型定義key_type,則has_template_key_type中的返回yes_type的那些成員函數總有一個能夠與它匹配,而其它則不會被實例化(VC7.0彷彿總試圖將其它的也實例化了,結果它總會抱怨說模板參數太少或太多)。

 

然而Paul Mensonides的這個解決方案還有個問題:如果那個內嵌的模板類的定義像如下這個樣子:

 

template<int>

struct key_type{};

 

則將沒有任何一個返回yes_type的重載版本能和它匹配,看看split類的定義吧,它的template template模板參數的形式是template<class,class ,...]> class T,而上面的key_type的形式爲template<int> class key_type,它們無法匹配,如果試圖再加入一個能與其匹配的split偏特化版本:

 

template<template<int>class T,int T1> struct split<T<T1> >{...};

 

這也是不實際的。因爲intclass可能有無窮多種組合。如果key_type再變成template<int, class> class key_type呢?如果...,總之,如你所見,以int這類non-type parameter作爲模板參數的加入使事情有了無限多種可能。split將窮於應付。

 

結論(Conclusion)

對於最後我提出的問題,彷彿沒有一個好的解決方案。所以只能放棄這種內嵌template的可能,假定情況是單純的。對於後者,這種技術有較好的表現。


發佈了23 篇原創文章 · 獲贊 8 · 訪問量 9萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章