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



Comments

  • unexpected end of file??

    Posted by Legacy on 09/03/2003 12:00am

    Originally posted by: mamtha

    I need to update the file-format for my MFC app since the next format was incompatble with the old one.
    it gives unexpected file format exception.
    how do i overcome this?

    Reply
  • Serialization in application in dialog based application?

    Posted by Legacy on 06/26/1999 12:00am

    Originally posted by: Vitaly A.Kaluzhny

    How can I manage to serialize object lists in dialog
    based application (InstallShiled like Wizard)?

    Reply
Leave a Comment
  • Your email address will not be published. All fields are required.

Top White Papers and Webcasts

  • On-demand Event Event Date: September 10, 2014 Modern mobile applications connect systems-of-engagement (mobile apps) with systems-of-record (traditional IT) to deliver new and innovative business value. But the lifecycle for development of mobile apps is also new and different. Emerging trends in mobile development call for faster delivery of incremental features, coupled with feedback from the users of the app "in the wild." This loop of continuous delivery and continuous feedback is how the best mobile …

  • Packaged application development teams frequently operate with limited testing environments due to time and labor constraints. By virtualizing the entire application stack, packaged application development teams can deliver business results faster, at higher quality, and with lower risk.

Most Popular Programming Stories

More for Developers

Latest Developer Headlines

RSS Feeds