当前位置: 首页 > news >正文

使用自定义的RTTI属性对对象进行流操作

        由于历史原因,在借鉴某些特定出名的游戏引擎中,不知道当时的作者的意图和编写方式 特此做这篇文章。(本文出自游戏编程精粹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">
    sphere0001
    0
    0; 0; 0; 1
    10;-0.5; 0
    true
    true
    1
    8
    0x0
</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.

相关文章:

  • 7对象树(1)
  • 文本分析(非结构化数据挖掘)——特征词选择(基于TF-IDF权值)
  • Java项目打包(使用IntelliJ IDEA打包Java项目)
  • Ubuntu 22.04 LTS 下载英伟达驱动
  • 买家利益为中心的购物平台
  • 每日一题洛谷P8716 [蓝桥杯 2020 省 AB2] 回文日期c++
  • Mapbox GL JS 实现鼠标绘制矩形功能的详细代码和讲解
  • C++ | std::function
  • Spring Boot中对同一接口定义多个切面的示例,分别通过接口方式和注解方式实现切面排序,并对比差异
  • 基于方法分类的无监督图像去雾论文
  • 小白入门机器学习概述
  • 128. 最长连续序列
  • 树莓派超全系列文档--(18)树莓派配置音频
  • 快速入手:基于SpringBoot的Dubbo应用融合Nacos成为注册中心
  • 工业机器人核心算法体系解析:从感知到决策的技术演进
  • Ubuntu 系统 Docker 中搭建 CUDA cuDNN 开发环境
  • 鸿蒙应用元服务开发-Account Kit概述
  • Raspberry 树莓派 CM4模块的底板设计注意事项
  • 运维简历之项目经验(Project Experience in Pperation and Maintenance Resume)
  • InfiniBand (IB)和 以太网 的区别
  • 做视频网站都需要什么/网络营销公司哪家可靠
  • 杭州网站建设公司/洛阳网站seo
  • 广州专门做网站的公司/山东百度推广总代理
  • 公司要招个做网站的人/网络营销团队
  • wordpress旧版本哪个好些/seo免费培训
  • 网站的底部导航栏怎么做/怎样精准搜索关键词