現(xiàn)在學(xué)校里假如不止學(xué)生教師,還有工人,警衛(wèi)等其它人員。如果他們不會在類內(nèi)部typedef任何東西,則Register需要一種機(jī)制以確定T內(nèi)部是否typedef了某個(gè)標(biāo)識符(例如person_tag)。如果沒有,就默認(rèn)處理。如果有,則再進(jìn)行更詳細(xì)的分類。
動機(jī)(Motivation)
假設(shè)一所大學(xué)的注冊系統(tǒng)提供了一個(gè)注冊函數(shù):
template<class T>
void Register(T person)
{
Register(person, typename T::person_tag());
};
而對于注冊者有以下幾種標(biāo)識:
struct student_tag{};
struct teacher_tag{};
還有Register的幾個(gè)供內(nèi)部使用的重載版本:
template<class T> void Register(T p, student_tag){...} // 注冊學(xué)生
template<class T> void Register(T p, teacher_tag){...} // 注冊教師
并規(guī)定學(xué)生類一定要在內(nèi)部typedef student_tag person_tag,教師類typedef teacher_tag person_tag,這樣,當(dāng)傳給起初的那個(gè)Register的對象為學(xué)生類對象時(shí),typename T::person_tag()其實(shí)就構(gòu)造了一個(gè)student_tag對象,從而激發(fā)函數(shù)重載,調(diào)用Register內(nèi)部版本的template<class T> void Register(T p, student_tag)版本。其他情況亦均有對應(yīng)。這是泛型編程里的常用手法(靜態(tài)多態(tài)),STL里屢見不鮮。
問題是,現(xiàn)在學(xué)校里假如不止學(xué)生教師,還有工人,警衛(wèi)等其它人員。如果他們不會在類內(nèi)部typedef任何東西,則Register需要一種機(jī)制以確定T內(nèi)部是否typedef了某個(gè)標(biāo)識符(例如person_tag)。如果沒有,就默認(rèn)處理。如果有,則再進(jìn)行更詳細(xì)的分類。
實(shí)現(xiàn)(Implementation)
這個(gè)問題可能有兩個(gè)實(shí)現(xiàn)途徑。
一是利用函數(shù)重載,具體如下:
typedef char (&yes_type)[1]; // sizeof(yes_type)==1
typedef char (&no_type)[2]; // sizeof(no_type)==2
以上的兩個(gè)typedef用于識別不同的重載函數(shù)。char (&)[1]表示對char[1]數(shù)組的引用,所以sizeof(char(&)[1])==sizeof(char[1])==1。注意圍繞&符號的一對圓括號,它們是必要的,如果沒有將會導(dǎo)致編譯錯(cuò)誤,正如char* [1]將被解析為char*的數(shù)組,char& [1]將被解析為引用的數(shù)組,而后者是非法的。將&用圓括號包圍則改變了運(yùn)算符的結(jié)合優(yōu)先序,這將被解析為對char[1]數(shù)組的引用。
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環(huán)境下,以下測試是成功的:
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;
};
下面我為你講解它的原理。
當(dāng)進(jìn)行重載解析時(shí),編譯器會首先嘗試實(shí)例化可以匹配的模板函數(shù)并將它們納入到有待進(jìn)行重載解析的函數(shù)的候選單之列,在本例中,當(dāng)typename T::key_type不存在時(shí),check的第一個(gè)模板版本不能實(shí)例化(因?yàn)槠涞诙€(gè)參數(shù)類型typename U::key_type*不存在),所以只能匹配第二個(gè)版本。當(dāng)typename T::key_type存在時(shí),第一個(gè)模板函數(shù)可以實(shí)例化,且可以匹配(注意第二個(gè)參數(shù)為缺省參數(shù)),所以無疑編譯器會匹配第一個(gè)版本,因?yàn)?/span>C++標(biāo)準(zhǔn)保證:只有當(dāng)其它所有重載版本都不能匹配的時(shí)候含有任意類型參數(shù)列表的版本(在本例中那是no_type check(...))才會被匹配。
一個(gè)值得注意的地方是:check的第一個(gè)版本只能是模板函數(shù),因?yàn)楫?dāng)編譯器推導(dǎo)類型的過程中發(fā)現(xiàn)該模板函數(shù)不能實(shí)例化時(shí)它就不去實(shí)例化它,而不是產(chǎn)生編譯錯(cuò)誤(除非沒有其它可匹配的重載版本)。因?yàn)榫幾g錯(cuò)誤只有將代碼編譯的過程中才會產(chǎn)生,而既然模板沒有實(shí)例化,那么該模板實(shí)際上并沒有經(jīng)過編譯。
然而,如果它不是模板函數(shù),則隨著does_sometypedef_exists類的實(shí)例化。它也會被實(shí)例化,然而如果不存在T::key_type,那么,該函數(shù)就成為非法。
還有一個(gè)值得注意的地方是:does_sometypedef_exists內(nèi)部的static T t;只是一個(gè)聲明,并不占用內(nèi)存空間,更妙的是,因?yàn)槭莻€(gè)聲明,所以編譯器根本不會對它初始化,所以它的默認(rèn)構(gòu)造函數(shù)就根本不會被執(zhí)行,事實(shí)上,編譯器在這種情況下甚至不會去看一看它是否有可用的默認(rèn)構(gòu)造函數(shù),它只需要類型信息就足夠了,不是么?因此,即使由于某些原因(例如,想讓T從堆上創(chuàng)建)T的默認(rèn)構(gòu)造函數(shù)被禁止(設(shè)為private),那么以上的traits也不會通不過編譯。“但是,等等!”你仿佛意識到了問題:“check的參數(shù)是傳值的!這時(shí)如果T的拷貝構(gòu)造函數(shù)是私有的將會發(fā)生什么事情呢?”事實(shí)是,根本不用去擔(dān)心,在sizeof的世界里,根本不會發(fā)生求值行為,編譯器只需要有關(guān)類型的信息。在編譯器內(nèi)部蘊(yùn)涵有一個(gè)巨大的類型推導(dǎo)系統(tǒng)。無論sizeof(...)里的表達(dá)式多么復(fù)雜,其類型都會最終在編譯期被正確推導(dǎo)出來。而對于sizeof(check(t)),編譯器有了函數(shù)的返回值類型信息就夠了,它并不會去執(zhí)行函數(shù)的代碼,也不會做實(shí)際的傳參行為,所以拷貝構(gòu)造也就無從發(fā)生。
但這里有一個(gè)十分怪異的問題(在我的VC7.0環(huán)境下存在),假設(shè)我們增加一個(gè)新類:
struct C
{
template<class T>
struct key_type{}; // 請注意這是個(gè)模板類
};
按理說,這種情況下does_sometypedef_exists<C>::value應(yīng)該為false,因?yàn)榈谝粋€(gè)重載版本的typename U::key_type*不能被推導(dǎo)為C::key_type* (C::key_type是個(gè)模板,它需要模板參數(shù)來實(shí)例化),然而在我的VC7.0下它通過編譯了,并且結(jié)果為true(就是說重載解析為第一個(gè)check函數(shù))。如果我將check的第一個(gè)版本作一點(diǎn)小小的改動,像這樣:
template<class U>
static yes_type check(U,
typename U::key_type* = (typename U::key_type*)0);
我僅僅加了一個(gè)轉(zhuǎn)換,編譯器就開始抱怨說使用模板類(它指的是C::key_type)需要模板參數(shù)了。我作了另外的種種測試(甚至我發(fā)現(xiàn)如果將10傳給它的第二個(gè)參數(shù),編譯器會說不能將int轉(zhuǎn)換為C::key_typ*,是的,這是編譯錯(cuò)誤的原文,這是否表示編譯器承認(rèn)C::key_type*為一種類型呢?我不知道)。結(jié)論是只有當(dāng)typename U::key_type*作為模板函數(shù)的參數(shù)類型時(shí)這種情況才會發(fā)生。
第二種實(shí)現(xiàn)是利用模板偏特化及默認(rèn)模板參數(shù)的規(guī)則:
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存在(假設(shè)X為任意類),則does_sometypedef_exists_1<X>首先由模板推導(dǎo)將does_sometypedef_exists_1的模板參數(shù)T匹配為X,則其偏特化版本因而被推導(dǎo)為:
struct does_sometypedef_exists_1<X,
typename check_helper<X,typename X::key_type>::type>
而typename check_helper<X,typename X::key_type>::type根據(jù)check_helper的定義其實(shí)就是X,所以該偏特化版本其實(shí)被推導(dǎo)為:
struct does_sometypedef_exists_1<X,X>
所以,如果你這樣測試:does_sometypedef_exists_1<X>::value,根據(jù)does_sometypedef_exists_1缺省定義(第二個(gè)模板參數(shù)默認(rèn)為T),你寫的相當(dāng)于:does_sometypedef_exists_1<X, X>::value。
而根據(jù)上面的推導(dǎo),如果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, typename X::key_type>::type也就隨之不存在,則does_sometypedef_exists_1的偏特化版本也就隨之不存在,于是編譯器會選擇使用缺省定義,其中value值為false。這正是我們所想要的結(jié)果。
測試(Test)
現(xiàn)在對我們的兩個(gè)實(shí)現(xiàn)版本測試一下吧,假設(shè)有一下幾個(gè)類:
// 沒有key_type
struct A{};
// typedef
struct B{typedef int key_type;};
// key_type為成員函數(shù)
struct C{void key_type(void){}};
// key_type為靜態(tài)常量數(shù)據(jù)成員
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編譯平臺上:
如果使用第一種實(shí)現(xiàn),這將輸出:0 1 0 0 1
如果使用第二種實(shí)現(xiàn),這將輸出:0 1 0 0 0
很顯然,兩種實(shí)現(xiàn)對于struct E給出的結(jié)果不一樣。事實(shí)上,我們希望該traits對E這種情況給出的結(jié)果為1。從這一點(diǎn)講第一種實(shí)現(xiàn)在我的編譯器上已經(jīng)神差鬼使的成功了,而第二種實(shí)現(xiàn)還沒有。不管怎樣,我們都必須試圖找到一種方法來實(shí)現(xiàn)它。這種方法不可以像實(shí)現(xiàn)一那樣依賴與編譯器的可能的“一時(shí)糊涂”,它應(yīng)該是以C++標(biāo)準(zhǔn)的規(guī)則為依據(jù)的。Paul Mensonides提供了一種方法,然而在我的VC7.0上編譯不能通過。后面我會介紹它。
改進(jìn)(Improvement)
第一種實(shí)現(xiàn)還可以做一點(diǎn)改進(jìn),像這樣:
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的第一個(gè)參數(shù),會使代碼看上去更簡潔和更可靠一些。
封裝(Encapsulation)
現(xiàn)在我們的traits只能偵測typename T::key_type的存在性,我們需要一個(gè)擴(kuò)充的機(jī)制,以讓我們能夠偵測任意名稱的內(nèi)嵌類型的存在性。我們使用宏:
#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>
經(jīng)過這重封裝,當(dāng)你要偵測某個(gè)名稱的內(nèi)嵌類型如some_type時(shí),你先在任何函數(shù)之外寫這樣的代碼:
IMPLEMENT_TYPEDEF_EXISTS(some_type)
這將會擴(kuò)展成一個(gè)名為does_sometypedef_exists_some_type的模板類,然后你這樣使用它:
DOES_TYPEDEF_EXISTS(X,some_type)::value;
這將偵測類X中有沒有some_type。不將::value直接納入到宏中的原因是為了保留traits編程的風(fēng)格。
Paul Mensonides對內(nèi)嵌template的偵測方法
Paul Mensonides是Boost庫的preprocesser部分的設(shè)計(jì)者,那完全是一個(gè)宏的世界,也是Boost庫中的一個(gè)十分精巧的部分。我最初是在comp.lang.c++.moderated上看到他關(guān)于這個(gè)問題的解答的。
template<class> struct split; // 缺省聲明,因?yàn)椴粫黄ヅ渌圆挥枚x
// 以下是偏特化
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. :(,后面有支持更多模板參數(shù)的版本,從略
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. :( 后面有支持更多模板參數(shù)的版本,從略
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說它能夠工作,我也覺得根據(jù)標(biāo)準(zhǔn)它也該能夠工作,但事實(shí)是在我的VC7.0上編譯器有一大堆抱怨。我試了其它各種方法,結(jié)果總是類似的編譯錯(cuò)誤將我擋住。我希望它在你的編譯器上能夠工作。
這里的原理是這樣的,如果類型X有內(nèi)嵌模板類型定義key_type,則has_template_key_type中的返回yes_type的那些成員函數(shù)總有一個(gè)能夠與它匹配,而其它則不會被實(shí)例化(VC7.0仿佛總試圖將其它的也實(shí)例化了,結(jié)果它總會抱怨說模板參數(shù)太少或太多)。
然而Paul Mensonides的這個(gè)解決方案還有個(gè)問題:如果那個(gè)內(nèi)嵌的模板類的定義像如下這個(gè)樣子:
template<int>
struct key_type{};
則將沒有任何一個(gè)返回yes_type的重載版本能和它匹配,看看split類的定義吧,它的template template模板參數(shù)的形式是template<class[ ,class ,...]> class T,而上面的key_type的形式為template<int> class key_type,它們無法匹配,如果試圖再加入一個(gè)能與其匹配的split偏特化版本:
template<template<int>class T,int T1> struct split<T<T1> >{...};
這也是不實(shí)際的。因?yàn)?/span>int和class可能有無窮多種組合。如果key_type再變成template<int, class> class key_type呢?如果...,總之,如你所見,以int這類non-type parameter作為模板參數(shù)的加入使事情有了無限多種可能。split將窮于應(yīng)付。
結(jié)論(Conclusion)
對于最后我提出的問題,仿佛沒有一個(gè)好的解決方案。所以只能放棄這種內(nèi)嵌template的可能,假定情況是單純的。對于后者,這種技術(shù)有較好的表現(xiàn)。
來自:http://blog.csdn.net/pongba/archive/2004/08/24/82783.aspx