Visual C++ 6 Unleashed

Visual C++ 6 Unleashed

By MICKEY WILLIAMS and David Bennett

The CObject Class

At least as far as MFC is concerned, CObject is the mother of all classes (well, most of them anyway). Almost all the classes in MFC are derived from CObject—with a few notable exceptions, such as CString. Deriving a class from CObject provides several very important features, including serialization, runtime type information, and some very important debugging features.

Serialization

Many features of Windows programming with MFC require the capability of serializing the data in your objects. Perhaps the simplest example of this is saving an object to a file. You need to have a way to convert your object to a series of bytes that can be written to disk and brought back later to restore your object to its previous state.

To implement serialization in your classes, you first derive them, either directly or indirectly, from CObject. Then you can implement the Serialize member function for your class to serialize its data. To see just how to do this, let's start by looking at a few macros MFC provides to help.

The DECLARE_SERIAL and IMPLEMENT_SERIAL Macros

To help implement serialization in your class, MFC provides a pair of macros: DECLARE_SERIAL for use in your class declaration (usually in an h file), and IMPLEMENT_SERIAL for use in your class implementation (a cpp file).

The DECLARE_SERIAL macro takes only one parameter: the name of your class. Placing this in your class declaration provides prototypes for the serialization functions and some special handling for the insertion operator. You can see how this is used in the following class declaration:

Class CEmployee : public CObject
{
public:
    DECLARE_SERIAL(CEmployee)
    void Serialize(CArchive& ar);
private:
    int        m_EmpNo;
    CString    m_Name;
    float      m_Salary;
};
							

Serialize()

Notice that this example declares a Serialize() function, which takes as a parameter a reference to a CArchive object, which provides a context for the serialization. Before calling the Serialize() function, the MFC framework has prepared the CArchive object either to read from or write to objects of your class. You must implement the specific behavior for the Serialize() function in each class that you intend to serialize.

As mentioned previously, the same Serialize() function implements both loading and storing, based on the CArchive context. You can use the CArchive::IsLoading() or CArchive::IsStoring() function to determine the direction of serialization. The implementation for the CEmployee class declared earlier might look like this:

IMPLEMENT_SERIAL(CEmployee, CObject, 0x200)
void CEmployee::Serialize(CArchive& ar)
{
    // call base class Serialize() first
    CObject::Serialize(ar);
    // then serialize the data for this class
    if(ar.IsLoading())
    {
        ar >> m_EmpNo;
        ar >> m_Name;
        ar >> m_Salary;
    }
    else
    {
        ar << m_EmpNo;
        ar << m_Name;
        ar << m_Salary;
    }
}  // end CEmployee::Serialize

There are many interesting things that you should notice in this example, beginning with the use of the IMPLEMENT_SERIAL macro, which takes three parameters: the class name, the base class it is derived from, and a schema number, which you will learn about in just a bit.

Next, you should notice that you call the Serialize member of the base class. Every implementation of Serialize() must call the Serialize() function of the base class to allow it to serialize its data first, before you serialize the data for your class.

Serialization Operators

Notice that the serialization is performed by the overloaded insertion and extraction operators. These are predefined for the CArchive class for the following data types:

The insertion and extraction operators are also defined for any class that implements serialization. You can thank the DECLARE_SERIAL and IMPLEMENT_SERIAL macros for this. If you need to use any other data types, you have to create your own override functions or use macros or type casts to use the supported types.

Serializing Different Versions

Earlier, you learned that the IMPLEMENT_SERIAL macro takes a schema number for its third parameter. This can be any number in the valid range of type UINT, with the exception of -1, which is reserved for use by MFC. The schema number effectively allows you to embed a version number in your serialized data; if the schema number you specify in IMPLEMENT_SERIAL does not match the schema number in the file you are reading, MFC will fail when attempting to read the file. In debug builds, MFC will display a failed assertion dialog box.

If MFC just fails when presented with out-of-date file information, how can it support multiple versions? This is where the VERSIONABLE_SCHEMA macro comes in. If you combine your current schema number and the VERSIONABLE_SCHEMA macro by using the OR operator (|), your Serialize() routine will write your data with the current schema number, but can read any schema. This is handled by use of the CArchive::GetObjectSchema() function, as you will see in the following example. Here, you assume that the previous version of CEmployee did not implement the m_Name member:

IMPLEMENT_SERIAL(CEmployee, CObject, VERSIONABLE_SCHEMA|0x200)
void CEmployee::Serialize(CArchive& ar)
{
    // first, call base class Serialize function
    CObject::Serialize(ar);

    // Now we do our stuff
    if(ar.IsStoring())
    {
        // We are writing our class data,
        //     so we don't care about the schema
        ar << m_EmpNo;
        ar << m_Name;
        ar << m_Salary;
    }
    else
    {
        // we are loading, so check the schema
        UINT nSchema = ar.GetObjectSchema();
        switch(nSchema)
        {
            case 0x100:
                // Old schema, default m_Name
                ar >> m_EmpNo;
                m_Name = ""Dilbert";
                ar >> m_Salary;
                break;
            case 0x200:
                // current version
                ar >> m_EmpNo;
                ar >> m_Name;
                ar >> m_Salary;
                break;
            default:
                // Unknown Version, do nothing
                break;
        }  // end switch
    }  // end if
}  // end CCLient::Serialize()

As you can see, you should provide reasonable defaults for data that cannot be retrieved from the archive. On the other hand, you probably should provide some sort of mechanism to report unknown cases to the user, instead of doing nothing, as I did here.

Share ThisShare This

Informit Network