Serialize-able Polymorphic Lists

This page stems from some of my earliest experiences trying to get a bunch of objects onto disk and back again. Having tried various complicated approaches I finally stumbled on the correct way to do it.

The aim is to have a list of objects all derived from some common base class that can be saved to disk. This is obviously a very common task in any program.

Step 1 - Derive your classes from CObject

CObject can be a pain because it defines a private copy constructor which can mean you may have to write your own copy constructor for each class (don't worry, if you use the CTypedPtrList template as I do in this example you seem to be able to get away without it). However, the advantages of the run-time type information provided by CObject far outweigh this. Our example will use objects from a simple 3D engine. All the classes are derived from R3DShape which is in turn derived from CObject.

class R3DShape : public CObject

{

public:

DECLARE_SERIAL(R3DShape) // required by MFC

R3DShape(); // default constructor

virtual void Draw(); // an overrideable draw function

virtual void Serialize(CArchive &ar); // required by MFC

protected:

float m_number;

R3DVector m_position;

};

 

class R3DTriangle : public R3DShape

{

public:

DECLARE_SERIAL(R3DShape)

void SetupTriangle(R3DVector v1...);

virtual void Serialize(CArchive &ar);

private:

float m_amembervariable;

};

 

class R3DRectangle : public R3DShape

{

DECLARE_SERIAL(R3DRectangle)

etc

etc

};

 

class R3DCube : public R3DShape { etc etc };

class R3DSpecialCube : public R3DCube { etc etc };

 

Step 2 - Use the IMPLEMENT_SERIAL macro

I remember reading one of Bjarne Stroustrup's books in which he said that macros and #defines were un-necessary and generally a bad idea in C++. Still, Microsoft have insisted on using them anyway, and they do seem to make life easier. The IMPLEMENT_SERIAL macro takes three arguments: A class, the name of it's base class, and a version number. The version number is so that you can make sure you don't try to load files created with old versions of your software. The implementation for R3DShape would look something like this:

IMPLEMENT_SERIAL(R3DShape, CObject, 1)

R3DShape::R3DShape()

{

// initialisation code here

}

The other classes must also have the macro. It is important that you put the macro in the class's *.cpp file and not the *.h file. The other calls to the macro (for the classes in step 1) will look like this:

IMPLEMENT_SERIAL(R3DTriangle, R3DShape, 1)

IMPLEMENT_SERIAL(R3DCube, R3DShape, 1)

IMPLEMENT_SERIAL(R3DSpecialCube, R3DCube, 1)

etc

 

Step 3 - Implement Serialize() functions

Each class must implement its own Serialize function. A reference to a CArchive object is the only parameter. CArchives are either passed in by the framework when the user clicks "save" or "open", or you create them yourself (having first created a CFile) object.

void R3DShape::Serialize(CArchive &ar)

{

CObject::Serialize(ar); // ALWAYS call the base class version 1st!

m_Position.Serialize(ar); // because m_Position is a CObject

if(ar.IsStoring())

ar << m_number;

else

ar >> m_number;

}

There are two important points here. First, always call the base class version of Serialize(). This ensures that all data is saved. When CObject::Serialize() is called, the run-time class information is saved so that when you load up again you get an object of the correct class. This leads on to the second point, which involves the special consideration required when serializing classes derived from CObject. Basically, if the exact type of the object is known and its memory has already been allocated (either with new or because it's an embedded object) then we use the object.Serialize(ar) method. If we haven't allocated memory yet and we know that the object we're loading is either of class X or is derived from X, then we use the << and >> operators. This is because << and >> on a CObject first figure out the class of the object we're loading and then allocate memory before returning a pointer. All this makes our job a lot easier. This is what the Serialize might look like for R3DSpecialCube:

void R3DSpecialCube::Serialize(CArchive &ar)

{

R3DCube::Serialize(ar); // call base class version

m_pData1->Serialize(ar);

m_Data2.Serialize(ar);

if(ar.IsStoring())

ar << m_D1 << m_D2 << m_D3;

else

ar >> m_D1 >> m_D2 >> m_D3;

}

Here, m_pData1 is a pointer to a CObject whose memory was already allocated, probably in R3DSpecialCube's constructor. m_Data2 is an embedded object. m_D1 might be some data of a standard type or it might be a CObject whose memory has not been allocated yet.

 

Step 4 - Use the CTypedPtrList template

My first impulse was to use CLists and CArrays for everything. But they caused major headaches and after reading the Scribble tutorial I discovered that CTypedPtrList was a much simpler way of doing things. Without going into too much detail, we create a list of R3DShapes like this:

CTypedPtrList<COblist, R3DShape*> mylist;

This will give you a list of pointers to R3DShapes, which may be R3DShapes or any class derived from R3DShape like R3DTriangle or R3DSpecialCube. This is where the polymorphism comes in! There are various things you might want to do with a list. In our example, we have a class R3DModel which contains a list of R3DShapes:

class R3DModel : public CObject

{

public:

DECLARE_SERIAL(R3DModel)

R3DModel();

~R3DModel();

virtual void Serialize(CArchive &ar)

R3DShape* AddShape();

protected:

CTypedPtrList<CObject, R3DShape> m_Shapelist;

}

You'll notice this is very similar to the CScribbleDoc class in the Scribble tutorial. Basically, R3DModel manages this list. The various implementations look something like this:

// This creates a new R3DShape and adds it to the list:

R3DShape* R3DModel::AddShape()

{

R3DShape *newshape=new R3DShape(); // allocate the memory

m_ShapeList.AddTail(newshape); // add it to the list

return newshape; // so that the creating object can access it

}

// destructor deletes all the items in the list:

R3DModel::~R3DModel()

{

while(!m_Shapelist.IsEmpty())

delete m_ShapeList.RemoveHead();

}

// The all important serialize function!!!

void R3DModel::Serialize(CArchive &ar)

{

m_ShapeList.Serialize(ar);

}

That really is all there is to it. The single line will take care of serializing each and every item in the list. Every item's Serialize() member will be called, and it doesn't matter how far down the class hierarchy an object is because every object calls its base class's Serialize() function. At the top of the heirarchy lies CObject::Serialize() which takes care of saving and loading the class information. So we can now have as many different kinds of R3DShape as we like, and the R3DModel class doesn't need to know in advance what kinds of R3DShape there are going to be.

Further Reading

I highly recommend the Scribble tutorial in the online documentation. Most of what I've covered (except the polymorphism) is demonstrated in Developer Products\Visual C++\Visual C++ Tutorials\Scribble:MDI Drawing Application\Creating the Document.

I can be reached by email on [email protected]


[Home][MFC Programming]

Hosted by www.Geocities.ws

1