Boost源码剖析之:泛型编程中的精灵type_traits(原创)

类别:编程语言 点击:0 评论:0 推荐:

动机

1. 分派

下面有一个模板函数,假设一个动物收容组织提供了它,他们接受所有无家可归的可怜的小动物,于是他们向外界提供了一个函数接受注册。函数看起来像这样:

template //T表示接受的是何种动物

void AcceptAnimals(T animal)

{

?? ?...? //do something

};

??? 但是,如果他们想将猫和狗分开处理(毕竟饲养一只猫和饲养一只狗并不相同。他们可能会为狗买一根链子,而温顺的猫则可能不需要)。一个可行的方法是分别提供两个函数:AcceptDog和AcceptCat,然而这种解决办法并不优雅(想想看,注册者可能既有一只猫又有一只狗,这样他不得不调用不同的函数来注册,而且,如果种类还在增多呢,那样会导致向外提供的接口的增多,注册者因此而不得不记住那些烦琐的名字,而这显然没有只需记住AccpetAnimal这一个名字简单)。如果想保持这个模板函数,并将它作为向外界提供的唯一接口,则我们需要某种方式来获取型别T的特征(trait),并按照不同的特征来采用不同的策略。这里我们有第二个解决办法:

约定所有的动物类(如class Cat,class Dog)都必须在内部typedef一个表明自己身份的型别,作为标识的型别如下:

struct cat_tag{}; //这只是个空类,目的是激发函数重载,后面会解释

struct dog_tag{}; //同上

于是,所有狗类都必须像这样:

class Dog

{

? public:

?? ??typedef? dog_tag? type; //型别(身份)标志,表示这是狗类,如果是猫类则为typedef cat_tag type;

? ...

}

然后,动物收容组织可以在内部提供对猫狗分开处理的函数,像这样:

template

void Accept(T dog,dog_tag) //第二个参数为无名参数,只是为了激发函数重载

{...}

template

void Accpet(T cat,cat_tag) //同上

{...}

于是先前的Accept函数可以改写如下:

template

void Accept(T animal)? //这是向外界提供的唯一接口

{

1?? Accept(animal,typename T::type()); //如果T为狗类,则typename T::type就是dog_tag,那么

??????????????????????????????? //typename T::type()就是创建了一个dog_tag类的临时对象,

??????????????????????????????? //根据函数重载的规则,这将调用Accept(T,dog_tag)的版本,

??????????????????????????????? //这正是转向处理狗的策略

??????????????????????????????? //如果T为猫类,则typename T::type为cat_tag,由上面的推导,

??????????????????????????????? //这将调用Accept(T,cat_tag)的版本,转向处理猫的策略

}?????????????????????????????? //typename 关键字告诉编译器T::type是个型别而不是静态成员

所有型别推导,函数重载,都在编译期完成,你几乎不用耗费任何运行期成本(除了创建dog_tag,cat_tag临时对象的成本,然而经过编译器的优化,这种成本可能也会消失)就拥有了可读性和可维护性高的代码。“但是,等等!”你说:“traits在哪?”,typename T::type其实就是traits,只不过少了一层封装而已,如果像这样作一些改进:

? template

? struct AnimalTraits

? {

???? typedef T::type type;

? };

于是1处的代码可以写成Accept(animal,typename AnimalTraits::type());

2. 效率

通常为了提高效率,为某种情况采取特殊的措施是必要的,例如STL里面的copy,原型像这样:

template

IterOut copy(IterIn first,IterIn last,IterOut dest){//将[first,last)区间内的元素拷贝到以dest开始的地方

?? return copy_opt(first,last,dest,ptr_category(first,dest)); //ptr_category用来萃取出迭代器的类别以进行?????????????????????

??????????????????????????????????????????????? //适当程度的优化

}

copy_opt有两个版本,其中一个是针对如基本型别的数组作优化的,如果拷贝发生在char数组间,那么根本用不着挨个元素赋值,基于数组在内存中分布的连续性,可以用速度极快的memmove函数来完成。ptr_category有很多重载版本,对可以使用memmove的情况返回一个空类如scalar_ptr的对象以激发函数重载。其原始版本则返回空类non_scalar_ptr的对象。copy_opt的两个版本于是像这样:

template

IterOut copy(IterIn first,IterIn last,IterOut dest,scalar_ptr){ ...}? //使用 memmove

template

IterOut copy(IterIn first,IterIn last,IterOut dest,non_scalar_ptr){ ...} //按部就班的逐个拷贝

