现在的位置: 首页 > 综合 > 正文

在C++中侦测内嵌型别的存在(rev#2)

2013年03月10日 ⁄ 综合 ⁄ 共 2810字 ⁄ 字号 评论关闭

C++中侦测内嵌类型的存在(rev#2)

 

By 刘未鹏(pongba)

C++的罗浮宫(http://blog.csdn.net/pongba)

 

动机(Motivation)

假设一所大学的注册系统提供了一个注册函数:

 

templateclass T>

void Register(T person)

{

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

};

 

而对于注册者有以下几种标识:

 

struct student_tag{};

struct teacher_tag{};

 

还有Register的几个供内部使用的重载版本:

 

templateclass T> void Register(T p, student_tag){...} // 注册学生

templateclass 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 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]数组的引用。

 

templateclass T>

struct does_sometypedef_exists

{

templateclass 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 ::value // 0

::value  // 1

};

 

下面我为你讲解它的原理。

 

当进行重载解析时,编译器会首先尝试实例化可以匹配的模板函数并将它们纳入到有待进行重载解析的函数的候选单之列,在本例中,当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

{

templateclass T>

struct key_type{};  // 请注意这是个模板类

};

 

按理说,这种情况下does_sometypedef_existsC>::value应该为false,因为第一个重载版本的typename U::key_type*不能被推导为C::key_type* C::key_type是个模板,它需要模板参数来实例化,然而在我的VC7.0下它通过编译了,并且结果为true就是说重载解析为第一个check函数)。如果我将check的第一个版本作一点小小的改动,像这样:

 

templateclass U>

抱歉!评论已关闭.