????????由于歷史原因,在借鑒某些特定出名的游戲引擎中,不知道當時的作者的意圖和編寫方式 特此做這篇文章。(本文出自游戲編程精粹4 中 使用自定義的RTTI屬性對對象進行流操作 文章)
????????載入和 保存 關卡,并不是一件容易辦到的事。而在游戲開發的過程中,用于處理關卡數據的軟件工具也在不斷地進化,這樣一來,載入和 保存關卡就更困難了。寫作本文的目的是要提出一種方法,盡可能將組成關卡的變量的流操作(streaming)和編輯予以自動化,以便簡化甚至徹底消除新舊版本數據文件之間的兼容性問題。文中提出的方法是由三個簡單元素共同形成的:擴展的RTTI(RuntimeTypeInformation)系統,與各類變量關聯的屬性,還有一個對象工廠(object factory)。
????????RTTI是一個負責保存程序使用到的類的元數據(元數據“的系統.例如,在[Wakeling01] 一文中描述的實現里,類的元數據包含一個ID (例如類的名稱)和一個指向其父類Meta的指針(這里不支持多重繼)承)。可以根據對象的地址來訪問這個對象所屬的類的元數據,并在元數據指針形成的樹結構中找出繼承關系。這常用來在運行時檢測某個C++對象是否是某個類或子類的實例,以便在使用多態結構的時候能夠安全地從基類轉型到子類(向下)。
????????C++語言內建對RTTI元數據的支持,每一款較新的編譯器都能夠為我們生成rtti信息.例如Dynamic_Cast操作符,利用C++RTTI來進行安全地轉型。
//pBase 只想一個基類
//Derived 繼承自 Base 基類
Derived * pDerived = dyanamic_cast<Derived*>( pBase );
????????若pBase實際指向一個子類對象,則pDerived包含一個轉型后的對象地址,否則為nullptr.
????????如同 [Wakeling01] 和 [Eberly00] 中所談到的那樣,我們可以設計一個自己的 RTTI 系統,自行設計的好處在于不必局限于莫格特定編譯器的CRTTI實現,也不必事無巨細地為每個類都保存RTTI metadata。而且,通過使用自定義CRTTI系統我們能夠任意擴展metadata,使其包含C++標準的metadata 中不包含的信息。
class CRTTI
{
public:CRTTI(const std::string& strClassName,const RTTI* pBase,ExtraData* pExtra = nullptr,) :m_pBaseRTTI(pBase),m_pExtraData(pExtra) {}virtual ~CRTTI() {}const std::string GetClassName() const { return m_strClassName; }const CRTTI* GetBaseRTTI() const { return m_pBaseRtt}protected:const std::string m_strClassName;const RTTI* m_pBaseRTTI;ExtraData* m_pExtraData;
}
????????有4個宏 (macro) 可助你將自定義的RTTI整合到你的類中,他們是
????????DECLARE_RTTI 在類的定義中增加一個靜態的 RTTI 的成員,并同時定義用來訪問該成員的虛方法GetRTTI()。
? ? ? ? DECLARE_ROOT_RTTI 用在繼承關系樹中的根類的定義中,除了添加DECLARE_RTTI宏會增加的那些成員以外,此宏還增加了RTTI系統所需的方法。
? ? ? ? IMPLEMENT_ROOT_RTTI 用在根類的實現文件中,將DECLARE_ROOT_RTTI定義的靜態metadata 予以初始化。本宏只有一個參數:類的名稱。
? ? ? ? IMPLEMENT_RTTI 與IMPLEMENT_ROOT_RTTI很相似,但是用在子類中,但是用在子類中,有一個額外的參數是父類名稱.
下面是個例子
//RootClass.h .h文件
#include "RTTI.h"class CRootClass
{DECLARE_ROOT_RTTI;...
}//RootClass.cpp .cpp文件
#include "RootClass.h"
IMPLEMENT_ROOT_RTTI(RootClass);
...//派生類 .h
#include "RootClass.h"class CDerived : public CRootClass
{DECLARE_RTTI;...
}//派生類 .cpp
#include "Derived.h"
IMPLEMENT_ROOT_RTTI(Derived, RootClass);
...
屬性
我們可以創建屬性(property)來代表類里的變量[CafrelliO1]。每個屬性的組成包括:名字、類型(表明了該變量的內存開銷大小)、該變量從類定義的頭部開始的偏移量、文字形式的描述(可選),還有一些標志(flag)。通過標志來表示一些信息,諸如該變量是否是可被編輯的(editable)還是只讀的(read-only),是否需要被保存,等等。一定要注意,每個屬性只能在特定類中被定義一次,然后即被該類的所有實例所使用.這就解釋了它為什么不包含指向指定變量的指針,但卻包含一個偏移量(與對象實例的地址相加,以訪問變量的值)。
????????在已有的類中開始定義屬性之前,我們必須通知框架我們希望將屬性保存在類的元數據中。對指定對象而言,這允許我們訪問對象所屬的類(以及繼承而自的基類)的屬性.這正是我們對對象實例進行編輯和Streaming操作時所需要的功能。
????????為簡化該操作,在ExtraProp.h中定義了兩個宏:DECLARE_PROPERTY和IMPLEMENT_PROPERTIES。前者的使用方法如下:
class CMyClass : public CPersistent
{DECLARE_RTTI;DECLARE_PROPERTIES(MyClass. ExtraProp);
public://尋常接口
protected:bool m_boSelected;
}
????????每個要使用RTTI編輯/保存系統的類,都必須繼承?Persistent類。稍后我們會看到這個類是負責Streaming處理的。當然,如果一個類需要RTTI支持,但并不希望定義任何屬性,那么在定義時可以僅使用DECLARE_RTTI宏。
????????DECLARE_PROPERTIES宏有兩個參數:CMyClass是包含該宏的類的名字,CExtraProp是另一個類的名字.后者是從RTTI中的CExtraData類繼承來的,負責保存額外的元數據,其中保存著一個屬性列表.DECLARE_PROPERTIES在CMyClass中聲明了一個靜態成員:CExtraProp.同時也插入了一個用來訪問它的靜態函數GetPropList().這個宏最后還聲明了一個靜態方法DefineProperties(),你可以在CPP文件中找到它的實現.
#include "NyClass.h"
#include "properties.h"IMPLEMENT_RTTI_PROP ( CMyClass, CPersistent)
IMPLEMENT_PROPERTIES ( CMyClass, CExtraProp)bool CMyClass::DefineProperties()
{ //靜態REGISTER_PROP(Bool, MyClass,m_boSelected, "Selected",Property::EXPOSE | Property::STREAM,"help or comment");return true;
}
????????IMPLEMENT_RTTI_PROP 是 IMPLETE_RTTI 的一個新版本(這兩個宏是互斥的),它對類的RTTI數據成員進行初始化,使其指向由DECLARE_PROPERTY宏定義的CExtraProp對象實例。還有一個IMPLEMENT_ROOT_RTTI_PROP宏,當需要在根類中支持屬性的時候,可替代IMPLETER_ROOT_RTTI宏。在這些宏的幫助下,我們在我們的RTTI系統和類中的屬性之間建立了聯系.
????????IMPLEMENT_PROPERTIES 和 DECLARE_PROPERTIES接受同樣的參數。它負責實現靜態的CExtraProp成員,并將 DefineProperties() 這個函數指針作為參數傳給CExtraProp的構造函數.
????????正如它的名字表示的那樣,DefineProperties() 的功能是初始化其所屬類的屬性。當構造用來保存屬性的CExtraProp類的實例的時候,初始化將自動地進行一次.
????????最后、REGISTER_PROP為類添加新的屬性比如說有一個名叫“選定”的布爾型屬性(Bool)、與類CMyClass中的m_boSelected變量有聯系。該屬性是可被編輯的(CPropert::EXPOSE標志),備注欄寫著“幫助或注釋”,并允許被流式地輸出到外部文件中(CProperty::STREAM標志)以備后用。就像這里用的Bool一樣,屬性的各種類型是通過一個定義在屬性.h中的一個枚舉值來定義的。
????????這個例子是故意寫的這么直接易懂的:我們馬上將會看到,DefineProperties() 能包含宏列
表以外的元素.表1.12.1給出了范例程序中實現了的屬性。
為已有的類增加屬性的步驟總結如下.
(1)若該類尚未支持我們的rtti系統,先增加rtti支持。
(2)將 CExtraProp 類作為第二個參數,調用DECLARE_PROPERTIES 和IMPLEMENT_PROPERTIES宏。這將在類的 metadata 信息塊中增加一個屬性列表。
(3)通過將實現_RTTI替換為IMPLEMENT_RTTI_PROP(或替換為其用于基類的對應宏),在rtti系統和屬性之間建立聯系.
(4)實現 DefineProperties,調用 REGISTER_PROP 來創建自己的屬性定義,從而將屬性與用于編輯或流操作的變量聯系起來.
編輯屬性
????????為類定義好屬性后,下一步就是利用屬性來顯示和修改內存中的對象實例的內容。
????????為了顯示對象實例中變量的值,我們需要訪問類中的metadata,取得保存在metadata中的屬性,要求屬性返回該對象實例中某個變量的值。請看如下代碼:
//pObj 指向一個由CPersistent派生而來的類的對象實例
const CRTTI * pRTTI = pObj->GetRTTI();
while(pRTTI)
{CExtraData* pData = pRTTI->GetExtraData();CExtraProp* pExtra = DYNAMIC_CAST(CExtraProp, pData);if(pExtra){CPropList* pProp = PExtra->GetPropList();while(pProp){//進行任何處理,例如:Display(pProp->GetValue(pObj));pProp = pList->GetNextProp();}}pRTTI = pRTTI->GetBaseRTTI();
}
????????????????在上面一段代碼中,可以看到屬性有一個虛方法GetValue(),它接受對象的地址作為參
數,以字符串的形式返回相應變量的值.這恰好是我們在控制臺窗口或編輯控件等處,將值顯示出來所必需的.不過,也可以按確切類型來返回值,請看下面這一段代碼:
//pProp 是一個CProperty* 類型的變量
CPropFloat* pFloat = DYNAMIC_CAST(CPropFloat, pProp);
if(pFloat)
{float fValue = pFloat->Get(pObj);...
}
????????區別在于,此處我們必須在執行恰當的轉型之前,事先知道屬性的確切類型.如上所示,因為屬性類使用我們自定義的rtti系統,類型驗證是輕而易舉的.
修改值
? ? ? ? 大多數情況下,對屬性相關聯的值進行修改和將其顯示出來一樣簡單。
?????????● 若新的值是從控制臺窗口或編輯控件中傳來的文本,首先要將這文本傳給CProperty類的SetValue()方法:若文本與屬性的類型并不相符(比如屬性是浮點數類型,卻讀入了一個
"a00"),屬性相應的變量的值維持不變,并且 SetValue() 返回false報錯。若類型相符,則值
被轉換,變量被修改。
????????● 若是知道屬性的真正類型,我們可以對其進行類型轉換,通過該類的訪問操作符(accessor)來設定新值。不過,有一些屬性需要特殊的編輯方法,常見的例子如指向其他persistable對象實例的指針。特殊的編輯方式主要有兩類。
????????● 我們不希望將地址作為16進制數顯示出來,因為用戶無法理解一個地址值。實際上,我們希望將被引用的對象的邏輯名顯示出來。
????????● 用戶能夠在一個列表中選取要引用的對象,該列表列出了相應類型的現有對象。其他類型的屬性也能夠從特殊編輯方式中獲益,例如在調色板中選取顏色,或以歐拉角度的形式輸入四元數(quaternion)。為了處理這些需求,范例程序中的framework調用了下面幾個CPersistent類的虛方法,從而繞過了默認的顯示和修改行為:
????????● SpecialGetValue()負責提供要顯示的文本。例如,對于指針屬性,我們可以返回被引用對象的名字,而不直接返回地址。
????????● 當需要支持特殊編輯方法時,SpecialEditing()負責處理用戶的輸入。這通常分兩步:打開相應的對話框,處理結果。例如可以用它來支持用戶在列表中選取對象。
????????● 每當用戶輸入了新的值時,ModifyProp()在SetValue()之前被調用。在直接調用 SetValue()不敷使用的情況下,ModifyProp()允許程序員對屬性輸入執行一些額外的處理。
保存
????????每個對象數據的定義都由<data class=..ID=..>這個tag開始,由</data>這個tag結束。class
參數用于識別對象的類型,這在載入對象需要重新創建這個實例時有用。ID代表該對象實例,作為能被其他對象所引用的名字。顯然這些ID必須是惟一的,那么該如何生成它們呢?在我們的這個實現中,我們采用對象在內存中的地址作為ID[Eberly00]。這一做法的主要缺陷在于,若我們將某個關卡重復載入和保存多次,就算期間并未進行任何修改,由于對象實例的地址在任意兩次程序運行中都可能有所不同,得出的關卡文件也會不同。
????????是CPersistent類提供了保存對象并將其所有屬性寫入磁盤的service。每個可以進行流操作的(Streamable)對象都繼承自CPersistent類。這一過程與之前我們看到的顯示實例的變量值的操作非常相似。但是在這里,有關指針的問題需要特別處理。
????????當保存一個對象,而這個對象包含一個引用著另一個persistable對象的指針屬性時,要執行下列步驟。
????????(1)和任何其他屬性一樣,該屬性將值寫入文件。對于指針屬性的情況,值是指向對象的地址。像前面說過的那樣,內存地址直接用作文件中的對象ID,因此無需轉換。
????????(2)如果指針不是NULL,它所指向的對象地址被添加到一個在系統內部自動維護的引用列表中。
????????(3)當處理完當前對象的流操作后,依次從引用列表中取出所有地址,并將其指向的對象進行保存。
????????(4)由另一個類記錄已經保存過的對象的地址,以避免在同一個文件中對單個對象實例進行多于一次的初始化,也提供了對循環引用的數據的支持。
????????這就是為什么只要保存場景的根(scene root),整個場景就會遞歸地被保存下來以備后用。
載入
????????將保存下來的數據文件重新載入的時候,必須要根據從文件中取出的類的ID來創建對象。對象工廠[Alexandrescu01]正是為這個問題而設計的。創建對象實例時,必須讀取其中包含的數據。遍歷保存下來的屬性,讀入每一個屬性的值,并將其賦予相應的變量,就像這個值是用戶手工輸入的一樣。
????????對于指針的情況,仍然有問題有待我們解決。問題是,不但新的對象實例很有可能與它們先前被保存時具有不同的地址,而且我們希望指向的對象也許還沒有創建完成。因此,我們需要在保存下來的實例ID和實際對象的地址之間建立某種映射關系,也就是在創建對象之后還要執行一個步驟:Linking。
鏈接(linking)
????????鏈接意味著,當所有對象載入完成后,要將指針屬性的值全部替換為被引用實例的實際內存地址。為此,我們可以創建一個STLmap:鍵值(collection key)是讀取得到的對象ID,相應的值(associatedvalue)是由類工廠返回的新地址。當加載操作從文件中讀取一個指針屬性的值時,要執行以下步驟。
????????(1)屬性將其指針設為NULL。這是為了確保每條指針都被初始化為一個可以測試的值。
????????(2)對象的ID就是該對象被保存(persist)時的內存地址。因此若屬性載入的ID等于零,我們可以推斷這保存下來的對象在得到保存的時候就具有一個NULL值指針了。由于屬性已將指針設為NULL,下面的步驟就沒有執行的必要,不會被執行的。
? ? ? ? (3)若ID不等于零,則屬性創建一個CLinkLoad對象并將其添加到一個列表中,該列表包含當載入完成時需要恢復的全部鏈接。在CLinkLoad實例中保存著擁有指針屬性的對象的地址,屬性的地址,和對象ID。
????????當所有對象都被創建和加載后,對鏈接進行處理,過程如下。
????????(1)對列表中的每一個CLinkLoad實例,在已經創建的對象組成的map中查找被引l用的對象的ID,并取得相應的內存地址。
????????(2)用該地址作為參數,調用CLinkLoad對象中引I用的屬性的Link()方法。
????????這條方法的功能是將地址賦予相關的指針變量。
????????如果指針的鏈接失敗,比如在map中沒有找到相同的ID,該對象也不會包含無效的指針值,而只會在加載時通過屬性得到NULL值。這個方法可能并不完美,但它確實避免了創建使用后未清除的垃圾指針(dangling pointer)。一般而言,鏈接不可能失敗,但若確實失敗了,可能意味著該文件已被損壞或破壞過。
????????最后,通過遍歷鏈接操作中用到的map,對每個加載的對象調用CPersistent類中的PostRead()方法。因為該map中包含所有由對象工廠返回的對象,因此這就使得每個類都執行各自特定的初始化操作[Brownlow02]。
與舊版本文件的兼容性問題:類的描述
????????有了流操作系統,下面讓我們來看一下,當有人修改了(比方說增加了新的)類的屬性時會發生什么情況。由于在程序中注冊在案的屬性與之前用舊版本程序保存下來的屬性不再對應,因此程序無法再從這個文件中讀取數據。
????????我們可以這樣:存在數據文件中也好,存成單獨的文件也好,總之將其中包含的metadata數據的描述也保存下來。也就是說,在每個類寫入到文件中時,將該類的所有屬性(名字和類型)列表予以保存,同時也將該類基類的相關數據予以保存。例如,某個游戲引擎可以將一個球對象的實例保存為下面形式的定義:
<class name="CRefCount" base="">
</class><class name="CPersistent" base="CRefCount"><prop name="Name" type="String"/>
</class><class name="CEngineObj" base="CPersistent">
</class><class name="CEngineNode" base="CEngineObj"><prop name="Subnodes" type="Fct"/><prop name="Rotation" type="Vect4D"/><prop name="Position" type="Vect3D"/><prop name="Draw Node" type="Bool"/><prop name="Collide" type="Bool"/>
</class><class name="CEngineSphere" base="CEngineNode"><prop name="Radius" type="Float"/><prop name="Section Pts" type="u32"/><prop name="Material" type="Fct"/>
</class>
<data class="CEngineSphere" id="OxD7E7co">sphere000100; 0; 0; 110;-0.5; 0truetrue180x0
</data>
????????可見,CPersistent類是從另一個叫做CRefCount類(這是一個引用計數類,參見[Meyers96])繼承而來的。CEngineObj類的描述中并不包含任何屬性定義,但這并不表示該類沒有成員變量,只是說它沒有需要被序列化保存的成員變量而已。
????????在剛才的例子里,得到保存的對象地址是0xD7E7C0,其名字是“sphere0001”,球的位置是(10;-0.5;0),等等。如果另一個CEngineSphere類(或其他父類如CEngineNode)的實例也被保存在同一個文件中,那么類的定義并不會被重復保存,只是會寫入新的<data..>數據塊而已。這里我們用到了幾種類型的屬性,有布爾型、浮點數、32位整數、字符串、矢量等。稍后我們會討論有關“函數”類型("Fct")的特殊情況。
與舊版本文件的兼容性問題:匹配
????????假設我們的類的描述已經被存進一個文件,類的實例被存進另一個單獨的文件。我們的載入子程序首先取出描述(可能已經過時了),將其與內存中當前版本軟件中的描述進行比較。這一步稱為“匹配”,試圖在文件中的類的屬性和內存中的類的屬性之間建立聯系。具體情況有三種。
????????● 某個類在可執行文件中的屬性和它外部文件中的屬性具有相同的名字和類型。在這種情況下,屬性將獲得外部文件中的數據。你能看到,映射并不是按照屬性在描述中出現的順序建立的,而是需要比較屬性的名字和類型。這使得我們可以改變次序、交換、刪除或插入屬性,甚至可以將屬性移到有繼承關系的另一個類中去(在前面的例子里,我們可以決定說:Collide標志應該是CEngineObj類的成員,而不是CEngineNode的成員,在這樣做的同時我們依然可以載入之前保存而成的文件)。此系統的限制也是很顯然的:在類或其父類的屬性列表中,不可以同時存在兩個或以上的對象具有相同的類型和名字。為了執行該規則,可以在注冊屬性的時候進行測試。
?????????● 在可執行文件中并沒有找到與文件中的屬性相同的屬性。在這種情況下,屬性是不被使用的(obsolete),其值被忽略。更確切地講,一條叫做ReadUnmatchedO的虛方法將被調用,因此應用程序可以提供一些自定義的行為(例如在日志文件中輸出一條警告)。
?????????● 有一個在可執行文件中出現的屬性,在文件中找不到它。在這種情況下,屬性不會收到任何數據,相應的變量將維持由類的構造函數賦予的默認值。每當在文件已經保存之
后新建屬性,就會出現這種情況。再次保存該文件,則新的屬性同時會被添加到描述和數
據中。
????????執行屬性匹配的代碼主要分布在CPersistent類的RecursiveMatch()和MatchProperty()方法中。該類在文件中的每個屬性與可執行文件中的屬性之間建立聯系(如果存在聯系的話)。當載入文件時,對象數據從文件里被載入可執行文件中的相應屬性。因此,不論有多少對象要寫入文件,匹配對于每個類總是只執行一次。
"函數" 屬性
????????到目前為止,我們的實現已能夠處理一些簡單類型(布爾型、無符號長整數、浮點數)、類(字符串、矢量)以及指針。每個屬性對應類中的一個成員變量,因此屬性的大小是已知的。可是如果我們要保存容器的內容——比方說指針列表呢?此時,我們遇到了一些根本問題。我們既無法事先知道該容器中有多少個對象,不同的容器中對象的類型、訪問方式也都不同。這些特殊情況都由CPropFct類管理。
????????“函數”類型的屬性讓我們指定在framework執行Get(從屬性變量轉換到字符串)、Set(從字符串轉換到屬性變量)、Write(寫入文件入、Read(從文件讀入)或Link操作時調用的函數的地址。下面是從CEngineNode:DefineProperties()中提取出來的一段代碼:
CProperty* pProp = REGISTER_PROP(Fct,...);
CPropFct* pFn = DYNAMIC_CAST(CPropFct,pProp);
pFn->SetFct (NULL,NULL,WriteNodes,ReadNodes, LinkNodes);
????????其中有些指針可以是NULL。在前面的例子里,“Subnodes”屬性只支持?streaming?和?linking?操作。作為參數的這三個函數指針的功能分別為處理保存、載入和鏈接一個元素為節點指針的STL表。
????????● WriteNodes()首先寫入容器中保存的指針數量,然后依次寫入各個指針的值[Beardsley02]。
????????● ReadNodes()首先讀取保存著的指針個數,然后為其中的每一個都創建一個CLinkLoad對象,在鏈接階段會用到的。
????????● 在每個由ReadNodes()讀入并創建的對象上調用LinkNodes()。它將被引用的地址插入己加載的對象的節點列表中。????????
????????當然,這只是個例子。一個類可以根據需要,支持任意數量的“函數”屬性。
技巧和提示
在我們現有的屬性和RTTI系統中,有下面一些技巧可用。
????????● 可以將數個屬性映射到同一個變量上。例如,你可以定義一個屬性將角度按弧度單位(這是游戲引擎能夠直接進行計算的單位)來保存,再定義另一個屬性將角度按度(degree)來保存。
????????● 類中的屬性保存在類的RTTI的附加數據(即CExtraProp類)中。當然我們也可以通過繼承CExtraProp類,為類添加其他數據,而不會影響之前描述過的機制。請注意,僅當這新增數據需要和繼承關系配合使用的時候,才有必要往CExtraProp的子類中添加數據,否則只需要將這數據記為靜態變量就足夠了。
????????● 在注冊屬性的時候,要指明該屬性是否將在用戶界面中被顯示出來,以及是否是只讀的。但是有時候,你可能會希望在一個類的實例中顯示某個值,但在該類的所有子類中隱藏該值。更進一步,你可能會希望某個屬性僅對特定的對象實例而言才是可以編輯的。例如,在編輯工具中,一些camera的位置是固定的(座標屬性是只讀的),但另外一些camera可以自由運動。你可以通過在牽涉到的類中重載IsPropExposed()和IsPropReadOnly()這兩條方法來實現該功能。
思考
????????若能實現下面這些功能,將使我們的RTTI系統更為有用。
????????● 文件兼容性問題僅在游戲開發的過程中需要解決。當游戲軟件開發完畢后,所有的文件都(應當)保存為各自的“最終版本”。因此,可以刪除屬性的文字描述,若載入子程序沒有找到這些描述,必須充分信任應用程序,并按照可執行文件中定義的格式讀取數據。
????????● 這個系統并不支持集合體(aggregation),也就是一種由其他類充當成員變量的類。當一個類的實例被保存,該實例中的所有屬性都被寫入文件。使用一種新的屬性類型應該就足以為系統增加該功能了。不過到目前為止,這功能并不必要,因為可以利用指向對象實例的指針屬性作為通融辦法。
????????● 當屬性的值發生了變化(通過ModifyProp()或SetValue())時,變化操作可以被一個singleton管理器偵測到。在體系上作此變動后,在大多數情況下,可以很直觀地實現UNDO(撤銷值的變化)功能。
結論
????????本文介紹了這樣一種方法,通過向每個類中維護的自定義的運行時類型信息(RTTI)中添加特定的屬性,而使對C++對象的編輯、載入和保存操作自動化.對象之間的鏈接(例如指針“也被考慮在內,且能夠在載入時得以重建.在保存屬性的同時也保存屬性的描述,從而我們可將其與編譯在當前執行文件中的元數據進行比較,干凈利落地處理潛在的新舊版本文件不兼容的問題.
????????
使用本方法的代價并不十分高昂:屬性是靜態對象,不會占用很多內存.耗時最多的操作就是在載入文件時,將在保存得到的文件中讀出的屬性與可執行文件中的屬性定義進行比較的操作,而這操作對每個類只執行一次.在光盤上的例子程中實現的二進制格式消除了許多字符串轉換操作,而這些字符串轉換在生成直觀可讀的xml格式時是必需的。
參考文獻:
[AlexandrescuOl] Alexandrescu, Andrei, Modern C++ Design, Addison-Wesley, 2001.
[BeardsleyO2] Beardsley, Jason, “Template-Based Object Serialization,”Game Programming Gems 3, Charles River Media, 2002.
[BrownlowO2] Brownlow, Martin, “Save Me Now!” Game Programming Gems 3, Charles River Media, 2002.
[Cafrellio1] Cafrelli, Charles, “A Property Class for Generic C++ Member Access,” Game Programming Gems 2, Charles River Media, 2001.
[Eberlyoo] Eberly, David H., 3D Game Engine Design, Morgan Kauffman, 2000.
[MaunderO2] Maunder, Chris, “MFC Grid Control 2.24,” available online at www.codeproject.com/miscctrl/gridctrl.asp, July 14, 2002.
[Meyers96] Meyers, Scott, More Effective C++, Addison-Wesley, 1996.
[WakelingOl] Wakeling, Scott, “Dynamic Type Information,” Game Programming Gems 2,Charles River Media, 2001.