您的位置:首页 > 编程语言 > C语言/C++

我的C++实践(5):类型萃取技术

2016-07-29 00:00 351 查看
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.hpp:辨别类类型(class,struct,union)
#ifndef IS_CLASST_HPP
#define IS_CLASST_HPP
template<typename T>
class IsClassT{  //确定某个类型是否为类类型
private:
typedef char One;
typedef struct{
char a[2];
} Two;
template<typename C>
static One test(int C::*); //C是类类型时使用这个版本,函数参数是一个C的成员指针
template<typename C>
static Two test(...); //C是非类类型时使用这个版本
public:
enum { Yes=sizeof(test<T>(0))==1 }; //是类类型则Yes为1,不是类类型时Yes为0
enum { No=!Yes };
};
#endif


//isclassttest.cpp:测试IsClassT的实现
#include <iostream>
#include "isclasst.hpp"
template<typename T>
void check(){ //检查T是否是类类型:以模板实参方式传递类型
if(IsClassT<T>::Yes){
std::cout<<" IsClassT(flag="<<IsClassT<T>::Yes<<")"<<std::endl;
}else{
std::cout<<" !IsClassT(flag="<<IsClassT<T>::Yes<<")"<<std::endl;
}
}
template<typename T>
void checkT(T& a){  //检查T是否是类类型:以函数调用实参方式传递类型
check<T>();
}
class MyClass{ //类类型
};
struct MyStruct{ //类类型
};
union MyUnion{ //类类型
};
void myfunc(){ //非类类型
}
enum E{ e1 }e; //非类类型
int main(){
std::cout<<"int: ";
check<int>(); //非类类型
std::cout<<"MyClass: ";
check<MyClass>(); //类类型
std::cout<<"MyStruct: ";
MyStruct s;
checkT(s); //类类型
std::cout<<"MyUnion: ";
check<MyUnion>(); //类类型
std::cout<<"enum: ";
checkT(e); //非类类型
std::cout<<"myfunc():";
checkT(myfunc); //非类类型
return 0;
}


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等价,但是很多编译器并不支持这个修正)。看下面的例子:

//typeoptest.cpp:模板实参演绎错误(出现int& &)的测试
#include <iostream>
#include "typeop.hpp"
/*
template<typename T>
void apply(typename TypeOp<T>::RefT arg,void (*func)(T)){
func(arg);
}
*/
template<typename T>
void apply(T& arg,void(*func)(T)){
func(arg);
}
void print(int a){ //类型为void(int)
std::cout<<a<<std::endl;
}
void incr(int& a){ //类型为void(int&)
++a;
}
int main(){
int x=7;
apply(x,print);
apply(x,incr);
return 0;
}


设实参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.hpp:根据传进去的类型,获取真正需要的类型
//可去掉类型中多余的&、const,例如避免在函数模板实参演绎时出现int& &或
//int const const之类的非法类型
#ifndef TYPEOP_HPP
#define TYPEOP_HPP
template<typename T>
class TypeOp{ //基本模板
public:
typedef T ArgT; //参数类型:调用时的参数类型为T
typedef T BareT; //裸类型:无引用符&,无限定符const
typedef T const ConstT;
typedef T& RefT;
typedef T& RefBareT;
typedef T const& RefConstT;
};
template<typename T>
class TypeOp<T const>{ //针对const类型的局部特化
public:
typedef T const ArgT; //调用时的参数类型:有限定符const
typedef T BareT;
typedef T const ConstT;
typedef T const& RefT;
typedef T& RefBareT;
typedef T const& RefConstT;
};
template<typename T>
class TypeOp<T&>{ //针对引用类型的局部特化
public:
typedef T & ArgT; //调用时的参数类型:有限定符&
typedef typename TypeOp<T>::BareT BareT;
typedef T const ConstT;
typedef T & RefT;
typedef typename TypeOp<T>::BareT & RefBareT;
typedef T const & RefConstT;
};
template<>
class TypeOp<void>{ //针对void的全局特化,因为指向void的引用或const类型不允许
public:
typedef void ArgT; //调用时的参数类型:为void
typedef void BareT;
typedef void const ConstT;
typedef void RefT;
typedef void RefBareT;
typedef void RefConstT;
};
#endif


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,这可使传值变成传引用)、对类型进行一些基本的操作等。

//ifthenelse.hpp:根据bool值来选择类型T1或T2
#ifndef IFTHENELSE_HPP
#define IFTHENELSE_HPP
template<bool C,typename Ta,typename Tb>
class IfThenElse;   //基本模板,根据第1个实参来决定是选择第2个实参还是第3个实参
template<typename Ta,typename Tb>
class IfThenElse<true,Ta,Tb>{ //局部特化:true的话选择第2个实参
public:
typedef Ta ResultT;
};
template<typename Ta,typename Tb>
class IfThenElse<false,Ta,Tb>{ //局部特化,false的话选择第3个参数
public:
typedef Tb ResultT;
};
#endif


