11.1 泛化仿重载模式简述
此处的泛化仿重载模式(OverLoad)是我原创的设计模式,最开始启蒙思想是源于【GoF】中职责链模式,但却又和职责链大不相同。
有时候我们时常遇到想将一些算法通过某种方法联系在一起,只需通过一次调用,而使得编译器可以根据不同的参数类型选择不同的执行实体。在C++中重载函数已经完成了这种需要,但是重载函数有一个局限就是函数的名字必须相同,且只支持函数,不支持对象。
如果我们有一些对象,且我们希望在不改变原有对象代码的情况下将这些对象组建成重载关系,那就可以用到我们下面介绍的东西。
泛化仿重载是可以根据需要执行的参数个数以及类型的不同来选择执行的操作,有点类似函数的重载,但是我这里并不要求所有操作的名字都是一样的,这些操作可以是用户或者程序员在需要的时候添加的。
11.1.1 定义
通过将若干对象加入到容器中,可以使得这些对象成员函数带有类似重载的关系。可以根据程序中参数类型的不同而调用不同的对象成员函数。
11.1.2 泛化目的
原创模式,一出生就带有泛化性质。所有的对象都是未知,所有的对象的成员函数的参数类型都是未知,利用泛化解决这些未知。
11.1.3 适用场合
在下列场合适用泛化仿重载模式:
l 需要一种方法将若干已有操作定义成类似重载的形似。
l 对于不同参数类型以及参数个数时候需要进行不同的操作,且这些操作都为已知。
l 通过容器管理若干算法,并进行最优调用。
11.2 泛化仿重载模式不足
我不是大师,也不是学者,只是对C++有着强烈爱好的一名大学生,我斗胆试图设计一个属于自己的泛化设计模式,但是这个设计模式却有着前所未有的困难,为了解决困难我已无数次反复修改程序,目的只是想为使用者提供最为智能最为舒服的使用环境。
泛化仿重载最大的难点是保存对象的参数类型以及参数个数,这个似乎可以使用到我们常用的TypeVector或者TypeList。不过一个对象的参数已经是由一个TypeVector或者TypeList存储,而我们这不是一个对象,而是一组对象,所以还需要用一个TypeVector或者TypeList嵌套存储每个对象的TypeVector或者TypeList。
TypeList原本是我所有默认的类型存储容器,但是后来在写到这个设计模式时候它却给我带来了很大的痛苦。TypeList的原型是一个typedef的递归宏定义,在其取出其中某个类型时候是将这个宏定义展开,这使得TypeList<TypeList>这种形式不可用,因为里面一个TypeList会完全展开成为外面一个TypeList的成员,当我们想要在外部的TypeList寻找某个内部TypeList时候变得尤为复杂(几乎不可行)。当然问题总会解决的,只是折磨我太久,我必须用一个结构体对内部的TypeList进行一次封装,使其不会展开。不过值得庆幸的是这个问题的折磨产生了TypeVector,这个类型容器最初的产生目的就是为了优化这个问题。
不光是上面的问题,这两个嵌套的类型容器都是通过typedef定义构成,由于这个容器需要不断地改变,这使得我在维护时候显得异常困难,因为结构体定义了一次typedef以后就不能再次修改。这迫使我不得不让程序员在自己的程序里自己维护一个TypeVector,这显得很笨拙,但是起码已经迈出了第一步,以后我会不断寻求更好的办法让它使用起来更为优美。
11.3 实作泛化仿重载模式
11.3.1 如何保存重载行为
现在遇到的第一个问题就是我们怎么保存这些行为,这些行为有以下难点:
l 可能是一般函数。
l 可能是仿函数或者其他成员函数。
l 无法预测的参数个数以及类型。
上面提到的前两个问题我们可以统一用前面介绍的泛化仿函数统一解决,泛
化仿函数的一个特点是无论是真正的函数还是仿函数都能统一实例成为泛化仿函数对象,所以我们存储的时候就可以通过存储泛化仿函数对象来存储行为。而最后一个问题缺将前两个问题放大了,当我们对行为的参数个数以及类型都没有足够信息的时候我们也就无法提前确定泛化仿函数的对象,因为在泛化仿函数的模版参数中必须提供足够的行为参数信息,所以我们无法用任何容器存储不确定的泛化仿函数对象。
在我的思考后,我决定用一个最简单的存储内存地址来存储泛化仿函数对象的地址,然后再单独存储行为的参数个数以及类型,当需要调用行为的时候可以根据存储的参数信息将其复原。
所以最后我得到的结论就是存储重载行为用一个STL::list<unsigned char*>容器,然后在使用时候用强制类型转换将其转换成对应参数类型的泛化仿函数。STL::list看过我前面章节应该对这个序列式容器不会陌生,所以我也就不在此对其赘述。
11.3.2 如何保存行为参数信息
这里所说的参数信息包括参数类型以及参数个数,由于一个行为有若干个参数,并且我们要存储若干个行为,所以这里我们要用到数据类型容器再存储数据类型容器的嵌套存储,这似乎很复杂,不过幸运的是编译器会将这复杂的嵌套存储运行期之前化解为某个具体的数据类型,因为编译器对模版的处理是编译期的替换工作。这给我们程序带来的好处是我们所有对类型的存储在运行期或者说在用户看来都将是不消耗系统资源的。
但是当我们采用TypeList嵌套存储的时候将会产生不确定的结果,例如看下面的代码:
TypeList的结构是:
#define TYPELIST_1(T1) TypeList<T1,NullType>
#define TYPELIST_2(T1,T2) TypeList<T1,TYPELIST_1(T2)>
但是如果当我们写出下面这样的代码时候:
//定义两个个类型容器存储行为参数的类型
typedef TYPELIST_2(int,char) ParmList1;
typedef TYPELIST_2(float,int) ParmList2;
//上面定义的两个参数类型容器是不同行为的,下面我们还需要一个类型容器存储每个行为的类型容器
typedef TYPELIST_2(ParmList1,ParmList2) TList;
在上面的代码中我们本来的想法是ParmList1和ParmList2是不同的个体,但是由于我们最初的宏定义,上面最后的TList容器会被分解为:
//#define TYPELIST_2(T1,T2) TypeList<T1,TYPELIST_1(T2)>
typedef TYPELIST_2(TYPELIST_2(int,char),TYPELIST_2(float,int)) TList;
接着分解成:
typedef TYPELIST_2(TypeList<int,TypeList<char,NullType> >,TypeList<float, TypeList<int,NullType> > TList;
最后就将分解成:
typedef TypeList<int,TypeList<char,TypeList<float,TypeList<int,NullType> > > > TList;
由上面的分析可以知道由于TypeList本身嵌套宏定义的特点,使得我们无法以独立的类型容器单元保存每个行为的参数类型,这就使得我们无法在需要的时候对其进行辨别,我们无法回到他定义时候的样子。这样就加大了程序的复杂度,解决办法就是在每个行为参数类别用一个外覆类包裹起来以不让其展开。
不过更好的办法就是用TypeVector,这个类型容器没有递归的宏定义,所以不会降类型展开,所以我们可以用TypeVector存储TypeVector,且在需要的时候随时进行辨别。
11.3.3 实作泛化仿重载
现在开始讲解泛化仿重载的源代码,其源代码基本分为两部分,一部分是对行为参数类型的维护,一部分是对行为地址的维护。
class OverLoad
{
//一个简化的定义
typedef unsigned char* FunPoint;
//我们采用std::list<unsigned char*>存储仿函数对象的地址
std::list<FunPoint> FunList;
public:
//这里定义一个空的类型容器
typedef TypeVector<> TVector;
//这里是将类型T添加类型容器TVector中
template<typename TVector,typename T>
struct TypeAdd
{
typedef typename Append<TVector,T>::Result Result;
};
//这里这个是类型的删除操作
template<typename TVector,unsigned int num>
struct TypeErase
{
typedef typename ErasePose<TVector,num>::Result Result;
};
//这里是添加行为到链表中,TVector中包含了返回值类型和参数类型
template<typename TVector,typename Fun>
void append(Fun& fun)
{
FunList.push_back((FunPoint) new Functor<typename TypeAt<TVector,0>::Result,typename ErasePose< TVector,0 >::Result>(fun));
}
//这里是算法的删除操作,很遗憾,型别的删除不能放在这里进行,因为型别链表是在主函数由程序员维护
template<typename TVector>
int& erase(Functor<typename TypeAt<TVector,0>::Result,typename ErasePose<TVector,0 >::Result> *fun)
{
std::list<FunPoint>::iterator ite = FunList.begin();
int i =0;
while(ite != FunList.end())
{
if(*ite == fun)
break;
++ite;
++i
}
if(ite!=FunList.end())
{
delete *ite;
FunList.erase(ite);
}
return i;
}
//下面就开始寻找合适的算法运行,我们根据参数个数不同重载了个版本,再次申明,我们假设参数至多只有2个
//如果要加多参数的重载版本也只是Ctrl + V的操作
//在运行之前最好做一个是否能够匹配运行的判断,当然如果你有是全把握绝对能运行正确,那就可以跳//过不用这个函数
template<typename Type_Vector,typename TVector>
bool CanRun()
{
int i = IndexOf<Type_Vector,TVector>::value;
std::list<FunPoint>::iterator ite = FunList.begin();
while(ite != FunList.end() && i>0)
{
--i;
++ite;
}
if(i==0)
return true;
else
return false;
}
template<typename Type_Vector,typename TVector>
typename TypeAt<TVector,0>::Result Run()
{
int i = IndexOf<Type_Vector,TVector>::value;
std::list<FunPoint>::iterator ite = FunList.begin();
while(ite != FunList.end() && i>0)
{
--i;
++ite;
}
if(i==0)
(*(Functor<typename TypeAt<TVector,0>::Result>*) *ite )();
else
throw Error();
}
template<typename Type_Vector,typename TVector>
typename TypeAt<TVector,0>::Result Run(typename TypeAt<TVector,1>::Result p1)
{
int i = IndexOf<Type_Vector,TVector>::value;
std::list<FunPoint>::iterator ite = FunList.begin();
while(ite != FunList.end() && i>0)
{
--i;
++ite;
}
if(i==0)
return (*(Functor<typename TypeAt<TVector,0>::Result,typename ErasePose<TVector,0 >::Result>*) *ite )(p1);
else
throw Error();
}
template<typename Type_Vector,typename TVector>
typename TypeAt<TVector,0>::Result Run(typename TypeAt<TVector,1>::Result p1,typename TypeAt<TVector,2>::Result p2)
{
int i = IndexOf<Type_Vector,TVector>::value;
std::list<FunPoint>::iterator ite = FunList.begin();
while(ite != FunList.end() && i>0)
{
--i;
++ite;
}
if(i==0)
return (*(Functor<typename TypeAt<TList,0>::Result,typename ErasePose<TList,0 >::Result>*) *ite )(p1,p2);
else
throw Error();
}
};
从上面的代码我们看到,其实在OverLoad类中并没有对行为参数类型容器进行维护,而只是提供了维护的函数,其实因为我们的类型容器无法在结构体中反复用typedef重叠定义(因为其实typedef原本只是一个取别名的关键字),所以我们就无法在结构体中维护一个类型容器。这个工作必须放到OverLoad外面进行。当然也许还有其他更好的维护办法,但是目前能完全运行的只有这个,不过我为了能够让程序员使用起来更舒服,做了很多优化试探,但是都没有成功,在后面我将介绍我的优化方案。
现在我们可以在主程序中这样使用:
struct Test1
{
void operator()()
{
std::cout<<"test1 has fun"<<std::endl;
}
};
struct Test2
{
void operator()(int i)
{
std::cout<<"test2("<<i<<") has fun"<<std::endl;
}
};
int main()
{
typedef OverLoad::TVector TVector_1;//定义出数据类型存储的容器
//定义出仿重载对象
OverLoad fun;
//定义第一个行为对象
Test1 test1;
//定义第二个行为对象
Test2 test2;
//将第一个行为放入仿重载对象中,注意模板参数中的void代表返回值为void且没有参数
fun.append< TypeVector<void> >(test1);
//将第一个行为的参数类型添加进类型容器当中
typedef OverLoad::TypeAdd< TVector_1,TypeVector<void> >::Result TVector_2;
//将第二个行为放入仿重载对象中,注意模板参数中的void和int代表返回值为void且只有一个int参//数
fun.append< TypeVector<void,int> >(test2);
//同样是将行为的参数类型添加进容器中,但是由于C++不支持对一个类型的typedef重复定义,所以//这里只有采用命名时候用不同后缀以区别。
typedef OverLoad::TypeAdd< TVector_2,TypeVector<void,int> >::Result TVector_3;
//使用仿重载
if(fun.CanRun< TVector_3,TypeVector<void> >())
{
fun.Run< TVector_3,TypeVector<void,int> >(21); //这时候根据参数个数和型别就判断出执行的是Test2
}
getchar();
return 0;
}
看到上面的主函数了吧,我们在将若干行为添加了以后就可以在使用时候根据提供的行为参数信息自动的调用相应的行为,这是多么好的一个想法。其实上面程序也可以很明显的看到使用时候是带有很大的不便,而这个不便最主要就是对类型容器的维护上,但是只要思想提出来了,程序是可以不断优化的,这是我一贯的前进思想。在后面我将提到我进行的优化工作。
11.4 泛化仿重载的优化
上面的代码可以看到,本来我们很好的初衷却被笨拙的使用方法给抹灭了。但是程序总是在不断地前进,我们的这个程序也可以不断地进行优化。其中最值得优化的地方就是对类型容器的维护,没多少程序员希望背负起维护类型容器的这个职责,所以我们必须要让类型容器的维护和行为容器的维护同步进行。
在优化的可行性分析里面,由于类型是在程序员编译器就肯定确定下来了的,所以我们肯定在后面运行期可以对其进行匹配,这是在理论上编译器完全可以实现的,只是看具体用什么方法实现。我想到的最好的一个方法就是将每个行为的参数类型和行为的地址存放在一起,在使用时候如果匹配行为参数成功后就直接调用存储在一起的行为。但是要这样就必须自己设计容器,因为现有的容器都无法提供这样的功能(其实在介绍泛化对象工厂时候就已经设计过类似这样的容器)。
最先让我想到的就是利用C++面向对象的多态和模板相结合。例如下面的代码:
//这是一个利用编译器选择模板特化来判断类型Base是否是Child的父类,如果是就返回1,否则返回0
template<typename Base,typename Child>
struct Inheritance
{
//利用模板的偏特化
template<typename T> struct IsBase
{
enum{value = 0};
};
template<>
struct IsBase<Base>
{
enum{value = 1};
};
enum {value = IsBase<Child>::value};
};
//这是我们容器的节点
struct BaseOverLoadListNode
{
//下面这个成员变量用来存储行为地址
unsigned char* pFun;
//这个是指向下一个节点的指针
BaseOverLoadListNode* next;
//这是一个虚函数,以判断类型是否匹配
template<typename T>
virtual bool Same()
{
return false;
}
};
//下面这是继承BaseOverLoadListNode,这个类用来存储行为的类型。
template<typename TVector>
struct OverLoadListNode:public BaseOverLoadListNode
{
//下面这个就用来存储行为参数类型
typedef TVector ParmList;
//构造函数初始化父类的成员变量
OverLoadListNode(unsigned char* _Fun):pFun(_Fun){};
//虚函数,判断类型T是否和ParmList匹配,是就返回1否则返回0
template<typename T>
bool Same()
{
return Inheritance<ParmList,T>::value;
}
};
上面的代码就是一个利用多态和模板来存储未知类型数据的典型例子,当我们收到一个行为的时候就用new创建一个OverLoadListNode节点,且保存行为的地址和参数类型,然后在OverLoad中维护一个链表,将此节点添加进去,因为链表是用的BaseOverLoadListNode指针,所以无需知道OverLoadListNode的特化类型就可以对其进行保存,当需要对某个用户提出的行为参数类型进行匹配时候就采用虚函数Same函数,这时候这个函数其实是执行的OverLoadListNode::Same()。
这一切看起来都是那么的完美,不过当我满心欢喜的敲入代码并且编译后发现一连串的编译错误,这些错误都来自虚函数不能使模板函数!这似乎突然又向我宣判了死刑,因为正是虚模板函数完成了我想要的,程序员不用单独维护类型容器。后来我想了很多办法,都还是无法解决这个问题,不过我相信总有一天我能将这一切变得很美丽。
11.5 摘要
l 泛化仿重载为一个全新的设计模式,也是一个全新的泛化设计模式。
l 泛化仿重载可以存储若干不同名字的行为,且同时记录下这些行为的参数类型。当程序员需要调用时候自动根据程序员提供的行为参数类型匹配相应的行为以执行。