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:


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()) 
        for (int i = 0; i < m_nSize; i++) 
            ar << m_pData[i]; 
        DWORD nOldSize = ar.ReadCount(); 
        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*) 
        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 !

    Part I: Software Engineering and Computer Games
    Part II: Software Engineering and Computer Games Reference