(4)类型提升: Promotion<T1,T2>。可以找出两个类型中更强大的类型,或变成另外一个更强大的类型。一般根据类型所占字节的大小来提升,要用到sizeof运算符。

//promote1.hpp
#ifndef PROMOTION_HPP
#define PROMOTION_HPP
#include "ifthenelse.hpp"
template<typename T1,typename T2>
class Promotion{ //类型提升的基本模板
public:
typedef typename
IfThenElse< (sizeof(T1)>sizeof(T2)),
T1,
typename IfThenElse<(sizeof(T1)<sizeof(T2)),T2,void>::ResultT
>::ResultT ResultT;
};
template<typename T>
class Promotion<T,T>{ //针对两个相同类型的局部特化
public:
typedef T ResultT;
};
//对基本类型和枚举类型:可能要提升为其他的更强大的类型,
//这需要另外的特化实现,为方便用户使用,把它们定义成宏
//把T1,T2提升为Tr
#define MK_PROMOTION(T1,T2,Tr)       /
template<> class Promotion<T1,T2>{   /
public:                              /
typedef Tr ResultT;              /
};                                   /
//......
//接着可以定义其他的提升规则:比如容器内的元素类型提升
template<typename T1,typename T2>
class Promotion<Array<T1>,Array<T2> >{ //容器内的元素类型提升
public:
typedef Array<typename Promotion<T1,T2>::ResultT> ResultT;
};
template<typename T>
class Promotion<Array<T>,Array<T> >{ //容器内的元素类型相同时的提升
public:
typedef Array<typename Promotion<T,T>::ResultT> ResultT;
};
#endif


基本模板中是把两种类型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>的实现及测试代码如下:

//rparam.hpp:传值还是传引用的自动选择实现
#ifndef RPARAM_HPP
#define RPARAM_HPP
#include <complex>
#include "ifthenelse.hpp"
#include "isclasst.hpp"
template<typename T>
class RParam{ //基本模板:非类类型传值,类类型传引用
public:
typedef typename IfThenElse<IsClassT<T>::No,
T,
T const&>::ResultT Type;
};
template<>
class RParam<std::complex<int> >{ //针对类类型std::complex<int>的特化:让其传值而不是传引用
public:
typedef std::complex<int> Type;
};
//...
#endif


//rparamtest.cpp:测试RParam<T>的实现
#include "rparam.hpp"
#include <complex>
template<typename T1,typename T2>
void foo_core(typename RParam<T1>::Type p1,
typename RParam<T2>::Type p2){ //根据实参类型自动决定传值还是传引用
//...
}
template<typename T1,typename T2>
inline void foo(T1 const& p1,T2 const& p2){ //包装函数:定义为内联函数
foo_core<T1,T2>(p1,p2);
}
class MyClass{
public:
MyClass(){ }
MyClass(MyClass const& rhs){
std::cout<<"MyClass copy constructor called/n";
}
};
int main(){
std::complex<int> mc1;
MyClass mc2;
foo(mc1,mc2); //第1个参数应该是传值,第2个参数应该是传引用
//结果是不会调用MyClass的拷贝构造函数
return 0;
}


基本模板中,对非类类型传值,对类类型传const类型的引用。针对某些需要特殊对待的类类型,例如假定类类型std::complex<T>传值会比传引用更高效,因此我们就要提供特化来让其传值而不是传引用。由于传值还是传引用是在使用的函数参数中进行的,因此policy的自动选择实际上是在客户端的函数代码中完成。foo_core(p1,p2)是测试的函数,但我们知道RParam<T>::Type并不能用来演绎模板参数T,因此要利用包装函数技术把它包装在内联的foo函数中,这时foo的参数T1和T2就可以演绎了。
还要其他的很多策略。比如对不大于2个指针大小的类型传值,对其他类型则传const引用,而对容器类型的对象,即使它小于2个指针的大小,我们也让它始终传引用,以避免昂贵的拷贝构造函数调用,等等。这种策略实现如下:

//rparam2.hpp:第二种策略的实现,对大于2个指针大小的类型传值,其他类型传引用
#ifndef RPARAM_HPP
#define RPARAM_HPP
#include <vector>
template<typename T>
class RParam{ //基本模板:大于2个指针大小的类型传值,其他类型传引用
public:
typedef typename IfThenElse<sizeof(T)<=2*sizeof(void*),
T,
T const&>::ResultT Type;
};
template<typename T>
class RParam<std::vector<T> >{ //针对容器类型的特化:让容器类型始终传引用
public:
typedef std::vector<T> const& Type;
};
//...
#endif