其实通常为了提高效率,还是需要分派。

3. 使某些代码能通过编译

这或许令人费解,原来不能通过编译的代码,经过traits的作用就能编译了吗?是的,考虑std::pair的代码(为使代码简洁,忽略大部分):

template

struct pair

{

? T1 first;

? T2 second;

?2 pair(const T1 & nfirst, const T2 & nsecond) //如果T1或T2本身是引用,则编译错误,因为没有“引

?? :first(nfirst), second(nsecond) { }???????? //用的引用”(C++之父Bjarne Stroustrup已经向C++标准

?...???????????????????????? ???????????//委员会提交一份提案建议将“引用的引用”看作“引用”,

};????????????????????????????????????? //或许以后这会合法化)。

这里可以使用一个traits(boost库里面的名字为add_reference)来避免这样的错误。这个traits内含一个typedef,如果add_reference的T为引用,则typedef T type;如果不是引用,则typedef T& type;这样,2处的代码可写成:

pair(add_reference::type nfirst,add_reference::type nsecond)。

这对所有的型别都能通过编译。

Boost库中的Traits

Boost中的Traits十分完善,可分为几大类:1. Primary Type Categorisation(初级型别分类) 2. Secondary Type Categorisation(次级型别分类) 3. Type Properties(型别属性) 4. Relationships Between Types(型别间关系) 5. Transformations Between Types(型别间转换) 6. Synthesizing Types(型别合成) 7. Function Traits(函数traits)

由于其中一些traits只是简单的模板偏特化,故不作介绍,本文仅介绍一些技术性较强的traits。由于traits的定义往往重复代码较多,所以必要时本文仅剖析其底层机制。所有源码均摘自相应头文件中,为使源码简洁,所有的宏均已展开。由于traits技巧与编译平台息息相关,某些平台可能不支持模板偏特化。这里我们假设编译器是符合C++标准的。在我的VC7.0上,以下代码均通过编译并正常工作。

1.??????? 初级型别分类

is_array(boost/type_traits/is_array.hpp)

定义

template is_array{static const bool value=false;};? //缺省

template is_array{static const bool value=true;}; //偏特化

注解

C++标准允许整型作为模板参数,上面的N就是这样。这也说明出现在模板偏特化版本中的模板参数(在本例中为typename T,size_t N两个)个数不一定要跟缺省的(本例中为typename T一个)相同,但是出现在类名称后面的参数个数却要跟缺省的个数相同(is_array,T[N]为一个参数,与缺省的相同)。

is_array::value? //true,with T=int,N=10,使用偏特化版本

is_array::value?? //false,with T=int ,使用缺省版本

is_class(.../is_class.hpp)? //省略前面的boost/type_traits,下同

定义

??? template

??? struct is_class_impl //底层实现,原因是根据不同的编译环境可能有不同的底层实现,我的编译环境

??? {??????????????? //为VC7.0,其他底层实现从略。

??? template static ::boost::type_traits::yes_type is_class_tester(void(U::*)(void));

??? template static ::boost::type_traits::no_type is_class_tester(...); //...表示任意参数列表

??? static const bool value = ::boost::type_traits::ice_and::value //is_union的判断需要编译器

??????? >::value???????????????????????????????????????????????? //的支持,可当作总为false

};

template

struct is_class

{

?? static const bool value=is_class_impl::value; //所有实现都在is_class_imp中

};

注解

::boost::type_traits::yes_type为一个typedef: typedef char yes_type; 所以sizeof(yes_type)为1.

::boost::type_traits::no_type为一个struct: struct no_type{char padding[8]; };sizeof(no_type)为8。他们

一般被用作重载函数的返回值型别,这样通过检查返回值型别的大小就知道到底调用了哪个函数,他

们被定义在boost/type_traits/detail/yes_no_type.hpp中。

is_class_impl中有两个static函数,第一个函数仅当模板参数U是类时才能够被具现化,因为它的参数类型是void(U::*)(void),即指向成员函数的指针。第二个函数具有不定量任意参数列表,C++标准说只有当其它所有的重载版本都不能匹配时,具有任意参数列表的重载版本才会被匹配。所以,如果T为类,则void (T::*)(void)这种型别就存在,所以对is_class_tester(0)的重载决议将是调用第一个函数,因为将0赋给任意型别的指针都是合法的。而如果T不是类,则就不存在void(T::*)(void)这种指针型别,所以第一个函数就不能具现化,这样,对is_class_tester(0)的重载决议结果只能调用第二个函数。

