Serialization: Serializing an Object

HomeOverviewHow Do ITutorial

The article Serialization: Making a Serializable Class shows how to make a class serializable. Once you have a serializable class, you can serialize objects of that class to and from a file via a CArchive object. This article explains:

You can let the framework create the archive for your serializable document or explicitly create the CArchive object yourself. You can transfer data between a file and your serializable object by using the << and >> operators for CArchive or, in some cases, by calling the Serialize function of a CObject-derived class.

What Is a CArchive Object

A CArchive object provides a type-safe buffering mechanism for writing or reading serializable objects to or from a CFile object. Usually the CFile object represents a disk file; however, it can also be a memory file (CSharedFile object), perhaps representing the Clipboard.

A given CArchive object either stores (writes, serializes) data or loads (reads, deserializes) data, but never both. The life of a CArchive object is limited to one pass through writing objects to a file or reading objects from a file. Thus, two successively created CArchive objects are required to serialize data to a file and then deserialize it back from the file.

When an archive stores objects to a file, the archive attaches the CRuntimeClass name to the objects. Then, when another archive loads objects from a file to memory, the CObject-derived objects are dynamically reconstructed based on the CRuntimeClass of the objects. A given object may be referenced more than once as it is written to the file by the storing archive. The loading archive, however, will reconstruct the object only once. The details about how an archive attaches CRuntimeClass information to objects and reconstructs objects, taking into account possible multiple references, are described in Technical Note 2.

As data is serialized to an archive, the archive accumulates the data until its buffer is full. Then the archive writes its buffer to the CFile object pointed to by the CArchive object. Similarly, as you read data from an archive, it reads data from the file to its buffer and then from the buffer to your deserialized object. This buffering reduces the number of times a hard disk is physically read, thus improving your application’s performance.

Two Ways to Create a CArchive Object

There are two ways to create a CArchive object:

Implicit Creation of a CArchive Object via the Framework

The most common, and easiest, way is to let the framework create a CArchive object for your document on behalf of the Save, Save As, and Open commands on the File menu.

Here is what the framework does when the user of your application issues the Save As command from the File menu:

  1. Presents the Save As dialog box and gets the filename from the user.

  2. Opens the file named by the user as a CFile object.

  3. Creates a CArchive object that points to this CFile object. In creating the CArchive object, the framework sets the mode to “store” (write, serialize), as opposed to “load” (read, deserialize).

  4. Calls the Serialize function defined in your CDocument-derived class, passing it a reference to the CArchive object.

Your document’s Serialize function then writes data to the CArchive object, as explained shortly. Upon return from your Serialize function, the framework destroys the CArchive object and then the CFile object.

Thus, if you let the framework create the CArchive object for your document, all you have to do is implement the document’s Serialize function that writes and reads to and from the archive. You also have to implement Serialize for any CObject-derived objects that the document’s Serialize function in turn serializes directly or indirectly.

Explicit Creation of a CArchive Object

Besides serializing a document via the framework, there are other occasions when you may need a CArchive object. For example, you might want to serialize data to and from the Clipboard, represented by a CSharedFile object. Or, you may want to use a user interface for saving a file that is different from the one offered by the framework. In this case, you can explicitly create a CArchive object. You do this the same way the framework does, using the following procedure.

To explicitly create a CArchive object

  1. Construct a CFile object or an object derived from CFile.

  2. Pass the CFile object to the constructor for CArchive, as shown in the following example:
    CFile theFile;
    theFile.Open(..., CFile::modeWrite);
    CArchive archive(&theFile, CArchive::store);
    

    The second argument to the CArchive constructor is an enumerated value that specifies whether the archive will be used for storing or loading data to or from the file. The Serialize function of an object checks this state by calling the IsStoring function for the archive object.

When you are finished storing or loading data to or from the CArchive object, close it. Although the CArchive (and CFile) objects will automatically close the archive (and file), it is good practice to explicitly do so since it makes recovery from errors easier. For more information about error handling, see the article Exceptions: Catching and Deleting Exceptions.

To close the CArchive object

Using the CArchive << and >> Operators

CArchive provides << and >> operators for writing and reading simple data types as well as CObjects to and from a file.

To store an object in a file via an archive

To load an object from a value previously stored in a file

Usually, you store and load data to and from a file via an archive in the Serialize functions of CObject-derived classes, which you must have declared with the DECLARE_SERIALIZE macro. A reference to a CArchive object is passed to your Serialize function. You call the IsLoading function of the CArchive object to determine whether the Serialize function has been called to load data from the file or store data to the file.