(6)根据不同类型来选择是拷贝、交换、还是移动: CSMtraits<T>。比如对类类型可以选择非位元的拷贝、交换还是移动,对非类类型执行位元拷贝或移动等。通过特化来实现不同的策略。这里还使用了继承,把实现都放在基类BitOrClassCSM<T,bool>中,让CSMtraits<T>继承自它。对拷贝、交换还是移动能够进行选择主要是基于性能的考虑。有些类型(比如容器类型,智能指针类型),可能交换或移动对象的内容会比直接调用对象的拷贝构造函数更高效,又比如对非类类型,直接的内存位元拷贝可能更高效,而且也比较安全。实际上在C++标准库中对容器类型的对象,通常是不允许拷贝的(这涉及大量的拷贝构造函数调用),而是只能交换或移动容器中存放的元素。CSMtraits<T>的实现如下:

//csm.hpp:从拷贝copy、交换swap、移动move中自动地选择出最佳的操作,来操作某一特定类型的元素
#ifndef BIT_OR_CLASS_CSM_HPP
#define BIT_OR_CLASS_CSM_HPP
#include <new>
#include <cassert>
#include <cstddef>
#include <cstring>
#include "rparam.hpp"
template<typename T,bool Bitwise>
class BitOrClassCSM;  //基本模板
//用于对象安全拷贝(非位元拷贝)的局部特化
template<typename T>
class BitOrClassCSM<T,false>{
public:
static void copy(typename RParam<T>::Type src,T* dst){
//把其中一项拷贝给所对应的另一项
*dst=src;
}
static void copy_n(T const* dst,std::size_t n){
//把其中n项拷贝给其他n项
for(std::size_t k=0;k<n;++k){
dst[k]=src[k];
}
}
static void copy_init(typename RParam<T>::Type src,void *dst){
//拷贝一项到未进行初始化的存储空间
::new(dst) T(src);
}
static void copy_init_n(T const* src,void* dst,std::size_t n){
//拷贝n项到未进行初始化的存储空间
for(std::size_t k=0;k<n;++k){
::new((void*)((char*)dst+k)) T(src[k]);
}
}
static void swap(T* a,T* b){
//交换其中两项
T tmp(*a);
*a=*b;
*b=tmp;
}
static void swap_n(T* a,T* b,std::size_t n){
//交换n项
for(std::size_t k=0;k<n;++k){
T tmp(a[k]);
a[k]=b[k];
b[k]=tmp;
}
}
static void move(T* src,T* dst){
//移动一项到另一项所在的位置
assert(src!=dst);
*dst=*src;
src->~T();
}
static void move_n(T* src,T* dst,std::size_t n){
//移动n项到另n项所在的位置
assert(src!=dst);
for(std::size k=0;k<n;++k){
dst[k]=src[k];
src[k].~T();
}
}
static void move_init(T* src,void* dst){
//移动一项到未初始化的存储空间
assert(src!=dst);
::new(dst) T(*src);
src->~T();
}
static void move_init_n(T const* src,void* dst,std::size_t n){
//移动n项到未初始化的存储空间
assert(src!=dst);
for(std::size_t k=0;k<n;++k){
::new((void*)((char*)dst+k)) T(src[k]);
src[k].~T();
}
}
};
//针对更快的对象位元拷贝而实现的局部特化
template<typename T>
class BitOrClassCSM<T,true> : public BitOrClassCSM<T,false>{
public:
static void copy_n(T const* src,T* dst,std::size_t n){
//拷贝n项到其他的对象
std::memcpy((void*)dst,(void*)src,n);
}
static void copy_init_n(T const* src,void* dst,std::size_t n){
//拷贝n项到未初始化的存储空间
std::memcpy(dst,(void*)src,n);
}
static void move_n(T* src,T* dst,std::size_t n){
//移动n项到其他对象的n项
assert(src!=dst);
std::memcpy((void*)dst,(void*)src,n);
}
static void move_init_n(T const* src,void* dst,std::size_t n){
//移动n项到未初始化的存储空间
assert(src!=dst);
std::memcpy(dst,(void*)src,n);
}
};
//根据“是类类型还是非类型“的策略来实现拷贝
template <typename T>
class CSMtraits : public BitOrClassCSM<T,IsClassT<T>::No >{
};
#endif


这里对非类类型使用位元拷贝,对类类型使用对象安全拷贝(非位元拷贝)。位元拷贝的特化BitOrClassCSM<T,true>继承了非位元拷贝的特化BitOrClassCSM<T,false>,这样就会隐藏父类中签名相同的函数版本,在应用时就会使用自己的实现版本。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: