泛型编程与设计新思维
作者:徐景周
永远记住,编写代码的宗旨在于简单明了,不要使用语言中的冷僻特性,耍小聪明,重要的是编写你理解的代码,理解你编写的代码,这样你可能会做的更好。
1998年,国际C++标准正式通过,标准化对C++最重要的贡献是:对"强大的抽象概念"给于更有力的支持,以降低软件的复杂度,C++提供了二种功能强大的抽象方法:面向对象编程与泛型编程。面向对象编程大家一定很熟悉了,这里就不再哆嗦了。提到泛型编程(Generic Programming),有的人可能还不太熟悉,但是提到STL,你就一定会有所耳闻了。STL(Standard Template Library,标准模板库) 其实就是泛型编程的实现品,STL是由Alexander Stepanov(STL之父)、David R Musser和Meng Lee三位大师共同发展,于1994年被纳入C++标准程序库。STL虽然加入C++标准库的时间相对较晚,但它却是C++标准程序库中最具革命性的部分,同时也是C++标准程序库中最重要的组成部分。由于新的C++标准库中几乎每一样东西都是由模板(Template)构成的,当然,STL也不会例外。所以,在这里有必要先概要说明一下模板的有关概念。
模板概念
通过使用模板可以使程序具有更好的代码重用性。记住,模板是对源代码进行重用,而不是通过继承和组合重用对象代码,当用户使用模板时,参数由编译器来替换。模板由类模板和函数模板二部分组成,以所处理的数据类型的说明作为参数的类就叫类模板,而以所处理的数据类型的说明作为参数的函数叫做函数模板。模板参数可以由类型参数或非类型参数组成,类型参数可用class和typename关键字来指明,二者的意义相同,都表示后面的参数名代表一个潜在的内置或用户定义的类型,非类型参数由一个普通参数声明构成。下面是类模板和函数模板的简单用法:
template泛型编程class Queue // 类模板,其中T1为类型参数,Size为非类型参数 { public: explicit Queue():size_(Size){}; // 显式构造,避免隐式转换 …… template void assign(T2 first,T2 last); // 内嵌函数模板 private: T* temp_; int size_; } // 类模板中内嵌函数模板Compare的外围实现(如在Queue类外实现) template template void Queue ::assign (T2 first,T2 last) {}; // 模板的使用方法 int ia[4] = {0,1,2,3}; Queue qi; qi.assign(ai,ai+4);
泛型编程和面向对象编程不同,它并不要求你通过额外的间接层来调用函数,它让你编写完全一般化并可重复使用的算法,其效率与针对某特定数据类型而设计的算法相同。泛型编程的代表作品STL是一种高效、泛型、可交互操作的软件组件。所谓泛型(Genericity),是指具有在多种数据类型上皆可操作的含意,与模板有些相似。STL巨大,而且可以扩充,它包含很多计算机基本算法和数据结构,而且将算法与数据结构完全分离,其中算法是泛型的,不与任何特定数据结构或对象类型系在一起。STL以迭代器(Iterators)和容器(Containers)为基础,是一种泛型算法(Generic Algorithms)库,容器的存在使这些算法有东西可以操作。STL包含各种泛型算法(algorithms)、泛型指针(iterators)、泛型容器(containers)以及函数对象(function objects)。STL并非只是一些有用组件的集合,它是描述软件组件抽象需求条件的一个正规而有条理的架构。
迭代器(Iterators)是STL的核心,它们是泛型指针,是一种指向其他对象(objects)的对象,迭代器能够遍历由对象所形成的区间(range)。迭代器让我们得以将容器(containers)与作用其上的算法(algorithms)分离,大多数的算法自身并不直接操作于容器上,而是操作于迭代器所形成的区间中。迭代器一般分为五种:Input Iterator、Output Iterator、Forward Iterator、Bidirections Iterator和Random Access Iterator。Input Iterator就象只从输入区间中读取数据一样,具有只读性,属于单向移动,如STL中的istream_iterator。Output Iterator刚好相反,只写出数据到输出区间中,具有只写性,属于单向移动,如STL中的ostream_iterator。Forward Iterator也属于单向移动,但不同之处是它同时具有数据读、写性。Bidirections Iterator如名称暗示,支持双向移动,不但可以累加(++)取得下一个元素,而且可以递减(--)取前一个元素,支持读、写性。Random Access Iterator功能最强,除了以上各迭代器的功能外,还支持随机元素访问(p+=n),下标(p[n])、相减(p1-p2)及前后次序关系(p1
template2、ForwardIteratorvoid advance(InputIterator& i, Distance n, input_iterator_tag) { for(; n>0; --n,++i){} // InputIterator具有++性 }
template3、BidirectionalIteratorvoid advance(ForwardIterator& i, Distance n,forward_iterator_tag) { advance(i, n, input_iterator_tag()); }
template4、RandomAccessIteratorvoid advance(BidirectionalIterator& i, Distance n, bidirectional_iterator_tag) { if(n>=0) // 具有++、--性 for(; n>0; --n,++i){} else for(; n>0; ++n,--i){} }
template函数对象(Function object)也称仿函数(Functor),是一种能以一般函数调用语法来调用的对象,函数指针(Function pointer)是一种函数对象,所有具有operator()操作符重载的成员函数也是函数对象。函数对象一般分为无参函数(Generator),单参函数(Unary Function)和双参函数(Binary Function)三种形式,它们分别能以f()、f(x)和f(x,y)的形式被调用,STL定义的其他所有函数对象都是这三种概念的强化。如下简单示例展示几种形式的实现:void advance(RandomAccessIterator& i, Distance n, random_access_iterator_tag) { i += n; // 具有++、--、+=等性 }
1、无参(Generator)形式
struct counter { typedef int result_type; counter(result_type init=0):n(init){} result_type operator() { return n++;} result_type n; }2、单参(Unary Function)形式
template3、双参(Binary Function)形式struct even // 函数对象even,找出第一个偶数 { bool operator()(Number x) const {return (x&1) == 0;} } // 使用算法find_if在区间A到A+N中找到满足函数对象even的元素 int A[] = {1,0,3,4}; const int N=sizeof(A)/sizeof(int); find_if(A,A+N, even ());
struct ltstr { bool operator()(const char* s1, const char* s2) const { return strcmp(s1容器(container)是一种对象(object),可以包含并管理其它的对象,并提供迭代器(iterators)用以定址其所包含之元素。根据迭代器种类的不同,容器也分为几中,以Input Iterator为迭代器的一般container,以Forward Iterator为迭代器的Forward Container,以Bidirectional Iterator 为迭代器的Reversible Container,以Random Access Iterator为迭代器的Random Access Container。STL定义二种大小可变的容器:序列式容器(Sequence Container)和关联式容器(Associative Container)序列式容器包括vector、list和deque,关联式容器包括set、map、multiset和multimap。以下示例简单说明部分容器的使用:A(a,a+N); set B(b,b+N); set_union(A.begin(),A.end(),B.begin(),B.end(), ostream_iterator (cout," "), ltstr());
1、vector使用 // 将A中以元素5为分割点,分别排序,使排序后5后面的元素都大于5之前的元素(后区间不排序), // 然后输出 int main() { int A[] = {7,2,6,4,5,8,9,3,1}; const int N=sizeof(A)/sizeof(int); vector设计新思维V(A,A+N); partial_sort(V,V+5,V+N); copy(V,V+N,ostream_iterator (cout," ")); cout <<> L1; L1.push_back(0); L1.push_front(1); L1.insert(++L1.begin,3); L1.sort(); copy(L1.begin(),L1.end(),ostream_iterator (cout," ")); } 输出:0 1 3 3、deque使用 int main() { deque Q; Q.push_back(3); Q.push_front(1); Q.insert(Q.begin()+1,2); Copy(Q.begin(),Q.end(),ostream_iterator (cout," ")); } 输出:1 2 3 4、map使用 int main() { map M; M.insert(make_pair("A",11); pair
将设计模式(design patterns)、泛型编程(generic programming)和面向对象编程(object-oriented programming)有机的结合起来,便形成了设计新思维。其中,设计模式是经过提炼的出色设计方法,对于很多情况下碰到的问题,它都是合理而可复用的解决方案;泛型编程是一种典范(paradigm),专注于将类型抽象化,形成功能需求方面的一个精细集合,并利用这些需求来实现算法,相同的算法可以运用于广泛的类型集中,所谓泛型,就是具有在多种数据类型上皆可操作的含意;最后同面象对象编程中的多态(polymorphism)和模板(templates)等技术相结合,便获得极高层次上的具有可复用性的泛型组件。泛型组件预先实现了设计模块,可以让用户指定类型和行为,从而形成合理的设计,主要特点是灵活、通用和易用。
policies和policy类,是一种重要的类设计技术,所谓policy,是用来定义一个类或类模板的接口,该接口由下列之一或全部组成:内部类型定义、成员函数和成员变量。基于policy的类由许多小型类(称为policies)组成,每一个这样的小型类只负责单纯如行为或结构的某一方面。Policies机制由模板和多重继承组成,它们可以互相混合搭配,从而形成设计戎的多样性,通过plicy类,不但可以定制行为,也可以定制结构。
下面简单举例说明泛化思维和面向对象思维在部分设计模式中的运用。
Singletons设计模式泛化实现 Singleton模式是一种保证一个对象(class)只有一个实体,并为它提供一个全局访问点。Singleton是一种经过改进的全局变量,既在程序中只能有唯一实体的类型,它的重点主要集中在产生和管理一个独立对象上,而且不允许产生另一个这样的对象。
先让我们看看一般的C++实现的基本手法,下面是实现源码:
// Singleton.h文件中 class Singleton { public: static Singleton& Instance() { if(!pInstance_){ if(destroyed_){ // 引用是否已经失效 OnDeadReference(); } else { Create(); // 第一次时创建实例 } } return *pInstance_; } private: Singleton(); // 禁止默认构造 Singleton(const Singleton&); // 禁止拷贝构造 Singleton& operator= (const Singleton&); // 禁止赋值操作 static void Create() // 传加创建的实例引用 { static Singleton theInstance; pInstance_ = &theInstance; } static void OnDeadReference() { throw std::runtime_error(" 实例被不正当消毁"); } virtual ~Singleton() { pInstance- = 0; destroyed_ = true; } static Singleton *pInstance_; static bool destroyed_; } // Singleton.cpp中静态成员变量初始化 Singleton* Singleton::pInstance_ = 0; Bool Singleton::destroyed_ = false;如上所示,Singleton模式实现中只有一个public成员Instance()用来第一次使用时创建单一实例,当第二次使用时静态变量将已经被设定好,不会再次创建实例。还将默认构造函数、拷贝构造函数和赋值操作符放在private中,目地是不让用户使用它们。另外,为避免实例意外消毁后再实例化情况,加入静态布尔变量destroy_来进行判断是否出错,从而达到稳定性。
从上面一般实现可以看出Singleton模式实现主要在于创建(Creation)方面和生存期(Lifetime)方面,既可以通过各种方法来创建Singleton。Creation必然能创建和摧毁对象,必然要开放这两个相应函数,将创建作为独立策略分离开来是必需的,这样你就可以创建多态对象了,所以泛化Singleton并不拥有Creator对象,它被放在CreationPolicy
下面是一个简单的泛化Singleton模式的实现(不考虑线程因素)
template < class T, templateInstance()是SingletonHolder开放的唯一一个public函数,它在CreationPolicy、LifetimePolicy中打造了一层外壳。其中模板参数类型T,接收类名,既需要进行Singleton的类。模板参数内的类模板缺省参数CreateUsingNew是指通过new操作符和默认构造函数来产生对象,DefaultLifetime是通过C++规则来管理生命期。LifetimePolicycalss CreationPolicy = CreateUsingNew, template class LifetimePolicy=DefaultLifetime, > classs SingletonHolder { public: static T& Instance() { if(!pInstance_) { if(destroyed_) { LifetimePolicy ::OnDeadReference(); destroyed_ = false; } pInstance_ = CreationPolicy ::Create(); LifetimePolicy ::SchedultCall(&DestorySingleton); } return *pInstance_; } private: static void DestroySinleton() { assert(!destroyed_); CreationPlicy ::Destroy(pInstance_); pInstance_ = 0; destroyed_ = true; } SingletonHolder(); SingletonHolder (const SingletonHolder &); SingletonHolder & operator= (const SingletonHolder &); Static T* pInstance_; Static bool destroyed_; };
下面是上述泛化Singleton模式实现的使用:
1、应用一
class A{}; typedef SingletonHolder SingleA;2、应用二
class A{}; class Derived : public A {}; template通过上面示例可以看出, SingletonHolder采用基于plicy设计实现,它将Singleton对象分解为数个policies,模板参数类中CreationPolicy和LifetimePolicy相当于二个policies封装体。利用它们可以协助制作出使用者自定义的Singleton对象,同时还预留了调整和扩展的空间。由此而得,泛型组件(generic components),是一种可复用的设计模板,结合了模板和模式,是C++中创造可扩充设计的新方法,提供了从设计到代码的简易过渡,帮助我们编写清晰、灵活、高度可复用的代码。struct MyCreator : public CreateUsingNew { static T* Create() { return new Derived; } static void Destroy(T* pInstance) { delete pInstance; } } typedef SingletonHolder SingleA;
递归之美 - Loki库TypeList源码剖析
邓 辉
TypeList概观
提起List,想必大家都不会陌生,它是一个元素的集合,并且提供了一些对该集合进行操作的方法,比如:计算集合中元素的个数、向集合中添加元素、获取给定索引处的元素等。我们所熟知的List中的元素一般都是实例化后的值,相关的操作也都是在运行期间进行的。本文将要剖析的List和上述的List在某种意义上比较相象,不过它所包含的元素都是类型(type),相关的操作是在编译期间进行的。
本文将要讲述的TypeList取自Andrei Alexandrescu的力作《Modern C++ Design》一书相关的Loki库,关于《Modern C++ Design》,C++的爱好者想必不会陌生,在该书中,作者向我们展现了C++设计的全新思维,把C++的表达能力发挥到了极至,而Loki库正是这种思维的具体表现。TypeLsit是Loki库中最为基础、核心的组件,理解了TypeList就具备了观赏这道C++新景观的基础。
下面我们先来看看如何定义一个TypeList,这样可以对于TypeList先有一个感性的认识。
typedef TYPELIST_3(char, int, long) MyTypeList;
定义了一个具有三个元素的TypeLsit,这三个元素分别为:char、int、long。TYPELIST_3为Loki库中提供的用于定义TypeList的工具,我们会在本文的后面进行介绍。
::Loki::Length
计算MyTypeList中元素的个数,结果为3。
typedef ::Loki::TypeAt
获取MyTypeList中第1个元素(从0开始),此时MyType就是int。
typedef ::Loki::Append
向MyTypeList中在添加一个元素:float,结果为MyTypeList1。此时::Length
好了,先介绍这么多,下面我们将介绍TypeList实现的一些相关的背景知识,包括:递归的基本概念、模板的特化(tempalte specilization)、模板的偏特化(template partial specilization)以及类型萃取技术(type traits)。
TypeList相关技术
递归概述
对于递归,大家肯定都不陌生,使用递归方法给出的解决方案总是显得非常的优雅、简洁。不过递归方法所适合解决的问题应该符合下面的条件:
- 一个问题的解决依赖于一个较小规模的同样的问题的解决
- 必须有一个明确的结束条件
- 这个结束条件是可达的
如果一个问题符合上述的三个条件,我们就可以使用递归的方法。首先我们定义一套解决问题的规则,接着缩小问题的规模并应用同样的规则直到达到结束条件,然后结果层层返回直到原始问题。著名的汉诺塔问题就是一个典型的递归问题,如果不使用递归方法,解决汉诺塔问题就会显得非常的复杂,晦涩。
我们在使用递归方法设计程序时,这个递归过程的调用总是在运行期间进行的。本文所介绍的TypeList的实现中,递归的执行是在编译期间进行的,那么在编译期间如何定义每次递归的返回结果,如何定义结束条件呢?其中主要使用了下面将要介绍的模板特化、偏特化以及类型萃取技术。
模板特化和偏特化(template specilizaiton、partial specilization)
什么是模板的特化、偏特化呢?大致的意思为:如果一个template拥有一个或者一个以上的template参数,我们可以针对其中一个或者多个参数进行特化处理(如果全部进行特化处理就是全特化,否则就是偏特化,切记:函数模板只能进行全特化,不能进行部分特化)。也就是说,我们可以提供一个特别版本,符合泛化条件,但是其中某些(全部)template参数已经由实际类型或者数值取代。
假设我们有一个class template定义如下:
template
class C { … };
对于模板的偏特化,刚刚接触可能会存在一些误解:以为模板的偏特化版本一定是对template参数U或者V或者T(或者它们的任意组合)指定某个参数值。其实不是这样的,所谓模板的偏特化是指另外提供一份template的定义,它的具体含义可以和通用的template定义版本无关。在一个偏特化版本中,template 参数的个数并不需要吻合通用的 template 中的个数。然而,出现於于class 名称之后的参数个数必须吻合通用的 template 的参数个数。下面举一个简单的例子进行说明:
template
class C { … };
这个泛型版本允许T为任意的类型。它的一个偏特化版本如下:
template
class C
这个偏特化版本仅适用于T为原生指针类型的情况。
当我们使用C
类型萃取(type traits)
类型萃取技术是泛型程序设计中的一个常用技术,它的思维核心为:把一系列与类型相关的性质包裹于单一的class 之内,这样我们就可以在编译期间获取一些所需要的和该类型相关的东西。其实这个思路就是软件领域一句著名的谚语:“任何事情都可以通过添加额外的中间层次得以解决”的又一次体现。通过把一系列想得到的类型相关的信息封装在另外一个类型定义中,这样就可以以一致的方式来对这些类型进行处理,提供了强大的可复用性和灵活性。
类型萃取技术一般都和模板的特化、偏特化技术结合在一起运用,这样它们就可以互相补充发挥出巨大的威力。下面简单举一个例子来了解一下类型萃取技术。
我们来看看Boost库中一个简单的template
template
struct is_pointer
{ static const bool value = false; };
template
struct is_pointer
{ static const bool value = true; };
一个简单的示例如下:
template
void Func(T param)
{
if (is_pointer
// do something
}
else {
//do something
}
…
}
通过类型萃取技术,我们就可以在编译期间保留每次递归的结果,供递归返回时使用。
关于这些技术的更多、更深入的介绍,请读者自行参考相关资料,不在此赘述。在下面的源码剖析中,读者将会看到这些技术的实际运用。
TypeList实现剖析
有了上述的背景知识,下面我们就来揭开TypeList的神秘面纱,走进TypeList的源码世界。首先,我们来看一下TypeList的定义。
TypeList定义
为了能够一致的进行TypeList的操作,在Loki库中定义了一个空类型NullType来标记一个TypeList的结束,NullType和TypeList的定义如下:
class NullType { };
template
struct Typelist
{
typedef T Head;
typedef U Tail;
};
对于规范型TypeList的定义采用了一种尾递归的方法:
- NullType是规范的TypeLsit
- 如果T是规范的TypeList,那么对于任意原子类型U,TypeList,T>是规范的TypeList
Loki库中所采用的TypeList均为规范型的TypeList,这样可以在不减少灵活性的前提下简化对于TypeList的操作。本文后面所指的TypeList均为规范型的。
如何定义一个TypeList呢?比如:包含:char、int、long三个元素的TypeLsit。根据上面的定义,可以得到如下的定义形式:
TypeList
// 然编译器会认为是 “>>” 操作符
这样的定义方法显得比较麻烦、罗嗦,为了简化用户对于TypeList的使用,Loki库中采用了宏定义的方式对于大小在1~50的TypeList进行了预定义:
#define TYPELIST_1(T1) ::Loki::Typelist
#define TYPELIST_2(T1, T2) ::Loki::Typelist
#define TYPELIST_3(T1, T2, T3) ::Loki::Typelist
…
依此类推。
这样用户在使用起来就会比较方便一些。
TypeList典型操作实现
了解了TypeList的定义,这里我们将对于TypeList相关的三个典型操作(Length、TypeAt和Append)的实现进行详细的剖析。掌握了这几个典型的操作,再学习其他的操作就会变得非常的容易。
我们将通过一个实例进行讲解,来看一下编译器实际的运作过程。我们定义了一个包含两个元素:int以及long的TypeList。
typedef TYPELIST_2(int, long) MyTypeList;
此时,编译器会根据TypeList的定义方式产生如下的类型定义结果:
struct TypeList
{
typedef long H;
typedef NullType T;
};
struct TypeList
{
typedef int H;
typedef TypeList
};
Length的实现 - 获取TypeList中的元素个数:
template
// 的话,会产生一个编译期错误
template <> struct Length
{ // 模板特化和类型萃取技
// 术
enum { value = 0 };
};
template
struct Length<> >
{
enum { value = 1 + Length::value };
};
当通过Length
struct Length
{
enum { value = 1 + Length
};
struct Length
{
enum { value = 1 + Length
};
根据Length结束条件的定义可知,Length
TypeAT的实现 - 获取给定位置处的元素
template
// 的类型不是TypeLsit
// 的话,会产生一个编译期错误
template
struct TypeAt
{ // 返回TypeList中的第一个元素
typedef Head Result;
};
template
struct TypeAt
{ // 用是告诉编译器其后的实体是类型。
typedef typename TypeAt
};
下面看看使用TypeAt
struct TypeAt
{
typedef long Result;
};
struct TypeAt
{
typedef TypeAt
};
很明显typedef TypeAt
Append的实现 - 在TypeList的末尾添加一个元素
template
// 的类型不是TypeLsit
// 的话,会产生一个编译期错误
template <> struct Append
{
typedef NullType Result;
};
template
{
typedef TYPELIST_1(T) Result;
};
template
struct Append
{
typedef Typelist
Result;};
template
struct Append
{ // 用是告诉编译器其后的实体是类型。模板的偏特
// 化
typedef Typelist
typename Append
Result;
};
同样的,让我们来看看当使用Append
struct Append
{
typedef TYPELIST_1(float) Result;
};
struct Append
{
typedef TypeList
};
struct Append
{
typedef TypeList
};
经过简单的替换,Append
typedef TypeList
等于:
typedef TypeList
等于:
typedef TypeList
也就是:
TYPELIST_3(int, long, float) ;等同于在原有TypeList的末尾添加了一个新元素float。不用多说了,这里类型萃取技术同样发挥了巨大的作用。
结束语
本文对于TypeLsit的实现进行了剖析,相信读者朋友对于TypeList含义以及实现手法已经有所掌握。那么TypeLsit到底有什么用处呢?源码面前,了无秘密,掌握了TypeList,就掌握了全面理解Loki库的钥匙。在Loki库中,你将会看到Abstract Factory模式、Visitor模式这些泛型组件是如何在TypeLsit的基础上搭建起来的。背起你的行囊,拿起这把钥匙,赶快踏上你的“寻宝”之路吧(Loki库的源代码可以从www.moderncppdesign.com下载)。
没有评论:
发表评论