随笔-4  评论-40  文章-117  trackbacks-0
(转载)


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

 

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

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在不改动旧有功能的基础上,提供了新的数学运算的能力。



posted on 2009-06-01 16:16 李阳 阅读(1333) 评论(0)  编辑 收藏 引用

只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   博问   Chat2DB   管理