Wednesday, October 21, 2009

29.4 Serializing an array of pointers



[ Team LiB ]










29.4 Serializing an array of pointers


The most important part about the Pop Framework's serialization code is one single line in the cGame::Serialize
method:



_pbiota->Serialize(ar);

Getting that line to work was quite a task. Why? The _pbiota object is a CTypedPtrArray<CObArray, cCritter*>
of pointers, some of which point to cCritter
objects and some of which point to objects of child class types such as cCritterBullet, cCritterArmed, cCritterArmedPlayer, and the like. The array template type CTypedPtrArray<CObArray, cCritter*>
is defined by MFC as an alternative to the simpler template type CArray<cCritter*, cCritter*>.


First of all, we need to tell the cCritter
class and its subclasses how to write and read their data; we do that by implementing Serialize
for the class and the subclasses.


Second of all, we need to arrange things so that we save and read the contents of the _pbiota array objects and not the values of the pointer addresses. This will happen partly because we used the DECLARE_SERIAL and IMPLEMENT_SERIAL macros and partly because we are using a CTypedPtrArray<CObArray, cCritter*>
array rather than a vanilla CArray<cCritter*, cCritter*>.


Third of all, _pbiota is a polymorphic array of pointers, some of which are cCritter*
pointers and some of which are various child critter class object pointers, so we need some way to tell which is which when we are writing and reading their data. This, again, is taken care of by the fact that we used the SERIAL macros and the fact that we used the special complicated kind of CArray
template CTypedPtrArray<CObArray, cCritter*>
instead of the simpler CArray<cCritter*, cCritter*>.


It turns out that the default behavior of a standard CArray
is in fact the wrong thing for arrays of pointers: it block copies whatever data is in the array. That is, it writes some information that you totally don't care about: the numerical values of the addresses where your data objects live. What you want to write is the data that lives in the objects. There are three ways of avoiding the inappropriate serialization behavior of CArray.



Serializing a CTypedPtrArray of CObject pointers


This is the most modern approach, the one we use in Pop Framework. If your class inherits from CObject, you can use a special kind of CTypedPtrArray
instead of a CArray
.



CTypedPtrArray<CObArray, cCritter*> *_pbiota;

(To be accurate we must immediately mention that _pbiota is actually a cBiota
* object; the cBiota
class is a child of the class CTypedPtrArray<CObArray, cCritter*>.) The virtue of using the CTypedPtrArray
is that a CTypedPtrArray
'knows' it's made of pointers so it will serialize your pointers by calling the proper kind of operator>>
or operator<<
for each pointer. This technique will not work if you use the CTypedPtrArray<CPtrArray, cCritter*> *_pbiota, in fact if you use an array like this, your serialization won't work at all.


The first modifying argument to a CTypedPtrArray
definition can be either CPtrArray
or CObArray. It's the second option that we must use here. The CObArray
tells the CTypedPtrArray
it is made of pointers to serializable CObjects. The effect of the first argument is to make CTypedPtrArray<CObArray, cCritter*>
in fact be a special kind of CObArray.


[It would be more logical if the modifying argument used to specify the kind of CTypedPtrArray
were CObPtrArray, and not CObArray, but, as we've said before, if MFC were fully consistent and logical, it wouldn't be true Windows programming! (Not that any other kinds of programming are perfectly logical either.)]


If you set a breakpoint at the line _pbiota.Serialize(ar) and step though a save or load in the debugger we find that the following MFC method gets called down in an MFC source-code file called Array_O.cpp. (In order to be able to step into MFC source code, you need to have set the options to install the source code when you installed Visual Studio.)



void CObArray::Serialize(CArchive& ar)
{
if (ar.IsStoring())
{
ar.WriteCount(m_nSize);
for (int i = 0; i < m_nSize; i++)
ar << m_pData[i];
}
else
{
DWORD nOldSize = ar.ReadCount();
SetSize(nOldSize);
for (int i = 0; i < m_nSize; i++)
ar >> m_pData[i];
}
}

In looking at this code, understand that any kind of CArray, CObArray, or CTypedPtr
array has two main private fields: its m_nSize
that gives its size, and its m_pData
that gives its array of elements. In the case of an array of cCritter*
pointers, m_pData
will have the type cCritter**.


As we described in the last subsection, we have a special overloaded pointer-based extraction operator<<
and the special overloaded pointer-based operator>>
to read the pointers intelligently. Rather than copying the address values of the pointers, the CObArray
calls call these special overloaded extraction and insertion operators.


It's worth repeating that we don't explicitly define operator<<(CArchive &, const
cCritter*)
and operator>>(CArchive &, cCritter *&). These are implicitly defined by (a) making cCritter
a child of cObject, (b) putting the DECLARE_SERIAL and IMPLEMMENT_SERIAL macros in, respectively, critter.h
and Critter.cpp, and (c) prototyping and implementing a cCritter::Serialize(CArchive &ar)
method.



Serializing a CArray of CObject pointers by overloading ::SerializeElements


The second approach is to stick to the more familiar approach of defining CArray<cCritter*, cCritter*> _critters. The problem here will be, as mentioned above, that the default CArray::Serialize
method will block copy the pointer addresses. So here we need override a certain global polymorphic the SerializeElements
function. We would add some code like this to our Popdoc.cpp
file.



void AFXAPI SerializeElements(CArchive &ar, cCritter **pcritterarray,
int count)
{
int i;
if (ar.IsStoring())
for (i=0; i<count; i++)
ar << pcritterarray[i];
// Uses operator<<(CArchive &, const c*)
else
for (i=0; i<count; i++)
ar >> pcritterarray[i];
//Uses operator>>(CArchive &, cCritter *&)
}

Note the peculiar prototype for the SerializeElements
global function. Another tricky point here is that the linker will complain if you define this function in critter.h or Critter.cpp. Your override of SerializeElements
has to be defined inside the document implementation file Popdoc.cpp. A good place to keep it is right before your override of the CDocument::Serialize
function. You may be tempted to skip writing this odd little bit of code when using a standard CArray
of pointers, but if you leave it out, then when you try and read in a file, your program will crash because you will read garbage values into your pointers.



Serializing a pointer array the hard way


We said there were three approaches, so what's the third? The third is to pigheadedly do it all yourself and not even derive cCritter
from CObject. The price would be that we then need to keep a CString _classname
field inside our cCritter
and class to take the place of the CRuntimeClass
information. It's enough to just have _classname
be either cCritter
or cCritterBullet
or whatever. Note, by the way, that any child of the CObject
class has a CRuntimeClass
member, and one of the fields of CRuntimeClass
is in fact a CString
that holds the name of the class. So if you do this by hand, you're only copying what the MFC framework wants to do for you automatically.


The trick for serializing an object this third way would be to save off the name string before saving the object, and when you are reading it back in, you read in the name string and have a switch statement to construct the right kind of critter child class object pointer to read the object into. But there's no reason to work this hard. Save your energy for something other than reinventing the CRuntimeClass
!






    [ Team LiB ]



    No comments:

    Post a Comment