C++模板实现模块间参数传递及回调(一)

来源:互联网 发布:阿里云独立控制面板 编辑:程序博客网 时间:2024/05/21 09:25
 

    前段时间在用c++设计一个项目的框架时遇见了一些问题,内部模块间的接口过于庞大,因此需要一种统一的方式来实现参数传递,后来就想到了C++的模板,并且做了一个雏形。虽然项目还没开始就挂了,但是我却继续研究了下去,于是就出现了几个泛型类,个人感觉可以在很多的项目设计中通用。

 

    这里介绍了一种模块间传递参数及实现回调机制的泛型接口,并实现了一个泛化的回调管理器。本文的内容对于修补代码和解BUG等类型的工作而言没有任何意义,但如果你要从零开始设计一个软件或者重构一个系统的话相信你可以从文中获得一点好处。理解本文的内容需要一定的C++模板相关知识。由于能力限制,这里讲的内容难免会有错误,欢迎指正和提供更简洁的实现方案。

 

关键字:模板、参数传递、回调函数

 

参考资料:《C++设计新思维:范型编程与设计模式之应用》

 

1 模块的参数传递

1.1 适用范围

当我们设计一个软件时,有时会需要在两个模块或者类之间传递类型不同数量不同的N组参数,为了实现这个目的不得不设计N个接口,如果这两个互相通信的类是抽象接口的话,当你需要修改或添加参数及方法时会十分麻烦,因为还需要同时修改所有的子类。更重要的是一个全功能接口的类违反了设计的美学。

当然你可以利用定义多个结构体,并强制转化为void*然后附加一个消息类型的方案避免上述问题,但是这种方法会导致产生多个结构体,并且转化总是会成功,即使是在你写错的情况下。

这里介绍的方法也是需要一个附加的消息类型,但是没有使用void*带来的各种麻烦。

1.2 使用方法

这里介绍一些这个方案的例子代码。

//模块1中代码:

 CParam *p1 = CreateParam(2.0,“12345”,false33);

 CParam *p2 = CreateParam(5);

//模块2中代码:

double d;

const char *s;

bool b;

int i;

GetParam(p1,d,s,b,i);//如果转化失败会导致一个断言或抛出异常

GetParam(p2,di);

这里你只需要调用CreateParam接口传递任意数目和类型的参数并生成一个CParam *变量,然后通过某个特定的消息传递给其他模块。通过这个方法你可以少定义很多无聊的结构体,并且转化失败时会得到通知。

1.3 实现

首先来定义转化函数:

template<class R,class T>

  R param_cast(T t)

  {

      R r = dynamic_cast<R>(t);

      assert(r && t);

      if (!r)

      {

          throw param_bad_cast();

      }

      return r;

  }

为了得到会失败的转化,我们需要使用dynamic_cast,这个转化操作符只能用于多态类型,失败时传回NULL。因此我们需要定义一个参数集合体的基类:

struct CParam

{

    virtual ~CParam(){}

};

这里虚拟的析构是必须的,因为我们需要一个多态的删除行为,以及使用dynamic_cast的能力。

下面是参数集合类:

template<class T1,class T2>

  struct CParam2 : public CParam

  {

      CParam2(T1 &t1,T2 &t2):v1(t1),v2(t2){}

      CParam2(){}

      T1 v1;

      T2 v2;

  };

这个类可以保存任意两个类型的参数的,前提是你的参数是内置类型,或带有无参的构造函数以及赋值和拷贝构造函数。为了保存一个单一的参数,我们需要利用模板的偏特化和一个帮助类。

class NullType;

template<class T1>

  struct CParam2<T1,NullType> : public CParam

  {

      CParam2(T1 &t1):v1(t1){}

      CParam2(){}

      T1 v1;

  };

这里的NullType只有声明没有定义,因为我们根本不需要定义一个NullType的变量。有了这两个类,我们可以实现任意数目和类型的参数了,这里我只扩展到5个:

#define PARAM1(T1) CParam2<T1,NullType>

#define PARAM2(T1,T2) CParam2<T1,T2>

