在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, typename X::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{
border-right: medium none; padding-right: 0cm; border-top: medium none; padding-left: 0cm; padding-bottom: 0cm; margin: 0cm 0cm 0pt; border-left: medium none; text-
评论