Visual C++ 6 Unleashed

Visual C++ 6 Unleashed

By Mickey Williams and David Bennett

Understanding Serialization in MFC

Serialization is the way that classes derived from CDocument store and retrieve data from an archive, which is usually associated with an instance of CFile. Serialization is the process of storing the state of an object for the purpose of loading it at another time. The property of an object to be stored and loaded is sometimes called persistence, which also is defined as the capability of an object to remember its state between executions. Figure 9.2 shows the interaction between a serialized object and an archive.

09fig02.gif

Figure 9.2 Serializing an object to and from an archive.

When an object is serialized, information about the type of object is written to storage, along with information and data about the object. When an object is deserialized, the same process happens in reverse, and the object is loaded and created from the input stream.

Using serialization to store objects is much more flexible than writing specialized functions that store data in a fixed format. Objects that are persistent are capable of storing themselves instead of relying on an external function to read and write them to disk. This capability makes a persistent object much easier to reuse, because the object is more self-contained.

Persistent objects also help you easily write programs that are saved to storage. An object that is serialized might be made up of many smaller objects that also are serialized. Because individual objects often are stored in a collection, serializing the collection also serializes all objects contained in the collection.

Using the Insertion and Extraction Operators

The MFC class library overloads the insertion and extraction operators for many commonly used classes and basic types. You often use the insertion operator << to serialize or store data to the CArchive object. You use the extraction operator >> to deserialize or load data from a CArchive object.

These operators are defined for all basic C++ types, as well as a few commonly used classes not derived from CObject, such as the CString, CRect, and CTime classes. The insertion and extraction operators return a reference to a CArchive object, enabling them to be chained together in the following way:

archive << m_nFoo << m_rcClient << m_szName;

When used with classes that are derived from CObject, the insertion and extraction operators allocate the memory storage required to contain an object and then call the object's Serialize member function. If you do not need to allocate storage, you should call the Serialize member function directly.

As a rule of thumb, if you know the type of the object when it is deserialized, call the Serialize function directly. In addition, you must always call Serialize exclusively. If you use Serialize to load or store an object, you must not use the insertion and extraction operators at any other time with that object.

Using the Serialization Macros

You must use two macros when creating a persistent class based on CObject. Use the DECLARE_SERIAL macro in the class declaration file and the IMPLEMENT_SERIAL macro in the class implementation file.

Declaring a Persistent Class

The DECLARE_SERIAL macro takes a single parameter: the name of the class to be serialized. A good place to put this macro is on the first line of the class declaration, where it serves as a reminder that the class can be serialized. Listing 9.4 provides an example of a class that can be serialized. Save this source code in a file named Users.h.

Example 9.4. The CUser Class Declaration

#pragma once
class CUser : public CObject
{
    DECLARE_SERIAL(CUser);
public:
    // Constructors
    CUser();
    CUser(const CString& strName, const CString& strAddr);
    // Attributes
    void Set(const CString& strName, const CString& strAddr);
    CString GetName() const;
    CString GetAddr() const;
    // Operations
    virtual void Serialize(CArchive& ar);
    // Implementation
private:
    // The user's name
    CString m_strName;
    // The user's e-mail address
    CString m_strAddr;
};








							

Defining a Persistent Class

The IMPLEMENT_SERIAL macro takes three parameters and usually is placed before any member functions are defined for a persistent class. The parameters for IMPLEMENT_ SERIAL follow:

  • The class to be serialized
  • The immediate base class of the class being serialized
  • The schema or version number

The schema number is a version number for the class layout used when you're serializing and deserializing objects. If the schema number of the data being loaded does not match the schema number of the object reading the file, the program throws an exception. The schema number should be incremented when changes are made that affect serialization, such as adding a class member or changing the serialization order.

Listing 9.5 provides the member functions for the CUser class, including the IMPLEMENT_SERIAL macro. Save this source code in a file named Users.cpp.

Example 9.5. The CUser Member Functions

#include "stdafx.h"
#include "Users.h"

IMPLEMENT_SERIAL(CUser, CObject, 1);
CUser::CUser() { }
CUser::CUser(const CString& strName, const CString& strAddr)
{
    Set(strName, strAddr);
}
void CUser::Set(const CString& strName, const CString& strAddr)
{
    m_strName = strName;
    m_strAddr = strAddr;
}
CString CUser::GetName() const
{
    return m_strName;
}
CString CUser::GetAddr() const
{
    return m_strAddr;

}








							

Overriding the Serialize Function

Every persistent class must implement a Serialize member function, which is called in order to serialize or deserialize an object. The single parameter for Serialize is the CArchive object used for loading or storing the object. Listing 9.6 shows the version of Serialize used by the CUser class; add this function to the Users.cpp source file.

Example 9.6. The CUser::Serialize Member Function

void CUser::Serialize(CArchive& ar)
{
    if(ar.IsLoading())
    {
        ar >> m_strName >> m_strAddr;
    }
    else
    {
        ar << m_strName << m_strAddr;
    }
}



						

Creating a Serialized Collection

You can serialize most MFC collection classes, enabling large amounts of information to be stored and retrieved easily. You can serialize a CArray collection by calling its Serialize member function, for example. As with the other MFC template-based collection classes, you cannot use the insertion and extraction operators with CArray.

By default, the template-based collection classes perform a bitwise write when serializing a collection and a bitwise read when deserializing an archive. This means that the data stored in the collection is written bit by bit to the archive.

Bitwise serialization is a problem when you use collections to store pointers to objects. The Customers project uses the CArray class to store a collection of CUser objects, for example. The declaration of the CArray member follows:

CArray<CUser*, CUser*&>    m_setOfUsers;

Because the m_setOfUsers collection stores CUser pointers, storing the collection using a bitwise write would store only the current addresses of the contained objects. This information would become useless when the archive is deserialized.

Most of the time, you need to implement a helper function to assist in serializing a template-based collection. Helper functions do not belong to a class; they are global functions that are overloaded based on the function signature. The helper function used when serializing a template is SerializeEle ments. Figure 9.3 shows how you call the SerializeElements function to help serialize items stored in a collection.

09fig03.gif

Figure 9.3 The SerializeElements helper function.

Listing 9.7 provides a version of SerializeElements used with collections of CUser objects. This function is used later in the chapter, in the section, "Modifying the Document Class."

Example 9.7. The SerializeElements Function

void AFXAPI SerializeElements( CArchive&      ar,
                               CUser**        pUser,
                               int            nCount )
{
    for( int i = 0; i < nCount; i++, pUser++ )
    {
        if( ar.IsStoring() )
        {
            (*pUser)->Serialize(ar);
        }
        else
        {
            CUser* pNewUser = new CUser;
            pNewUser->Serialize(ar);
            *pUser = pNewUser;
        }
    }
}


						

The SerializeOElements function has three parameters:

  • A pointer to a CArchive object, as with Serialize.
  • The address of an object stored in the collection. In this example, pointers to CUser are stored in a CArray, so the parameter is a pointer to a CUser pointer.
  • The number of elements to be serialized.

In Listing 9.7, when you're serializing objects to the archive, each CUser object is written individually to the archive. When you're deserializing objects, a new CUser object is created, and that object is deserialized from the archive. The collection stores a pointer to the new object.

+ Share This