动机(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); //请注意* 和=之间的空格,那是必要的,否则
//编译器会将它解析为operator*=操作符
static no_type check(...);
static T t; //声明
static const bool value=sizeof(check(t))==sizeof(yes_type);
};
在我的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的默认构造函数被禁止(设为private)(例如,想让T从堆上创建),那么以上的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>
{ //上面的两个typename都是必要的。
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,typename X::key_type>::type也就随之不存在,则does_sometypedef_exists_1的偏特化版本也就随之不存在,于是编译器会选择使用缺省定义,其中value值为false。这正是我们所想要的结果。
测试(Test)
现在对我们的两个实现版本测试一下吧,假设有一下几个类:
struct A{}; //没有key_type
struct B{typedef int key_type;}; //typedef
struct C{void key_type(void){}}; //key_type为成员函数
struct D{static const bool key_type=false;}; //key_type为静态常量数据成员
const bool D::key_type; //定义,D里面的是声明
struct E{
template<class>
struct key_type{}; //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; //则将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> >{...};
这也是不实际的。因为int和class可能有无穷多种组合。如果key_type再变成template<int,class> class key_type呢?如果...,总之,如你所见,以int作为模板参数的加入使事情有了无限多种可能。split将穷于应付。
结论(Conclusion)
对于最后我提出的问题,仿佛没有一个好的解决方案。所以只能放弃这种内嵌template的可能,假定情况是单纯的。对于后者,这种技术有教好的表现。