龙盟编程博客 | 无障碍搜索 | 云盘搜索神器
快速搜索
主页 > 软件开发 > C/C++开发 >

Visual C++泛型编程实践[组图]

时间:2009-12-22 15:42来源:未知 作者:admin 点击:
分享到:
泛型程序设计(Generic Programming) 是建立在C++的Template机制基础上的一种完全不同于面向对象的程序设计思维模式,STL是泛型概念的一套实作产品。Loki是一个与Boost齐名的开放源码的C++程序

  泛型程序设计(Generic Programming) 是建立在C++的Template机制基础上的一种完全不同于面向对象的程序设计思维模式,STL是泛型概念的一套实作产品。Loki是一个与Boost齐名的开放源码的C++程序库,它通过一些精巧的装置为常规C++开发提供了一些很有用的工具。

STL非常好用,弹性非常大,效率也很理想。目前几种主流的C++编译器均有相关的STL实现,而个人认为目前非常流行的Visualc++ 6.0平台中开发文档应用程序时,其文档序列化的功能非常好用,但由于其序列化能力建立在MFC之上,并不被STL支持,所以,如何既拥有STL的效率及通用性,又保留MFC的序列化能力,便成了VisualC++ 6.0平台上运用STL技术开发文档应用程序时不得不面对的一个问题,在这里我就以非常流行的Visual C++ 6.0+SP5平台结合一个假定的例子来介绍一下如何在Visual C++6.0中结合使用STL、Loki及模板技术来开发一个文档应用的开发历程,希望能对大家有所启发。

  

  示例

  

   先来简单介绍一下我所用到的例子:一个简单的商务进销存基本应用(不必关注细节),它应该包含:职员(Employee)、产品(ProdUCt)、仓库(Storage)、往来单位(Supply)、帐户(Account)、单据(Bill)等等,由于每种信息均应有唯一标识,所以我在这里选用STL中的map来表示如下(为了说明简单起见,我们只列两种):

  

  std::map<size,Employee*> itsEmployees;

  //职员表

  std::map<size,Product*> itsProducts;

  //产品表

  ..

  第一步:实现

  

   我们将以上map放入多(单)文档应用程序的文档类中,很显然,我们必须对每一个表至少提供以下三种最基本的操作:添加新成员函数、删除指定成员函数、获取指定成员函数。

  

   对于添加新成员,我们可以实现如下:

  

  size addAccountMember(Account* e); //添加帐户

  {

    //获取下一个可用的ID号

    size id=getNextAccountID();

    itsAccounts[id]=e;

    returnid;

  }

  size addEmployeeMember(Employee* e);//添加职员

  {

    //获取下一个可用的ID号

    size id=getNextEmployeeID();

    itsEmployees[id]=e;

    return id;

  }

  ..

   接下来的删除方法仅有一个size(唯一标识)参数,实现如下:

  

  void delAccount(size ID); //删除指定帐户

  {

  itsAccounts.erase(ID);

  }

  void delEmployee(size ID); //删除指定职员

  {

  itsEmployees.erase(ID);

  }

  ..

   获取指定成员的方法如下:

  

  Account* getAccountMember(size ID)

  //获取指定帐户

  {

  return itsAccounts[ID];

  }

  Employee* getEmployeeMember(size ID) //获取指定职员

  {

  return itsEmployees[ID];

  }

  ..

   另外,我们还要为每一个表提供一个获取下一个可用ID的成员函数:

  

  //获取下一个可用职员号

  Size getNextEmployeeID()

  {

    if(itsEmployees.empty())

   return 1;

    std::map<size,Employee*>::iterator it=itsEmployees.end();

    --it;

    return it->first+1;

  }

  //获取下一个可用帐户号

  Size getNextAccountID()

  {

    if(itsAccounts.empty())

    return 1;

    std::map<size,Account*>::iterator it=itsAccounts.end();

    --it;

    return it->first+1;

  }

  ..

  第二步:分析

  

   以上实现的确达到了我们的设计目的,但仅从直观上来看我就觉得它应该还有改善的空间,最简单的原因:因为它的命名混乱,没有通用性,如:

  

  addAccountMember, addEmployeeMember,..

  delAccount, delEmployee,..

  getAccountMember, getEmployeeMember,..

  getNextAccountID, getNextEmployeeID,..

   对于同一种功能存在这么多不同名称的函数想起来就让我感到可怕,在我们的这个简单的例子中只对6个表实现了三种功能,我们需要为每个表实现4种不同名称的函数,结果,我们需要记住4*6=24个不同名称的函数及它们所对应的功能,假如,假如我们要对更多的表实现更多的功能..,真的不敢相象我们到底要实现多少个不同名称的函数。我想,不用等到函数接口数量爆炸,我的脑子就先爆炸了。假如能够对同一种功能的函数使用一组相同的名字如:

  

  addMember

  delMember

  getMember

  getNextMemberID

  

   那么,我们的接口名称数量就只与实现的功能多少成常数关系,而与我们要操作的表的个数无关了,整个程序就应该清楚多了。

  第三步:改进(重构)

  

   重构是一个最近很流行的程序设计思想,说白了就是对已有程序进行改进,在不改变程序外在行为的前提下对程序结构及设计进行改进,以使程序代码更清楚、程序更健壮、更易于维护。

  

   第一次改进:使用函数重载减少接口名称数量对于添加成员,我们可以直接使用C++的函数重载技术改进如下:

  

  