The Serialize function of a serializable CObject-derived class typically has the following form:

void CPerson::Serialize(CArchive& ar)
{
    CObject::Serialize(ar);
    if (ar.IsStoring())
    {
        // TODO:  add storing code here
    }
    else
    {
    // TODO:  add loading code here
    }
}

The above code template is exactly the same as the one AppWizard creates for the Serialize function of the document (a class derived from CDocument). This code template helps you write code that is easier to review, because the storing code and the loading code should always be parallel, as in the following example:

void CPerson:Serialize(CArchive& ar)
{
    if (ar.IsStoring())
    {
        ar << m_strName;
        ar << m_wAge;
    }
    else
    {
        ar >> m_strName;
        ar >> m_wAge;
    }
}

The library defines << and >> operators for CArchive as the first operand and the following data types and class types as the second operand:

CObject* SIZE and CSize float
WORD CString POINT and CPoint
DWORD BYTE RECT and CRect
double LONG CTime and CTimeSpan
int COleCurrency COleVariant
COleDateTime COleDateTimeSpan  

Note   Storing and loading CObjects via an archive requires extra consideration. For more information see Storing and Loading CObjects via an Archive below.

The CArchive << and >> operators always return a reference to the CArchive object, which is the first operand. This enables you to chain the operators, as illustrated below:

BYTE bSomeByte;
WORD wSomeWord;
DWORD wSomeDoubleWord;
...
ar << bSomeByte << wSomeWord << wSomeDoubleWord;

Storing and Loading CObjects via an Archive

Storing and loading CObjects via an archive requires extra consideration. In certain cases, you should call the Serialize function of the object, where the CArchive object is a parameter of the Serialize call, as opposed to using the << or >> operator of the CArchive. The important fact to keep in mind is that the CArchive >> operator constructs the CObject in memory based on CRuntimeClass information previously written to the file by the storing archive.

Therefore, whether you use the CArchive << and >> operators, versus calling Serialize, depends on whether you need the loading archive to dynamically reconstruct the object based on previously stored CRuntimeClass information. Use the Serialize function in the following cases:

Caution   If you load the object using the Serialize function, you must also store the object using the Serialize function. Don’t store using the CArchive << operator and then load using the Serialize function, or store using the Serialize function and then load using CArchive >> operator.

The following example illustrates the cases:

class CMyObject : public CObject
{
// ...Member functions
   public:
   CMyObject() { }
   virtual void Serialize( CArchive& ar ) { }

// Implementation
   protected:
   DECLARE_SERIAL( CMyObject )
};


class COtherObject : public CObject
{
   // ...Member functions
   public:
   COtherObject() { }
   virtual void Serialize( CArchive& ar ) { }

// Implementation
protected:
   DECLARE_SERIAL( COtherObject )
};


class CCompoundObject : public CObject
{
   // ...Member functions
   public:
   CCompoundObject();
   virtual void Serialize( CArchive& ar );

// Implementation
protected:
   CMyObject m_myob;    // Embedded object
   COtherObject* m_pOther;    // Object allocated in constructor
   CObject* m_pObDyn;    // Dynamically allocated object
   //..Other member data and implementation

   DECLARE_SERIAL( CCompoundObject )
};

IMPLEMENT_SERIAL(CMyObject,CObject,1)
IMPLEMENT_SERIAL(COtherObject,CObject,1)
IMPLEMENT_SERIAL(CCompoundObject,CObject,1)


CCompoundObject::CCompoundObject()
{
   m_pOther = new COtherObject; // Exact type known and object already 
            //allocated.
   m_pObDyn = NULL;    // Will be allocated in another member function
            // if needed, could be a derived class object.
}

void CCompoundObject::Serialize( CArchive& ar )
{
   CObject::Serialize( ar );    // Always call base class Serialize.
   m_myob.Serialize( ar );    // Call Serialize on embedded member.
   m_pOther->Serialize( ar );    // Call Serialize on objects of known exact type.

   // Serialize dynamic members and other raw data
   if ( ar.IsStoring() )
   {
      ar << m_pObDyn;
      // Store other members
   }
   else
   {
      ar >> m_pObDyn; // Polymorphic reconstruction of persistent 
            // object 
            //load other members
   }
}

In summary, if your serializable class defines an embedded CObject as a member, you should not use the CArchive << and >> operators for that object, but should call the Serialize function instead. Also, if your serializable class defines a pointer to a CObject (or an object derived from CObject) as a member, but constructs this other object in its own constructor, you should also call Serialize.