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在不改動舊有功能的基礎上,提供了新的數學運算的能力。