size addMember(Account* e); //添加帐户

  {

    //获取下一个可用的ID号

    size id=getNextAccountID();

    itsAccounts[id]=e;

    return id;

  }

  size addMember(Employee* e); //添加帐户

  {

    //获取下一个可用的ID号

    size id=getNextEmployeeID();

    itsEmployees[id]=e;

    return id;

  }

   这样一来,消除了对不同表进行操作时调用的函数名称的差异,但我们可以看出,这两个函数的操作逻辑是完全一样的,变化的部分与参数相关,这正是模板技术可以发挥作用的地方,但如何将不同的表添加方法与不同的ID号获取方法及对应的map联系起来呢?

  

   我们再来看删除函数:由于不同表的删除方法均只有一个相同类型的参数size ID,而函数重载必须要有不同的参数列表,所以,要想实现一个void delMember(size ID)分别对应不同的表的删除操作好象是不可能的,getMember(size ID)方法也是一样,它对不同的表操作虽然有不同的返回值,但参数也是一样的,所以,也不能运用C++内的函数重载方法来实现函数接口命名的一致化。而获取下一个可用ID的函数方法甚至连参数都没有,怎么办呢?看来我们没有办法了。

  

   幸运的是,Andrei Alexandrescu在他的《 C++设计新思维――泛型编程与设计模式之应用》一书中为我们提供了一种解决办法: Type2Type――它是一个可用于代表参数类型,以让你传递给重载函数的轻量级的ID,其定义如下:

  

  Template <typename T>

  Struct Type2Type

  {

  typedef T OriginalType;

  };

   它没有任何数值,但其不同型别却足以区分各个Type2Type实体,而这正是我们所要的。现在,让我们来先解决addMember成员函数中的获取下一个可用ID号的函数,我们可以定义一个重载的函数如下:

  

  size getNextMemberID(Loki:: Type2Type<Employee>)

  //对应职员操作

  {

    if (itsEmployees.empty())

   return 1;

    std::map<size,Employee*>::iterator it=itsEmployees.

    end();

    --it;

    return it->first+1;

  }

  size getNextMemberID(Loki:: Type2Type<Account>)

  //对应帐户操作

  略..

   相应的,删除类函数定义如下:

  

  void delMember(size ID, Loki:: Type2Type<Account>)

  void delMember(size ID, Loki:: Type2Type<Employee>)

   获取类函数定义如下:

  

  Account* getMember(size ID, Loki:: Type2Type<Account>)

  Employee* getMember(size ID, Loki:: Type2Type<Employee>)

   这样,我们的函数接口就比刚开始的方法更清楚,我们的大脑中要记住的函数名就要少多了。

   第二次改进:使用模板技术减少接口函数数量经过第一次的改进,我们的接口结构比初始的方案要更清楚,但它似乎还存在一个问题:软件大师Martin Fowler在他的著作《重构――改善既有代码的设计》中将之列为代码的坏味道之首――代码重复。

我们可以看到,添加、删除、获取的函数实现中,几乎完全是一样的实现逻辑,只不过所操作的map变量不同而已,如下(以添加为例):

  

  size addMember(Account* e); //添加帐户

  {

    //获取下一个可用的ID号

    size id= getNextMemberID(Loki::Type2Type<Account>());

    itsAccounts[id]=e;

    return id;

  }

  size addMember(Employee* e); //添加帐户

  {

    //获取下一个可用的ID号

    size id=getNextMemberID(Loki::Type2Type<Employee>());

    itsEmployees[id]=e;

    return id;

  }

   假如我们能有办法根据不同的参数获得不同的要操作的map变量,那么这两个方法完全可以实现为一个模板方法如下:

  

  template<typename T>

  size addMember(T* e)

  {

    size empid=getNextMemberID(Loki::Type2Type<T>());

    //要害在于以下函数

    std::map<size,T*>& its=getMap(Loki::Type2Type<T>());

    its[empid]=e;

    return empid;

  }

   假如getMap()方法能实现,那么,我们的模板方法就可以成功。

  

