第二章:C++泛型机制的基石——数据类型表
2.1 类模板的公有数据类型成员
2.1.1 类的数据类型成员
C++类中不仅可以定义数据成员和函数成员,而且还可以定义数据类型成员。在泛型设计中,类的数据类型成员是一个常用的感念。所谓类的数据类型成员,就是在一个类中使用typedef定义一个已知数据类型的别名。例如:
typedef long double LDBL
在C++中,这种在类模板中定义的数据类型也称nested type(嵌入式类型)。既然nested type与字段、方法都属于类成员,那么当然可以为它们赋予响应的访问属性。于是,这些具有public属性的类型成员就成为类外部模块可以使用的公有类型成员。显然,外部模块就可以通过这些公有类型成员,实现与类模板之间的功能协作。
template<typename T1, typename T2> class MyTraits{ public: typedef T1 my_Type1; typedef T2 my_Type2; }; int main(int argc, char** argv){ //类外引用类模板的公有类型成员,与引用静态成员一样。 //为了区别,需要加上typename关键字 typename MyTraits<int, double>::my_Type1 t1; typename MyTraits<int, double>::my_Type2 t2; }
2.1.2 再谈typedef
typedef字面的意思是类型定义,或定义类型。但是它不能凭空定义一个类型,它只能为某个现有类型(系统固有的、用户自定义的)命名一个别名。人们常常使用别名的原因相同,那就是在某些时候使用别名会带来极大的好处。
- 增加程序的可读性。
可以通过给某一数据类型命名别名的方法说明这个数据类型在应用中的作用。
typedef int Phone; typedef int Age; //很明显wangxin_P这个变量就是用来存储电话的! Phone Wangxin_P;
- 为冗长复杂的类型命名简单的别名。
/* 一个数组,数组存放的是: 函数指针:{ 参数:指针函数:{ 参数:空; 返回值:空; } 返回值:空; } */ void ( *b[10] )( void(*)() ); //pFunParam是一个函数指针,指向参数为空,返回值为空的函数类型。 typedef void(* pFunParam) (); //pFunx是一个函数指针,指向参数为别名为pFunParam的类型,返回值为空。 typedef void(*pFunx) (pFunParam); void (*b[10])(void(*)()); pFunx b[10]; /* 以上两者等价: 声明一个数组b,具有十个元素,元素类型是: 函数指针:{ 参数:函数指针:{ 参数:空; 返回值:空; } 返回值:空; } */
- 编写平台无关代码(跨平台)
最值得称道的 typedef 应用莫过于编写平台无关代码。具体做法是使用类名别名编程,例如使用 UINT32 这个别名定义程序中的32位无符号整型变量,至于这个别名的真实类型名则根据运行这个代码的平台使用 typedef 确定。也就是说,在更换运行平台时,源代码不需要进行改动!!!!
//平台A typedef int UINT32; //平台B typedef unsigned int UINT32;
2.2 内嵌式数据类型表及数据类型衍生
为方便使用,一个类模板会把用 typedef 定义的所有公有数据类型集中形成一个数据类型表,并放在模板中靠前的位置。这个数据类型表存在于类模板内部,所以叫内嵌式数据类型表。
为了充分利用从模板参数中获得数目有限的数据类型,一个模板通常会尽可能利用那些从模板参数获得的类型资源,除了把通过模板参数传递过来的类型定义成公有成员之外,也把它们的衍生类型也一并定义成公有成员。
//有人把这种数据类型表形象地称为类型榨取机,或者类型萃取机 template<typename T> struct map{ typedef T value_type; typedef T& reference; typedef T* pointer; }
2.3 数据类型表
2.3.1 数据类型表的概念
在一个类模板中,如果其全部成员都是公有数据类型,那么相对于嵌入式数据类型表,这个类模板就叫独立数据类型表,简称数据类型表。
这种表通常被用于规范模板类型表。假如有一个项目,要求项目中的模板都必须至少使用两个参数,而且必须向外提供这两个参数的值类型及引用类型,那么为了说明这个要求,项目负责人就可以写一个如下独立数据类型表。
template<typename T, typename U> class TypeTb1{ public: typedef T value_type1; typedef T& reference1; typedef U value_type2; typedef U& reference2; };
然后要求其他人所设计的类模板必须继承自这个独立类型表。于是凡是继承了 TypeTb1 的类就都有一张与 TypeTb1 完全相同的类型表。简单地说,这种独立类型表就是一种接口,是一种类型接口。
template<typename T, typename U> class A:public TypeTb1<T, U> { //类A的设计内容 }; //例如STL使用binary类模板描述了它对标准二元函数(有两个形参的函数)的要求 template<class _A1, class _A2, class _R> struct binary { typedef _A1 Arg1; typedef _A2 Arg2; typedef _R Rtn; }
2.3.2 数据类型表的应用
template<typename T> class Num:public TypeTb1<T> { //继承了TypeTb1 }; //普通测试函数,可以发布Num<int>的数据类型表 //缺点,重用性差,每次都需要修改模板参数 //事实证明编译器已经只能到可以省略显式调用数据类型表时的typename关键字了 void Tfunction(Num<int>::value_type x, Num<int>::reference y, Num<int>::pointer z){ cout<<"x="<<x<<endl; cout<<"y=&x="<<y<<endl; cout<<"*z="<<z<<endl; } //测试函数模板,注意: //1.Num不是类型,Num<int>才是类型。 //2.需要使用关键字typename。隐式调用需要关键字。 template<typename T> void Tfunc(typename T::value_type x, typename T::reference y, typename T::pointer z){ cout<<"x="<<x<<endl; cout<<"y=&x="<<y<<endl; cout<<"*z="<<z<<endl; } int main(int argc, char** argv){ //显式调用时,typename关键字可以省略 typename Num<int>::value_type a = 100; //调用普通测试函数 Tfunction(a, a, &a); //调用测试函数模板,类型为Num<int>,而不是Num,Num类模板。 Tfunc<Num<int>>(a, a, &a); return 0; }
2.4 特化数据类型表
众所周知,数据类型表就是一种类模板,而由特化类模板形成的数据类型表就是特化类型表。特化类型表的作用之一就是为了实现同一业务逻辑不同接口的统一。
class Test1{ public: char compute(int x, double y){ return x; } }; class Test2{ public: int compute(double x, double y){ return x; } }; //Test1和Test2两个类的逻辑相同,只是接口不同 //出于代码重用的方式,我们可以写成一个类模板 template<typename Arg1, typename Arg2, typename Ret> class Test{ public: Ret compute(Arg1 x, Arg2 y){ return x; } }; //这种方法的缺点就是我们需要在声明的时候给出三个模板参数 Test<int, double, char> t1; //但是如果只是区分两个类,不考虑其他参数类型情况,我们不必如此麻烦 //我们可以使用一套统一的数据类型表,作为类的接口 template<typename T> class TypeTb1{ typedef int ret_type; //返回值数据类型 typedef int par1_type; //参数1的数据类型 typedef int par2_type; //参数2的数据类型 }; //声明两个空类,作为两个标识符,注意Test1和Test2都是类型 class Test1; class Test2; //特例化数据类型表,当模板参数为Test1类时 template<> class TypeTb1<Test1>{ typedef char ret_type; //返回值数据类型 typedef int par1_type; //参数1的数据类型 typedef double par2_type; //参数2的数据类型 }; template<> class TypeTb1<Test2>{ typedef int ret_type; //返回值数据类型 typedef double par1_type; //参数1的数据类型 typedef double par2_type; //参数2的数据类型 }; //使用统一数据类型表作为接口,声明的类模板 template<typename T> class Test{ public: //当模板参数为Test1或者Test2时,生成特例化类型表 //所以compute函数的参数与返回值类型跟着改变 typename TypeTb1<T>::ret_type compute( typename TypeTb1<T>::par1_type x, typename TypeTb1<T>::par2_type y){ return x; } } //成功通过一个模板参数区分了两个类 Test<Test1> t1; Test<Test2> t2;
2.5 STL 中的Traits表
Traits 实际上是特化数据类型表在STL中的一个具体应用,也是数据类型表。但是因为他构思巧妙,备受推崇,人称Traits技巧。
- 应用背景
在STL中有一种具有指示数据位置功能的对象,叫做迭代器( iterator ),它们大多是类模板,并在模板中包含内嵌数据类型表,从而能向外提供模板参数传递过来的数据类型及其衍生类型的别名给用户。这些 Iterator 类模板代码大体如下
template<typename T> class Iterator_1{ public: typedef T value_type; typedef value_type* pointer; typedef value_type& reference; //其余代码··· } //Iterator_2, Iterator_3 ......
- 使用特化模板实现指针的数据类型表
众所周知,C/C++指针也是用来指示数据位置的,因此也属于迭代器范畴,所以为了类型接口的统一,那么指针也应该能向外提供数据类型。但很遗憾,指针是C/C++的原生类型,不是类或者类模板,不可能有内嵌数据类型表,所以必须想别的方法满足 STL 的要求。
指针之所以不能向外提供数据类型,原因就在于它不是一个类,没有用于建立数据类型表的地方,所以首先要为指针配备一个可以容纳指针数据类型表的类模板。显然,为了使应用程序以 T* 类型找到这个类模板,这应该是一个如下形式的特化模板:
//基础模板 template<typename T> struct Traits{ //空表 } //指针的特化模板 template<typename T> struct Traits<T*>{ typedef T value_type; typedef value_type* pointer; typedef value_type& reference; } //Traits表特别体现了数据类型表的“类型压榨”能力 //即它不仅能把用户经由模板参数传递来的一个数据类型的所有衍生类型都能提取出来, //而且还可以把衍生类型的原生数据类型压榨出来。 Traits<double*>::value_type t1 = 100; cout<<t1<<endl;
- 汇总同类类模板的内嵌数据类型表形成统一接口
使用特化数据类型表解决了原生类型的数据类型表的问题,但各个迭代器的数据类型表还是分散于各自迭代器类中的,不利于管理。因此希望有一个同一类事物(这里是迭代器)数据类型表的总表。
于是 STL 的设计者看到了 Traits 基础模板的那个空表。如果将各个迭代器模板中的数据类型表内容稍加改变后复制到这个空表中不是挺好的吗!那 Traits 就是迭代器类型表的总表。于是 Traits 的空表就变成了下面的样子:
//这里的模板参数T的实参将来都是各个迭代器类以及T*。 template<typename T> struct Traits{ //因为编译器不认识带有域分隔符::的类型,因此这里需要使用关键字typename typedef typename T::value_type value_type; //隐式调用数据类型成员 typedef typename T::pointer pointer; typedef typename T::reference reference; } //指针特化模板与之前一样。 //测试如下,由此可见数据类型表都变成了Traits,统一了接口 Traits<Iterator_1<int>>::value_type t1 = 100; Traits<Iterator_2<double>>::value_type t2 = 9.23; Traits<double*>::value_type t3 = 3.33;