#define PARAM3(T1,T2,T3) CParam2<T1,CParam2<T2,T3> >

#define PARAM4(T1,T2,T3,T4) CParam2<T1,CParam2<T2,CParam2<T3,T4> > >

#define PARAM5(T1,T2,T3,T4,T5)  \

CParam2<T1,CParam2<T2,CParam2<T3,CParam2<T4,T5> > > >

现在我们可以实现CreateParam和GetParam了。这里TRAITS()是一个宏,定义为#define TRAITS(T) T ,作用稍后说明。

template<class T1>

  CParam * CreateParam(TRAITS(T1) t1)

  {

      return (new CParam2<T1,NullType>(t1));

  }

template<class T1,class T2>

  CParam * CreateParam(TRAITS(T1) t1,TRAITS(T2) t2)

  {

      return (new CParam2<T1,T2>(t1,t2));

  }

template<class T1,class T2,class T3>

  CParam * CreateParam(TRAITS(T1) t1,TRAITS(T2) t2,TRAITS(T3) t3)

  {

      CParam2<T2,T3> t(t2,t3);

      return new CParam2<T1,CParam2<T2,T3> >(t1,t);

  }

同理可得任意数量的参数的CreateParam,一般来说10个足够大部分项目使用。

template<class T1>

  void GetParam(CParam *p,T1 &t1)

  {

      CParam2<T1,NullType>* p2 = param_cast<CParam2<T1,NullType>*>(p);

      t1 = p2->v1;

  }

  template<class T1,class T2>

  void GetParam(CParam *p,T1 &t1,T2 &t2)

  {

      CParam2<T1,T2>* p2 = param_cast<CParam2<T1,T2>*>(p);

      t1 = p2->v1;

      t2 = p2->v2;

  }

  template<class T1,class T2,class T3>

  void GetParam(CParam *p,T1 &t1,T2 &t2,T3 &t3)

  {

      PARAM3(T1,T2,T3)* p2 = param_cast<PARAM3(T1,T2,T3)*>(p);

      t1 = p2->v1;

      t2 = p2->v2.v1;

      t3 = p2->v2.v2;

  }

同理可得其他的GetParam

通过上面的方法,我们可以在多个模块间用统一的接口传递CParam *来实现参数的传递,但同时有一个非常严肃的问题需要面对:谁来删除这个指针呢?一种方案是创建者负责删除,但如果是多线程的情况呢?再加上参数接收者数目的不固定问题就更复杂了,难道真的无法解决了吗?且看下回分解!

1.4 内存释放

上回说到CParam *指针的删除问题,这个问题困扰我很久,但最终还是有了答案,即智能指针。

我尝试过STL自带的auto_prt,可惜它是一个销毁拷贝,这个东西只能被拷贝一次就会失效,不符合需求。因此我实现了一个智能指针类:

template<class T,class Ref>

  class CParamPtr

  {

  public:

      CParamPtr():t(0),r(0){}

      CParamPtr(T *v):t(v),r(new Ref){*r = 1;}

      ~CParamPtr(){Release();}

      CParamPtr(CParamPtr<T,Ref> &other):t(other.t),r(other.r){if(r)++*r;}

      void operator=(CParamPtr<T,Ref> &other)

                    {

          if(this==&other)

              return;

          Release();

          t = other.t;

          r = other.r;

          if(r)

              ++*r;

      }

      void operator=(T *p)

                    {

          if(p==t)

              return;

          Release();

          t = p;

          r = new Ref;

          *r = 1;

      }

      operator T*(){return t;}

      void operator->(){return t;}

  private:

      void Release()

      {

          if(r && (--*r == 0))

          {

              delete t;

              delete r;

          }

      }

  private:

      T *t;

      Ref *r;

  };

这个类带有两个模板参数,第一个是指针类型,可以是CParam ,第二个是引用计数类型,可以是int

对于单线程环境下的参数传递我们可以这么写:

typedef CParamPtr<CParam,int> ParamType;

对于多线程环境,我写了一个Windows下的计数类型:

struct ThreadRefCount

{

  ThreadRefCount():n(0){}