有了前面的铺垫,这个应该水到渠成:

  

  std::map<size,Account*>& getMap(Loki::Type2Type<Account>)

  {

    return itsAccounts;

  }

  std::map<size,Employee*>& getMap(Loki::Type2Type

  <Employee>);

  {

    return itsEmployees;

  }

   这样我们就可以将所有的添加、删除、获取函数进行模板化实现如下:

  

  template <typename T>

  size getNextMemberID(Loki::Type2Type<T>)

  {

    std::map<size,T*>& its=getMap(Loki::Type2Type<T>());

    if (its.empty())

   return 1;

   std::map<size,T*>::iterator it=its.end();

   --it;

   return it->first+1;

  }

  template <typename T>

  size addMember(T* e)

  {

    size empid=getNextMemberID(Loki::Type2Type<T>());

    std::map<size,T*>& its=getMap(Loki::Type2Type<T>());

    its[empid]=e;

    return empid;

  }

  template <typename T>

  T* getMember(size memberID,Loki::Type2Type<T>)

  {

    std::map<size,T*>& its=getMap(Loki::Type2Type<T>());

    return its[memberID];

  }

  template <typename T>

  void delMember(size memberID,Loki::Type2Type<T>)

  {

    std::map<size,T*>& its=getMap(Loki::Type2Type<T>());

    its.erase(memberID);

  }

   这样,对于本例中6个表分别实现添加、删除、获取成员三组方法,我们总共需要用:四个模板化函数、以及一组分别针对6个表的getMap重载函数。然后,我们每增加一个表,只需要为getMap方法添加一个重载的实现,与初始设计中的4*6=24种名称各不相同,每增加

  一个表支持,要添加4种不同名称的函数实现的方案比较起来,是不是更清楚、更易维护、易于扩展了呢?

  

  第四步:添加序列化支持

  

   在本文开头我提到:Visual c++ 6.0平台中开发文档应用程序时,其文档序列化的功能非常好用,但由于其序列化能力建力在MFC之上,并不被STL支持,如何既拥有STL的效率及通用性,又保留MFC的序列化能力呢?由于篇幅的限制,我以下就只讲怎么做,而不讲为什么了(参见《MFC深入浅出》)。

  

   在这里我们假定map所包含的对象已具备序列化的能力,那么,对于一个map来说,其序列化实现应该如下(以Account 为例):

  

  void SerializeMap(CArchive& ar,std::map<size,Account*>&map)

  {

    typedef std::map< size,Account*>::value_type

    value_type;

    typedef std::map< size,Account*>::iterator iterator;

    if (ar.IsStoring())

    {

   DWord n=map.size();

   ar.WriteCount(n);

   for(iterator it=map.begin();it!=map.end();++it)

   {

    ar<<it->first<<it->second;

   }

    }

    else

    {

   size first;

   Account* second;

   DWORD nNewCount=ar.ReadCount();

   while (nNewCount--)

   {

    ar>>first>>second;

    value_type value(first,second);

    map.insert(value);

   }

    }

  }

   将其中的型别相关信息提取出来,利用模板技术就得到一个map的序列化支持函数如下:

  

  template <typename Key,typename T>

  void SerializeMap(CArchive& ar,std::map<Key,T>& map)

  {

    typedef std::map<Key,T>::value_type value_type;

    typedef std::map<Key,T>::iterator iterator;

    if (ar.IsStoring())

    {

   DWORD n=map.size();

   ar.WriteCount(n);

   for(iterator it=map.begin();it!=map.end();++it)

   {

    ar<<it->first<<it->second;

   }

    }

    else

    {

   Key first;

   T second;

   DWORD nNewCount=ar.ReadCount();

   while (nNewCount--)

   {

    ar>>first>>second;

    value_type value(first,second);

    map.insert(value);

   }

    }

  }

   这样,我们只需要在文档类的序列化函数中如下调用:

  

  SerializeMap(ar,itsEmployees);

  SerializeMap(ar,itsAccounts);

  ........

   即可拥有MFC内置的序列化能力了。

  

  

精彩图集

赞助商链接