Serializable Polymorphic Lists

This tutorial 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
unnecessary 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 method for R3DSpecialCube might look like:

  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 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 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:


  R3DShape* R3DModel::AddShape()
  //
  // This creates a new R3DShape and adds it to the list:
  //
  {
    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
  }

  R3DModel::~R3DModel()
  //
  // Destructor deletes all the items in the list:
  //
  {
    while (!m_Shapelist.IsEmpty())
          delete m_ShapeList.RemoveHead();
  }

  void R3DModel::Serialize (CArchive &ar)
  //
  // The all important serialize function!!!
  //
  {
    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

More by Author

Get the Free Newsletter!

Subscribe to Developer Insider for top news, trends & analysis

Must Read