C++模板中的类型参数T是抽象的,我们并不能在模板内部直接获得它的具体特征。类型萃取(抽取)技术就是要抽取类型的一些具体特征(trait),比如它是哪种具体类型,它是引用类型,内建类型,还是类类型等。可见,类型萃取技术其实就是trait模板技术的具体体现。获取类型的具体特征在Java、C#等语言中也称为反射(reflection),C++中通过模板技术也可以实现一定的反射行为。
类型信息是编译期的实体,现在要针对类型来进行编程,这其实就是模板元编程的一个方面。我们平常使用的if/else,while,for等基本的逻辑结构都是运行期的行为,在面向类型的编程中并不能使用,这就需要用到一些特殊的模板技术。实现类型萃取要用到的基本思想一个是特化,一个就是用typedef来携带类型信息。实际上,我们在用模板做设计时,一般建议在模板定义内部,为模板的每个类型参数提供typedef定义,这样在泛型代码中可以很容易地访问或抽取这些类型。
在C和C++中,普通的函数可以称为值函数,它们接受的参数是某些值,返回的结果也是值。而所谓的类型函数接受的实参是类型,返回的是被抽取出来的类型或常量值等(即用typedef定义的类型别名,一般不同的具体类型都定义统一的别名)。如类模板就是类型函数,sizeof是内建的类型函数,返回给定类型实参的大小。在类型编程中,很多地方都要用到sizeof。
下面演示一些有用的类型萃取实现,这些都是类型函数。
(1)确定某个类型是否为类类型(即class,struct,union):IsClassT<T>。
IsClassT的实现使用了“替换并非错误(SFINAE)“原则。即用模板实参替换模板参数时如果失败(创建出无效的类型),则不一定会出错,因为如果其他的重载版本演绎成功的话,代码仍然会有效。这里使用了对类类型特有的一种构造,即成员指针int (C::*)(...)作为test成员函数模板的形参,...表示成员函数的形参个数不定。当C是类类型时,这个构造有效,返回的char型占1个字节,sizeof的值等于1,Yes就会为1。注意size运算不需要函数的定义,这里的test成员函数模板无需定义实现,只需声明即可。当C是非类类型时,成员指针C::*构造无效,但并不会出错,因为对test的另外一个重载版本,非类类型C是有效的,这时test<T>(0)调用的就是另外那个重载版本,返回的针对char[2]型的结构体占2个字节,sizeof的值等于1,Yes就会为0。
(2)去掉类型中多余的&、const(或volatile)限定符:TypeOp<T>。用于避免在函数模板实参演绎时出现int&&或int const const之类的非法类型。我们知道在C++中int& &(指向引用的引用),int const const之类的式子是不合法的,平时我们一般不会写这样的式子。但是在编写函数模板时,稍不留神就会使模板参数演绎出这样的类型来(C++标准有一个修正允许只在模板实参演绎时出现这样的类型,这时看成是与int&或int const等价,但是很多编译器并不支持这个修正)。看下面的例子:
设实参argument为A,形参parameter为P。在调用apply(x,print)中,对x有A=int,P=T&是引用类型,&可不参与匹配,演绎出T=int;对print有A=void(int)是函数类型,P=void(*)(T)是非引用类型,A会发生退化转型,转型为指针类型void(*)(int),演绎出T=int。两个结论一致,得出T=int。在调用apply(x,incr)中,对incr有A=void(int&),P=void(*)(T),同样退化转型,演绎出T=int&。但是这样x的形参T&就变成了int& &了,不合法。我们可以创建一个类型函数TypeOp<T>来去掉多余的&或const修饰符。如下:
TypeOp<T>中,对const类型和引用类型进行了局部特化,由于指向void的引用是不允许的,因此针对void类型有一个全局特化。ArgT表示传进来的实参类型,BareT表示实参对应的裸类型(即去掉&或const限定符后的类型)。若函数模板中形参需要是引用类型或const类型,可以把形参声明为TypeOp<T>::RefT或TypeOp<T>::ConstT类型,它就可以进行安全的实参演绎。当然这时T不可演绎,因此必须要有其他的某个形参能演绎出T来(参看注释掉的apply的实现)。
删除typeoptest.cpp中原来的apply函数模板,换成被注释掉的那个apply实现。这里T位于受限名称TypeOp<T>::RefT中,因此这个参数中T不可演绎,但是apply的第2个参数可以演绎出T,然后就可以用演绎出来的结果生成第1个参数的实际类型。基本模板TypeOp<T>代表了传进来的是非引用、非const的类型,因此其ArgT和BareT相同。当传进来的是引用类型时,例如这里是int&(因为第2个参数已经演绎出T=int&),如果是原来的T&,就会变成非法的int& &,现在的TypeOp<T>::RefT就变成了TypeOp<int&>::RefT,这会调用特化版本TypeOp<T&>,其内部T变成了int(去掉了一个&),真正的引用类型RefT就是int&,因此TypeOp<T>::RefT最终结果是int&,而不是int& &。对于传const类型的参数,可以类似分析。注意若演绎出T是void,则从void的全局特化中可以看出其引用类型TypeOp<void>::RefT仍然是void,避免了出现类似void&的无效类型。
(3)从多个类型中选择一个类型:IfThenElse<bool,T1,T2>。根据bool值来选择类型T1或T2。这可用于类型提升、选择返回类型、对类型添加修饰符(如&,const,这可使传值变成传引用)、对类型进行一些基本的操作等。
(4)类型提升:Promotion<T1,T2>。可以找出两个类型中更强大的类型,或变成另外一个更强大的类型。一般根据类型所占字节的大小来提升,要用到sizeof运算符。
基本模板中是把两种类型T1,T2提升为其中大的一种类型。T1>T2则提升为T1,T1<T2则提升为T2,否则sizeof(T1)==sizeof(T2),若T1与T2不是同种类型,则会提升为void类型。若T1与T2是同一种类型,则会调用特化版本,直接提升所期望的类型。MK_PROMOTION(T1,T2,Tr)宏是把T1和T2提升为第三种类型Tr,这一般用于内建类型之间的提升,比如把bool和char的混合运算提升为int类型。这里也可以不用宏,而是直接用模板来实现。但是用宏并结合模板来实现代码会更简洁,而且接口也更简洁。注意宏在预处理阶段展开,而模板在编译阶段展开,因此这里的MK_PROMOTION(T1,T2,Tr)实现是可行的。这里还特化了其他的一些提升规则,比如对容器类型Array<T>,对容器内的元素类型进行提升。这时我们还必须提供最后那个针对容器内的元素类型相同的特化。有可能你会认为当容器内的元素类型相同时,会调用Promotion<T,T>这个特化,但实际上Promotion<T,T>与Promotion<Array<T>,Array<T> >的特化程度是一样的,这会产生特化调用的二义性。因此我们必须提供了一个更加特殊化的特化版本Promotion<Array<T>,Array<T> >,这时编译器就会选择调用这个版本。
上面这些类型函数都是用来确定具体的类型属性(trait),没有policy。下面我们为trait引入policy选择功能,上面的类型函数称为property trait,而下面的这些类型函数则可以称为policy trait。
(5)根据不同类型来选择是传值还是传const引用:RParam<T>。比如对类类型使用传const引用的策略,对非类类型使用传值的策略,而对某些可能“传值时性能更好”的类类型,我们可以通过特化来指定它们为传值。我们知道对类类型传值的话,会有昂贵的拷贝构造函数调用开销,特别对于容器类型来说就更耗费资源了。在使用时,把函数的形参声明为RParam<T>::Type类型,就可以自动根据类型T的特征来选择是传值还是传引用。RParam<T>的实现及测试代码如下:
基本模板中,对非类类型传值,对类类型传const类型的引用。针对某些需要特殊对待的类类型,例如假定类类型std::complex<T>传值会比传引用更高效,因此我们就要提供特化来让其传值而不是传引用。由于传值还是传引用是在使用的函数参数中进行的,因此policy的自动选择实际上是在客户端的函数代码中完成。foo_core(p1,p2)是测试的函数,但我们知道RParam<T>::Type并不能用来演绎模板参数T,因此要利用包装函数技术把它包装在内联的foo函数中,这时foo的参数T1和T2就可以演绎了。
还要其他的很多策略。比如对不大于2个指针大小的类型传值,对其他类型则传const引用,而对容器类型的对象,即使它小于2个指针的大小,我们也让它始终传引用,以避免昂贵的拷贝构造函数调用,等等。这种策略实现如下:
(6)根据不同类型来选择是拷贝、交换、还是移动:CSMtraits<T>。比如对类类型可以选择非位元的拷贝、交换还是移动,对非类类型执行位元拷贝或移动等。通过特化来实现不同的策略。这里还使用了继承,把实现都放在基类BitOrClassCSM<T,bool>中,让CSMtraits<T>继承自它。对拷贝、交换还是移动能够进行选择主要是基于性能的考虑。有些类型(比如容器类型,智能指针类型),可能交换或移动对象的内容会比直接调用对象的拷贝构造函数更高效,又比如对非类类型,直接的内存位元拷贝可能更高效,而且也比较安全。实际上在C++标准库中对容器类型的对象,通常是不允许拷贝的(这涉及大量的拷贝构造函数调用),而是只能交换或移动容器中存放的元素。CSMtraits<T>的实现如下:
这里对非类类型使用位元拷贝,对类类型使用对象安全拷贝(非位元拷贝)。位元拷贝的特化BitOrClassCSM<T,true>继承了非位元拷贝的特化BitOrClassCSM<T,false>,这样就会隐藏父类中签名相同的函数版本,在应用时就会使用自己的实现版本。
本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系我们删除。