  void operator=(int v){n = v;}

  void operator++(){InterlockedIncrement(&n);}

  int operator--(){return InterlockedDecrement(&n);}

  long volatile n;

};

多线程环境:

typedef CParamPtr<CParam,ThreadRefCount> ParamType;

现在我们可以这样来传递参数,而无需关心内存释放问题:

ParamType p(CreateParam(2.0,“12345”,false33));

Somefunciton(p);

你也可以利用CParamPtr来传递自己的类型的指针。

1.5 关于效率

使用上述方法来传递参数会导致至少一次的newdelete操作,并附加若干次参数的拷贝。

频繁的内存分配会对效率造成影响,但是有方法可以把影响减少至最低程度,详情这里不介绍,请参考《C++设计新思维:范型编程与设计模式之应用》第四章:小型对象分配技术。事实上对于大部分项目而言,这里的开销可以忽略不计。

实现CreateParam可以使用传值或传引用,两种各有优缺点,对于内置类型可以直接传值,对于类和结构体可以传引用来减少一次参数的拷贝。上面的TRAITS()宏即是这个用途,你可以把TRAITS(T)定义为TT&来针对你的项目来切换。事实上有一种方法可以在编译期侦测参数是否为内置类型,详情请见C++设计新思维:范型编程与设计模式之应用》第二章,利用里面的技术你可以这么写 #define TRAITS(T) TypeTraits<T>::ParamType

每次获取参数需要一次dynamic_cast调用,这个操作究竟有多大代价我并没有仔细研究过,正式发布时可以考虑把dynamic_cast替换为static_cast,因为只要一次转化成功在不改代码的情况下以后都不会失败。

1.6 已知缺陷

这种方法不适合传递引用类型,请用指针来代替。

当TRAITS(T)定义为T时,传递空指针需要格外注意,不要直接传递NULL,这会被解释为int类型,即:

CreateParam(NULL);//不要这么写

//应该这么写

Int  *p = NULL;

CreateParam(p);

不过即使你写错了,在取参数时会导致一个异常,你也可以很容易的发现错误。

当TRAITS(T)定义为T&时,无法在CreateParam直接使用右值,即:

CreateParam(1,false);//无法通过编译

//需要这么写

Int i = 1;

Bool b =false;

CreateParam(i,b);

1.7 更复杂的实现

如果前面的东西看得你有点小头昏,建议跳过这一节。

上述方法还有一个缺点,如果你要从CParam*取一个单一的参数会相当的复杂,必须知道CParam*的实际类型,函数写起来会很麻烦,实现起来更麻烦,如:

Int a

CParam*p;

GetSingleParam<bool,int,double,2>(p,a);

怎样才能写出如下的代码呢:

GetSingleParam<2>(p,a);

实现起来相当的复杂,因为这个函数只知道两个信息,参数的序号及类型,有可能通过这两个参数来得到结果吗?答案是肯定的。

 

使用多重继承可以达到上面的效果,这里我们不再使用CParam2类,而使用下面的类:

template <class T, class U>

struct Typelist

{

typedef T Head;

typedef U Tail;

};

这个Typelist来自C++设计新思维:范型编程与设计模式之应用》第三章,我们需要借用它强大的能力。

template<class T,int i>

struct GenClass

{

  T Value;

};

以及它的偏特化版本:

template<int i>

struct GenClass< NullType,i> : public CParam {};

template<class HEAD,class TAIL,int i>

struct GenClass< Typelist<HEAD, TAIL>,i> : public GenClass<HEAD,i>

      , public GenClass<TAIL,i+1>

{

};

对于bool,int,double类型的参数集有如下实现:

GenClass<Typelist<bool,Typelist<int,Typelist<double,NullType> > >,1>,这个类会产生如下的继承体系:

 

 

 

现在我们可以这么定义GetSingleParam

template<int i,class T>

void GetSingleParam(CParam*p,T &t)

{

  t = param_cast<GenClass<T,i>*>(p)->Value;

}

使用这种方法有个严重的缺陷,无法把dynamic_cast替换为static_cast来提高效率,因为使用了多重继承。