C++ & Design

Ogre源码剖析2:如何实现低耦合的类间消息传递机制

1

关于低耦合的消息传递,实现的方式有很多,哪种方法更好与具体的使用环境有关,本文使用试错的方法,逐步探索达成这一目的具体方式,并理解实现方式背后的原因。

 

面向对象的系统当中,不可避免的有大量的类间消息传递的需求:一个类需要通知另一个或几个类做些什么。

 

这种类间消息传递,简单的说,就是调用其他类的方法。

如下:

void A::OnMessageXX()

{

         B::GetInstance()->DoSomething();

}

在这里,类A需要通知类B做些事情。这种调用在所有的面向对象程序中都是极其常见的。

 

但是如果类A需要调用类B,就不可避免的产生了耦合性。虽然耦合性终归是不可能完全避免的,但是在一定程度上降低耦合性是完全可能的。

(至于为什么在设计中应该尽可能降低耦合性,不在本文的探讨范围之内)

 

上面的例子,我们使用了Singleton的模式,从全局作用域中获取了B的实例,并调用了B的相关方法。使用Singleton的一个缺点是,假若我们希望对类A编写测试代码,我们需要做一些额外的解耦合工作。(关于编写测试与解耦合,可以参考Robert C. Martin Series的Working Effectively with Legacy Code一书,该书的中译版在这

我们也可以通过将B参数化的方法降低A与B间的耦合程度,像下面这样:

void A::OnMessageXX(B* pBInstance)

{

         pBInstance->DoSomething();

}

现在的写法要比之前的做法耦合性低,通过使用多态的方法,现在传入函数的类B指针可能是另一个实现了B的相应接口的派生类,A并不关心B接口背后的具体实现。

 

但是等等,你说,现在对类B的耦合性虽然在A中被降低了,但是依旧存在于调用A::OnMessageXX的地方。在那里我们还是需要取得B的实例,然后传递给A。

 

没错,是这样。

 

通过参数化类A的方法,我们把类A与类B间的耦合转移了一部分到A的调用者那里。实际上总的耦合并没有消除,只是被分解了。但是程序设计中不可能完全不存在耦合,我们需要做的是”正确”,而不是”完美”。类A的耦合性降低了,使得我们在未来需求变更的时候,类A有更大的可能性不需要被修改,并且对功能的扩展更加友好,这就达成了我们的目标了。

基于上述做法,如果我们在未来扩展是派生出一个B的子类,override相关的方法,那么类A的代码基本是不需要修改的。

 

不过,问题是,假若A::OnMessageXX中,并不仅仅需要对类B发出消息,还需要对一系列相关的类B1,B2,B3等等发出消息呢?

 

哦,或许我们可以这样做:

 

void A::OnMessageXX(const std::list<B*>& lstBInstances)

{

         for (std::list<B*>::const_iterator itr = lstBInstances.begin();

                   itr != lstBInstances.end();

                   ++itr)

         {

                   (*itr)->DoSomething();

}

}

 

是的,上面这是一种做法,有一系列B的对象需要被通知到,所以我们可以用一个列表把他们串起来,然后在循环中通知他们去干活。不过这样做的前提是,这一系列B对象都是派生自一个公共基类B,有共通的接口;此外,我们需要在A的OnMessageXX被调用之前构造一个需要接受通知的B对象列表。

 

当A需要通知B,C,D等一系列没有公共接口的对象的时候,上面的这种做法就无法处理了。

 

对于B、C、D等需要由A来调用的类来说,它们需要在A通知它们的时候,做一些特定的事情。而又A则是在某些特定的时刻需要通知B、C、D。这样,我们可以把问题看成一个消息响应机制。

 

B、C、D可以在A的某些事件上注册一些回调函数,当事件发生时,A确保注册该事件的函数被调用到。

 

如下:

typedef void(callback*)();

class A {

public:

         enum EventIds {

         EVENT_MSG1,

         EVENT_MSG2,

};

void RegisterEvent(int nEventId, callback pfn);

private:

callback m_pfnCallback;

};

 

现在,B可以调用A::RegisterEvent注册一个事件,并传递一个函数指针给A。

当A中发生了注册的事件时,这个函数指针会被回调到。

不过这种简单的做法适应性很差:

1、  不能支持单个事件的多个callback (可能有很多类都需要注册该事件,并在事件发生时依次被回调)

2、  不能支持多个事件的同时存在

3、  回调函数没有参数’

 

针对问题1,2,我们可以使用一个事件映射解决问题,做法如下:

typedef int EventId;

typedef void (callback*)();

typedef std::list<callback> CallbackList;

typedef std::map<EventId, CallbackList> CallbackMap;

现在这个数据结构就能够支持多个event同时存在,且每个event都可以支持多个回调函数了。

 

但是这种用法依旧很不方便,如果类B想要注册A上的一个事件,他需要定义一个 callback类型的函数,并把这个函数的地址传递给A。问题是,往往我们希望类B的回调函数在被调用到的时候,对类B中的数据和状态进行修改,而一个单独的函数,若想获得/修改B中的状态,则必须要与类B紧密耦合。(通过获取全局对象,或者Singleton的方式)

这种紧密耦合引发我们的思考,能否在Callback中同时包含类B的指针与类B的成员函数。

 

答案是肯定的:泛型回调就可以做到这一点。关于泛型回调(Generic callback)的信息,在Herb Sutter的Exceptional C++ Style的35条中有详细介绍。

 

一下比较简单的泛型回调的定义如下:

class callbackbase {

public:

virtual void operator()() const {};

virtual ~callbackbase() = 0 {};

};

 

template <class T>

class callback : public callbackbase {

public:

typedef void (T::*Func)();

callback(T& t, Func func) : object(t), f(func) {}     // 绑定到实际对象

void operator() () const { (object->*f)(); }              // 调用回调函数

private:

T* object;

Func f;

};

 

有了这种泛型回调类,我们就可以将类B的实例与B的成员回调函数绑定在一起注册到容器当中了,而不必再被如何在普通函数中修改B对象状态的问题所困扰了。不过回调函数的参数问题依旧。如果想支持参数,我们不得不对每一种参数类型做一个不同的typedef,像上面定义的这样 typedef void (T::*Func)();(如:typedef void (T::*Func)(int);)

一种解决方案是借助于Any(一种任意类型类)进行参数传递。

 

但是还有更完善的解决方案,不需要id号,也不需要泛型回调,Ogre采用Listener的方式实现的类间消息传递不仅可以支持单个类B对类A中某个事件的单次/多次注册,也可以支持类B、C、D对同一个事件的注册。而且可以完美的解决参数传递问题。

 

具体的方案如下:

class A {

public:

         class Listener {

         public:

                   virtual void OnMessageXX(int param1, float param2) = 0;

                   virtual void OnMessageYY(int param1, const std::string& param2) = 0;

};

 

void registerListener(Listener* obj) { m_lstListener.push_back(obj); }

void removeListener(Listener* obj)

{

         ListenerList::iterator itr = std::find(m_lstListener.begin(), m_lstListener.end(), obj);

         if (itr != m_lstListener.end())

                   m_lstListener.erase(itr);

}

 

private:

         typedef std::list<Listener*> ListenerList;

         ListenerList m_lstListeners;

};

 

有了以上定义,当类A收到某个消息XX之后,只需遍历m_lstListeners列表,调用所有列表成员的OnMessageXX即可。

 

而所有注册A的消息的类,都必须从A::Listener派生一个类,在它感兴趣的消息处理函数中做出相应处理,而对不感兴趣的消息,只需设为空函数即可。

 

一个简单的类B的定义如下:

 

 

class B {

public:

         friend class BListener;

         class BListener : public A::Listener {

         public:

                   BListener(B* pBInstance) : m_pBInstance(pBInstance) {}

                   virtual void OnMessageXX(int param1, float param2)

{ m_pBInstance->DoSomething(); }

                   virtual void OnMessageYY(int param1, const std::string& param2) {}

         private:

                   B* m_pBInstance;

};

 

explicit B(A* pAInstance) : m_pAInstance(pAInstance)

{

m_pListener(new BListener(this));

m_pAInstance->registerListener(m_pListener);

}

         ~B() { m_pAInstance->removeListener(m_pListener); delete m_pListener; }

 

void DoSomething();

 

private:

         BListener* m_pListener;

}

 

类B在创建自身实例时,接受一个A的指针(这是合理的,因为类B需要监听类A的消息,理应知道A的存在),并创建一个派生自A::Listener的监听者对象,并把自身的指针传递给该对象,以使得该监听者改变类B的状态,而后类B将创建好的监听者对象加入到A的监听者列表中。

在B进行析构的时候,需要从A中删除自己注册的监听者。而后将该对象释放。

 

这种做法的好处:

1、  类B(以及类C等)对类A实现了信息隐藏,类A不再关注任何需要监听它自身消息的其他类,只需关注其自身的状态。从而减低了类A与其他与之关联的类之间的耦合。(类A不必再费尽心机的去获取B的指针,不管是通过全局变量,还是Singleton,还是参数,还是类成员变量,都不再需要了,A只关心在Listener中定义好的一组接口即可)而且,如果有必要类B可以对同一个消息注册多次,且可以对同一消息有不同的反应(通过定义不同的BListener实现达到这一目的),只需在B不再需要监听相关消息时将所注册过的对象注销掉即可。

2、  由于1中所述,类A的实现无需关心类B的实现,因此类A的逻辑中不需要包含任何类B的方法调用,从而,类A的cpp文件中,无需包含类B的头文件,(可能还包括类C,D等等,此处类B指代需要根据类A状态而做出动作的类)从而降低编译时间,这是解耦合所带来的附加好处。

3、  同样是解耦合带来的好处:因为无需关注类B等等其他类的实现,类A的代码逻辑变得更加清晰,并且减少未来逻辑需求变更的改动所需要付出的代价(逻辑变更可能需要更改接口,需要增加状态判断,无论是调试时间还是编译时间都是不可忽视的代价)。

Ogre源码剖析1:任意类型类Any

3

有些时候我们可能想做这样一件事:

float f = 1.f;

int n = 2;

std::vector<X> myContainer;           // X是一个虚构的用户定义类型

myContainer.pushback(X(f));

myContainer.pushback(X(n));

 

我们想在一个容器里保存两种乃至多种不同的数据类型。

但是,显然普通的模板参数如std::vector<int>,或者std::vector<float>都无法满足我们的需求。

 

也可能存在下面这样的情况:

float fVal = 1.f;

X xMsg = fVal;

PushMessage(xMsg); // PushMessage发送了一个异步消息

 

// 另一个地方(可能是另一个线程),回调函数被调用:

OnMessageXXX(X xMsg)

{

         float f = static_cast<float>(xMsg);

         // do something with value f

}

 

针对这种需求,我们想要一种类型,它可以接受任意的类型,并在需要的时候把我们放入的真正的类型取出来,它可以被放置到容器中,可以被拷贝,以及串行化(前提要求被置入其中的类型可以被输出到标准输出流–std::ostream当中)。

 

如何设计这种类型呢?

 

一种容易想到的方案如下:

 

class Any {

public:

enum {

                  ANYTYPE_INT,

                  ANYTYPE_FLOAT,

                  ANYTYPE_LONG,

                  // … other data types

};

 

union {

                   int nIntData;

                   float fFloatData;

                   long lLongData;

                   // … other data types

};

 

Any(int nData) : nIntData(nData), m_type(ANYTYPE_INT) {}

Any(long lData) : lLongData(lData), m_type(ANYTYPE_LONG) {}

Any(float fData) : fFloatData(fData), m_type(ANYTYPE_FLOAT) {}

// … other constructors

};

 

现在我们可以这样写:

Int nData = 3;

Any myVal(nData);

 

为了让Any可以被赋值,我们需要给Any提供一个operator =。

 

Any& Any::operator = (const Any& rhs)

{

         if (&rhs == this)

                   return *this;

         m_type = rhs.m_type;

         switch(rhs.m_type)

{

case ANYTYPE_INT:

         nIntData = rhs.nIntData;

         break;

case ANYTYPE_LONG:

lLongData = rhs.lLongData;

break;

case ANYTYPE_FLOAT:

         fFloatData = rhs.fFloatData;

         break;

default:

         ASSERT(0);

};

}

 

这样Any就有了相互之间被赋值的能力了,如下:

 

Any myVal1(1);

Any myVal2(5.6f);

myVal1 = myVal2;

 

然而我们还希望Any可以直接用各种数据类型赋值。因此,针对需要支持的每一种数据类型,都应该重载一个operator =。如下:

 

Any& operator = (int inData)

{

         nIntData = inData;

         m_type = ANYTYPE_INT;

         return *this;

}

 

Any& operator = (long inData)

{

         lLongData = inData;

         m_type = ANYTYPE_LONG;

         return *this;

}

 

等等。

当然如果嫌这种写法过于冗长,出于节省代码的考虑,我们可以使用一个宏来代替:

#define OPEQUAL(type, typeID, varName)

         Any& operator = (type nData)

         {

                   varName = paramName;

                   m_type = typeID;

                   return *this;

         }

这样就可以将上述代码转换成:

OPEQUAL(int, ANYTYPE_INT, nIntData);

OPEQUAL(long, ANYTYPE_LONG, lLongData);

OPEQUAL(float, ANYTYPE_FLOAT, fFloatData);

 

此外,作为一种数据的承载,我们还希望在需要的时候把实际的数据取出来,因此我们需要重载一系列获取函数:

 

operator int() const { return nIntData; }

operator long() const { return lLongData; }

operator float() const { return fFloatData; }

 

有了这些类型operator之后,当需要从Any中取出我们想要的数据时,即可以通过:

Any myAnyVal(25);

int nIntData = myAnyVal;

这种形式得到想要的值。但是需要注意的是,这里不可以对Any的隐式转换做正确性的假定。即,我们不能写下如下代码:

Any myAnyVal(25);

float fData = myAnyVal;

我们不能指望这里返回正确的值。因为Any中保存的数据实际为int,而当这个赋值发生时,根据重载决议,编译器会调用Any的operator float()返回一个float值。而由于float在内存中的放置是基于IEEE浮点格式的,int则是2进制的数据,最后返回的数据就不可能正确了。

基于此,我们要求在使用Any的时候,存放数据的位置和取出数据的位置,都必须由程序员指定对应好的数据类型,并且寄希望于程序员知道自己在做什么。

 

上述以上的实现方法有2个好处:

1、  节省内存,对于不同大小的数据类型,通过union的形式共用了存储空间。

2、  速度快,没有使用动态分配内存的模式,而是直接将对象放置在union空间中。

 

但是如果我们想让这个Any支持std::string,就不能像上述实现得那么直接了。

 

因为union中不能支持non-trivial的数据类型(因为对于non-trivial的数据类型,编译器在为其分配内存空间之后,还要调用其构造函数)。我们必须另外想一种方法。

一种简单的容易想到的方法是保存指针,如下:

 

union {

         int nIntData;

std::string * ptrStr;

};

 

这样,我们需要在operator =以及copy constructor中手动管理内存。当Any初始化为String类型时,需要动态申请内存,而如果构建好的Any中所含的类型在operator =中由String转为其他类型时,需要将其动态释放,在由其他类型转为String时,则需要动态申请。如果这里的其他类型是另外的动态类型,且也是通过指针保存在union结构中的,则也需要做相应的释放&申请处理。

 

如下:

Any& operator = (int inData)

{

         switch (m_type)

         {

         case ANYTYPE_STRING:

         delete ptrStr;

nIntData = inData;

                   break;

         // other cases

};

m_type = ANYTYPE_INT;

}

 

Any& operator = (std::string& inData)

{

         switch (m_type)

         {

         case ANYTYPE_INT:

                  ptrStr = new std::string(inData);

                   break;

         // other cases

};

m_type = ANYTYPE_STRING;

}

 

除了以上所述的operator = 中所增加的代码之外,constructor以及copy constructor针对std::string重载的版本也需要做内存分配。destructor中也需要根据数据的实际类型判断是否释放相应的指针。

 

现在,这种做法对于C++内置类型像之前一样是直接支持的。但对于用户自定义类型,则需要通过指针的形式,动态的创建以及释放。当Any类中包含的数据从基本类型切换到non-trivial类型或者反向切换的时候,需要释放或者分配内存;但是在基本类型之间切换的时候,不需要做动态内存分配&释放。

这种不一致性导致了代码需要根据类型不同做出不同的处理,随着Any支持类型的增多,不可避免的代码膨胀发生了,而且每增加一种新类型,需要修改所有重载版本的operator =,以及新增一份constructor及copy constructor,并在destructor中增加对应类型的判断。

最为让人恼火的是,Any在处理trivial类型和non-trivial类型时的行为不一。这非常容易导致错误。

 

下面,我们尝试把问题简化一下,通过让Any始终保存指针来避免行为不统一的问题。无论从何种类型切换到另一种类型,我们都确保必须释放先前的内存,并为目标类型分配新的内存。

 

新的数据结构可以设置如下:

class Any {

public:

         void * m_pointer;

         int m_nType;

 

// supporting types

enum {

ANYTYPE_INT,

ANYTYPE_FLOAT,

ANYTYPE_STRING,

//… other supporting types

};

 

Any(int inData) : m_nType(ANYTYPE_INT), m_pointer(new int(inData)) {}

Any(float inData) : m_nType(ANYTYPE_FLOAT), m_pointer(new float(inData) {}

Any(const std::string& inData) :

m_nType(ANYTYPE_STRING), m_pointer(new std::string(inData)) {}

Any(const Any& rhs) : m_nType(rhs.m_nType) {

         switch (m_nType)

         {

         case ANYTYPE_INT:

                   m_pointer = new int(*((int *)rhs.m_pointer));

                   break;

         case ANYTYPE_FLOAT:

                   m_pointer = new float(*((float *)rhs.m_pointer));

                   break;

         case ANYTYPE_STRING:

                   m_pointer = new std::string(*((std::string *)rhs.m_pointer));

                   break;

}

}

Any& Operator = (int inData) {

if (m_nType == ANYTYPE_INT)

         *(int*)m_pointer = inData;

else {

         switch (m_nType)

         {

         case ANYTYPE_FLOAT:

                   delete (float*)m_pointer;

                   break;

         case ANYTYPE_STRING:

                   delete (std::string*)m_pointer;

                   break;

}

m_pointer = new int(inData);

};

}

// other operator equals…

};

 

现在的情况比之前好很多,我们不用再为Any在对待trivial类型数据与non-trivial数据时的行为不一而头痛了。

但是问题依旧很麻烦,因为使用void指针消除了存储时的类型信息,所以当delete指针时,我们需要人为的指定每一处指针所指代的类型,从而使编译器得以调用正确的析构函数。

从而,每一个重载版本的operator =当中,我们都需要判断当前的类型是否与传入参数的类型相符,若不相符,需要根据存储的类型标识符m_nType对m_pointer转型,并使用delete operator完成内存释放的工作,而后再为传入参数分配新的内存。

 

来回转型可能令你觉得厌烦。或许你会想到将不同种类的指针放在union当中,这样就不必为void指针转型了。但这样做行不通,原因是我们依旧需要根据m_nType的类型决定使用union中的哪个成员,实际上依旧等价于上面的做法,只不过省却了转型操作符(将转型操作符的工作移交到union的定义当中了)。

 

难道没有更好的方法吗?

在思考如何实现Any的过程中,我们发现了它的两个特点,譬如:

1、  我们需要保存类型信息:m_nType以及相应的enum定义;

2、  在使用Any时,我们需要明确的指出其contain的数据类型,以此得到正确的数据。(例如,使用int构建的Any,从Any中把数据拿出来时,目标也应当是一个int,而不能是float,否则会调用到错误的重载函数,从而得出错误结果)

3、  我们需要针对Any支持的所有类型实现constructor & operator = & operator T的重载版本。

 

为什么不利用C++自身的设施完成这种工作呢?

由第1、2点,我们发现,Any在存数和取数的时候,需要使用对应的数据类型,从而调用匹配正确的构造函数/operator =以及operator T() (这里的T指代各种类型如int, float)的重载版本。而且,Any被赋予一个值之后,再未被再次赋予其他数据类型的值之前,类型信息是始终保存在Any当中的。而当Any保存的数据类型变动时,对应的类型ID也需要更新。在这里,C++的运行时类型信息(runtime type info)正是用武之地。

 

在实现针对不同类型的重载函数时,我们发现几乎绝大多数工作都是重复或者类似的,在早期的版本中,甚至可以用宏来节省代码编写的工作量。C++中的模板正暗合了这里的需求。

 

下面,我们开始随着Ogre::Any的设计思路前行。(Ogre::Any使用了Boost::Any,但在数值Any以及stream操作上做了扩展)

 

如果利用模板,我们就不必再为每一种需要支持的类型写一个重载版本的constructor & operator = & operator T了。

C++的成员函数模板使我们可以写下类似下面这样的代码:

class Any {

public:

template <typename ValueType>

Any(const ValueType& v);

 

template <typename ValueType>

Any& operator = (const ValueType& v);

 

template <typename ValueType>            

operator ValueType() const;                       // 实际实现中并没有定义这个

};

 

但是operator ValueType()的约束太过于宽泛了,有了它的存在,现在Any可能被用在任何我们意想不到的地方。所以实际的实现中Ogre::Any并没有定义转型操作符,而是使用了名为any_cast的函数,当我们需要在某个地方从Any中取出我们想要的数据时,我们必须清楚这个Any里放着的是什么,并且明确的把它cast出来。

any_cast的声明如下:

template <typename ValueType>

ValueType* any_cast(Any* anyVal);

 

template <typename ValueType>

const ValueType* any_cast(const Any* anyVal);

 

template <typename ValueType>

ValueType any_cast(const Any& anyVal);

 

这样我们就可以用像使用static_cast一样的方法,使用any_cast,如下:

Any anyVal(3246);

int nVal = any_cast<int>(anyVal);

 

OK,模板现在节约了我们大量的重复劳动,一个模板就涵盖了所有的可能,我们不必再为以后需要新增加的数据类型而头痛了。

基于成员函数模板的Any看起来似乎不错。但是究竟应该如何存储数据呢?

 

首先,想到的是在Any中置放一个模板成员变量,像这样:

class Any {

public:

template <typename ValueType>

Any(const ValueType& v);

private:

T m_val;

};

 

但是这样做显然行不通,因为这样一来Any类就必须是一个模板类了。这不符合我们对Any的期望。况且,一旦确定了Any的模板参数,他也就成了一个只能承载确定类型的wrapper了。这不是我们想要的。

 

那么如果不保存模板成员,而是使用模板成员指针是否可行呢?答案依旧是不可行。因为形如T* m_val;的定义依旧需要在编译期获知T的准确类型。一旦在编译期确定了准确的类型,我们就无法在运行期动态改变他了。

我们需要的是一个运行期可以动态变化的模板成员。

 

由于前面的分析,直接将一个模板成员存储于Any当中是行不通的,但是我们却可以保存一个确定类型的指针,并让这个指针应该指向实际存储我们需要的模板数据的实例。

 

通过类似于以下这样的继承关系:

class placeholder;

class holder<T> : public placeholder;

我们拥有了承载无穷种数据类型的可能性。

该继承关系如下图:

 

有了这样的数据承载类之后,Any中只需保存一个placeholder接口的指针即可。而在constructor / operator = 的时候,只需删除此前的数据,并为新的数据类型创建对应的holder<T>实例即可。

 

placeholder需要定义clone接口,用于在copy constructor中生成数据的拷贝。

需要定义getType接口,用于在any_cast中识别当前保存的数据类型与目标类型是否一致。(getType的实现可以采用此前的enum + m_nType的方法,但是这种方法的局限性在于我们需要为每一个可能的类型增加一个标识符,因此更好的做法是使用C++的运行时类型识别信息RTTI:Runtime type info / Runtime type identify)

 

实际的placeholder定义如下:

class placeholder {

public:

virtual ~placeholder() {}                               // 虚析构函数用以保证派生类的析构函数得以调用

virtual placeholder * clone() const = 0;  

virtual const std::type_info& getType() const = 0; // 返回rtti信息

virtual void writeToStream(std::ostream& o) = 0;  // 串行化支持

};

 

真正的承载数据的类模板holder定义很简单,实现相应的基类接口即可,如下:

template<typename ValueType>

class holder : public placeholder

{

public: // structors

 

holder(const ValueType & value) : held(value) { }

virtual const std::type_info & getType() const { return typeid(ValueType); }

    virtual placeholder * clone() const { return new holder(held); }

         virtual void writeToStream(std::ostream& o) { o << held; }

 

ValueType held;

};

 

这样一来,Any的定义就自然而生了:

class Any {

public:

  Any() : mContent(0) {}

template<typename ValueType>

explicit Any(const ValueType & value)

: mContent(new holder<ValueType>(value)) { }

Any(const Any & other)

: mContent(other.mContent ? other.mContent->clone() : 0) { }

virtual ~Any() { delete mContent; }

 

Any& swap(Any & rhs)

  {

       std::swap(mContent, rhs.mContent);

    return *this;

  }

template<typename ValueType>

  Any& operator=(const ValueType & rhs)

  {

       Any(rhs).swap(*this);

    return *this;

}

Any & operator=(const Any & rhs)

  {

Any(rhs).swap(*this);

    return *this;

}

 

bool isEmpty() const { return !mContent; }

  const std::type_info& getType() const

  { return mContent ? mContent->getType() : typeid(void); }

 

inline friend std::ostream& operator <<

( std::ostream& o, const Any& v )

{

if (v.mContent) v.mContent->writeToStream(o);

         return o;

}

protected:

placeholder* mContent;

template<typename ValueType>

    friend ValueType * any_cast(Any *);

};

 

以上就是Any的几乎所有的定义了。此前介绍的holder以及placeholder由于只在Any中被使用到,因此在实做中被定义为Any的嵌套类。

 

any_cast的定义具体如下:

template<typename ValueType>

ValueType * any_cast(Any * operand)

{

return operand && operand->getType() == typeid(ValueType)

             ? &static_cast<Any::holder<ValueType> *>(operand->mContent)->held

        : 0;

}

需要判断被cast的Any中所贮存的数据的类型是否与目标类型一致,判断采用了C++的RTTI中的typeid。当类型不一致时,返回的结果为0。这一点的行为与dynamic_cast类似(dynamic_cast在正常的转型失败时会返回0)。使用any_cast而不是用类型转换操作符的原因,一方面在于基于模板的类型转换操作符过于随意,另一方面在于any_cast的使用方式与static_cast等几乎完全一致,符合C++的使用习惯,且当需要查找程序中有多少地方使用了any_cast时,一个grep就可以简单的给出结果。

 

另外两个重载版本的any_cast皆是调用上述指针版本的any_cast完成的。

 

至此,关于Any的实现的探讨告一段落。

最终Ogre::Any利用C++的模板特性,RTTI特性实现了一个非常具有实用价值的任意类型构件。由于采用了指针存储数据,如果大量的使用Any,会比基于union以及基本类型的实现方式速度缓慢一些。

然而对于期待高速度的多类型组合结构,在Boost中存在另一个构件可以达到相应目的,即Boost::Variant。

Boost::Variant可以像这样使用:Boost::Variant<int, float, std::string, vector<int> > myVariant;

具体Boost::Variant的使用以及实现方式,不在本文的探讨范围内。如果对此感兴趣,可以参阅Boost的文档:http://www.boost.org/doc/libs/1_37_0/doc/html/variant.html

 

另外,关于Boost::Any的实现,刘未鹏还撰写过一篇非常优秀的文章:

http://blog.csdn.net/pongba/archive/2004/08/24/82811.aspx

 

 

 

 

附:Ogre中除了使用Boost::Any之外,还对Any在数值上的应用做了扩展:

Ogre在Any的基础上,实现了+-*/的数学运算操作符,构成了AnyNumberic类。

Any::placeholder接口实现了getType,clone,writeToStream以实现对于实际数据的取类型,拷贝,写流操作。

在AnyNumberic中,numplaceholder的实现则需附加定义一套加减乘除的操作接口,从而使得AnyNumberic可以实现+-*/等运算符。(Any的结构此前已经分析过了,通过调用placeholder的接口实现对实际数据的操作,而placeholder接口背后的实现,则是一个根据初始化Any的实际类型实例化的模板类,在AnyNumberic中,这一结构相同)

class numplaceholder : public Any::placeholder {

         virtual ~numplaceholder() {} // override virtual destructor

         virtual placeholder add(placeholder* rhs) = 0;

         virtual placeholder subtract(placeholder* rhs) = 0;

         virtual placeholder multiply(placeholder* rhs) = 0;

         virtual placeholder multiply(float rhs) = 0; // override version

         virtual placeholder divide(placeholder* rhs) = 0;

};

 

template<typename ValueType>

class numholder : public numplaceholder {

//…

};

利用这一实现,AnyNumberic即可采用如下方式实现operator运算符:

AnyNumeric AnyNumeric::operator+(const AnyNumeric& rhs) const

{

         return AnyNumeric(

                                     static_cast<numplaceholder*>(mContent)->add(rhs.mContent));

}

其中mContent为继承自Any的数据成员,类型为Any::placeholder的指针。

AnyNumberic没有定义新的数据成员,仅仅是提供了一些新的接口,并通过派生Any::placeholder在不改动旧有功能的基础上,提供了新的数学运算的能力。

Go to Top