现在注意3处的表达式sizeof(is_class_tester(0))==sizeof(boost::type_traits::yes_type)。按照以上的推论,如果T为类,is_class_tester(0)实际调用第一个重载版本,返回yes_type,则表达式评估为true。如果T不是类,则is_class_tester(0)调用第二个重载版本,返回no_type,则表达式评估为false。这正是我们想要的。一个值得注意的地方是:在sizeof的世界里,没有表达式被真正求值,编译器只推导出表达式的结果的型别,然后给出该型别的大小。对于sizeof(is_class_tester(0))编译器实际并不调用函数的代码来求值,而只关心函数的返回值型别。所以声明该函数就够了。另一个值得注意之处是is_class_tester的两个重载版本都用了模板函数的形式。第一个版本用模板形式的原因是如果不那样做,而是这样static yes_type is_class_tester(void(T::*)(void));则当T不是类时,该traits将不能通过编译,原因很简单,当T不是类时void (T::*)(void)根本不存在。然而,使用模板时,当T不是类时该重载版本会因不能具现化而根本不编译,C++标准允许不被使用的模板不编译(具现化)。这样编译器就只能使用第二个版本,这正合我们的意思。而第二个版本为模板是因为第一个版本是模板,因为在3处对is_class_tester的调用是这样的:is_class_tester(0),如果第二版本不是模板则这样代码只能解析为对is_class_tester模板函数(即第一个版本)的调用,重载解析也就不复存在。

“等等!”你意识到了一些问题:“模板函数的调用可以不用显式指定模板参数!”好吧,也就是说你试图这样写:

???? template static ::boost::type_traits::yes_type is_class_tester(void(U::*)(void));? //模板

???? static ::boost::type_traits::no_type is_class_tester(...);?? //非模板

???? 然后在3标记的那一行这样调用:is_class_tester(0)(原来是is_class_tester(0)),是的,我得承认,这的确构成了函数重载的条件,也的确令人欣喜的通过了编译,然而结果肯定不是你想要的!你会发现对所有型别is_class::value现在都是0。也就是说,编译器总是调用is_class_tester(..);这是因为,当调用的函数的所有重载版本中有一个或多个为模板时,编译器首先要尝试进行模板函数具现化而非重载决议,而在尝试具现化的过程中,编译器会进行模板参数推导,0的型别被编译器推导为int(0虽然可以赋给指针,但0的型别不可能被推导为指针型别,因为指针型别可能有无数种,而事实上C++是强类型语言,对象只能属于某一种型别),而第一个函数的参数型别void (U::*)(void)根本无法与int匹配(因为如果匹配了,那么模板参数U被推导为什么呢?)。所以第一个版本具现化失败后编译器只能采用非模板的第二个版本。结果如你所见,是令人懊恼的。然而如果你写的是is_class_tester(0)你其实是显式具现化了is_class_tester每一个模板函数(除了那些不能以T为模板参数具现化的),而它们都被列入接受重载决议的侯选单,然后编译器要做的就只剩下重载决议了。(关于编译器在含有模板函数的重载版本时是如何进行重载决议的,可参见C++ Primer的Function Templates一节,里面有极其详细的介绍)。

以上所将的利用函数重载来达到某些目的的技术在type_traits甚至整个Boost库里多处用到。

初级型别分类还有:is_void,is_integral,is_float,is_pointer,is_reference,is_union,is_enum,

is_function。请参见Boost提供的文档。

2.次级型别分类

is_member_function_pointer(.../is_member_function_pointer.hpp)

定义(.../detail/is_mem_fun_pointer_impl.hpp)

??? template

??? struct is_mem_fun_pointer_impl??? //缺省版本

??? {

??????? static const bool value = false;

};

??? template ?? //偏特化版本,匹配无参数的成员函数

struct is_mem_fun_pointer_impl { static const bool value = true; };

??? template ? //匹配一个参数的成员函数

struct is_mem_fun_pointer_impl { static const bool value = true; };

.etc. ... //其它版本只是匹配不同参数个数的成员函数的偏特化而已,参见源文件。

template

struct is_mem_function_pointer

{ static const bool value=is_mem_fun_pointer_impl::value; };

注解

假设你有一个类X,你这样判断:is_mem_function_pointer::value;则编译器先将

is_mem_function_pointer的模板参数class T推导为int (X::*)(int),然后将其传给is_mem_fun_pointer_impl

,随后编译器寻找后者的偏特化版本中最佳匹配项为is_mem_fun_pointer_impl

本文地址:http://com.8s8s.com/it/it28196.htm