Implementing persistence presents special problems in development of object-oriented applications. A development team must consider how to preserve the structure and relationships of application objects when storing and retrieving persistent data. This consideration can be troublesome, since data in a file is typically stored as an unstructured binary stream. To address this problem, the MFC application framework implements serialization, which enables you to preserve your application data's object structure when saving it to, and restoring it from, a persistent archive.
After this lesson, you will be able to:Estimated lesson time: 50 minutes
- Understand how the MFC application framework implements serialization.
- Use the overloaded << and >> operators to serialize built-in types and MFC types to an archive.
- Make a class serializable.
- Serialize an MFC collection.
MFC provides built-in support for serialization through the CObject class. All classes that implement serialization must derive from CObject and must also provide an overload of the CObject::Serialize() function. The Serialize() function's task is to archive selected data members of the class, and save them to or restore them from an object of the MFC class CArchive.
A CArchive object acts as an intermediary between the object to be serialized and the storage medium. Also, a CArchive object is always associated with a CFile object. The CFile object usually represents a disk file, but it can also represent a memory file. For example, you could associate a CArchive object with a CSharedFile object to serialize data to and from the Windows Clipboard. Additionally, a CArchive object provides a type-safe buffering mechanism for reading and writing serializable objects to and from a CFile object.
A given CArchive object is used either to store data or to load data, but never both. The life of a CArchive object is limited to one pass, through either writing objects to a file or reading objects from a file. Separately created CArchive objects are required to serialize data to a file, and also to restore data back from a file. The status of a CArchive object, whether used for storing or loading, can be determined by querying the Boolean return value of the CArchive::IsStoring() function.
The CArchive class defines both the insertion operator (<<) and the extraction operator (>>). These operators are used in a manner similar to the insertion and extraction operators defined for the standard C++ stream classes, as illustrated by the following code:
if (ar.IsStoring()) { ar << m_string; } else { ar >> m_string; } |
You can use the insertion and extraction operators to store data to, and retrieve data from, a CArchive object. Table 6.2 lists the data types and objects that can be used with the insertion and extraction operators.
Table 6.2 Data Types and Objects Used with Insertion and Extraction Operators
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 |
In Lesson 4 of Chapter 3, you learned that the application data is stored in the application's document object. Application data is serialized to a document file on disk, then restored from the document file into the document object. A document file type is associated with an application by specifying a filename extension in the Advanced Options dialog box, in Step 4 of the AppWizard (see Lesson 1 of Chapter 2).
The document object begins application data serialization in response to file commands selected by the user. A CArchive object of the appropriate type (according to whether data is to be saved to or restored from the archive) is created by the framework, and passed as a parameter to the document object's Serialize() function.
The AppWizard creates a stub Serialize() function for your document class. You must add code to this function to store or retrieve persistent data members to or from the archive. You can store and retrieve simple data members using the << and >> operators. If the document object contains more complex objects that implement their own serialization code, you must call the Serialize() function for those objects, and forward a reference to the current archive.
As an example, consider an application TestApp that maintains a document class with three data members, as shown by the following code sample:
Class CtestAppDoc { CString m_string; DWORD m_dwVar; MyObj m_obj; } |
Assume that the MyObj class is a serializable class.
The following code illustrates a Serialize() function that might be written for the TestApp document class:
void CTestAppDoc::Serialize(CArchive& ar) { if (ar.IsStoring()) { ar << m_string; ar << m_dwVar; } else { ar >> m_string; ar >> m_dwVar; } m_obj.Serialize(ar); } |
Note how the MyObj::Serialize() function is called outside of the conditional branching code, as it contains its own branch condition to determine whether data is being stored or retrieved. Figure 6.1 illustrates how you can apply this technique to serialize objects recursively.
Figure 6.1 Serializing contained objects
As long as your serialization routines are kept consistent, complex object structures can be saved to disk and restored to an application. You need to ensure that the storing and restoring branches of your Serialize() functions match—in other words, that they store and restore the same objects in the same order.
The serialization routines handle proper reconstruction of an object structure when this structure is restored from a disk file. Serialization accomplishes this by writing information about the object's type as well as its state (data member values) into the disk file. When an object is restored, this information is used to determine what type of object needs to be created to receive the data. The serialization routine automatically performs the object creation. However, to ensure that this action works properly, you must provide a default constructor (one with no arguments) for your serializable class.
In the following practice exercise, you will learn the steps required to implement simple serialization of application data.
In Lesson 1 of Chapter 5 you added two member variables, CMyAppDoc::m_nLines and CMyAppDoc::m_string, as application data for the MyApp application. You will now add code to this project to serialize these data items to a document file.
void CMyApp::Serialize(CArchive& ar) { if (ar.IsStoring()) { // TODO: add storing code here } else { // TODO: add loading code here } } |
void CMyAppDoc::Serialize(CArchive& ar) { if (ar.IsStoring()) { ar << m_nLines; ar << m_string; } else { ar >> m_nLines; ar >> m_string; } } |
SetModifiedFlag(); |
Thus, the entire function looks as follows:
void CMyAppDoc::OnDataEdit() { CEditDataDialog aDlg; aDlg.m_nLines = m_nLines; aDlg.m_strLineText = m_string; if(aDlg.DoModal()) { m_nLines = aDlg.m_nLines; m_string = aDlg.m_strLineText; UpdateAllViews(NULL); SetModifiedFlag(); } } |
The CDocument::SetModifiedFlag() function is called to notify the application framework that the application data has been modified. This function causes the framework to prompt the user to save changes before closing a document.
m_nLines = 0; m_string = ""; |
Thus, the entire function looks as follows:
void CMyAppDoc::DeleteContents() { m_nLines = 0; m_string = ""; CDocument::DeleteContents(); } |
Unlike multiple-document interface (MDI) applications, which create a new document object each time a new document is created or an existing document file is opened, single-document interface (SDI) applications create only one document object, which is reused each time a document is created or opened. The DeleteContents() function clears the application data held in the document object before the object is reused. When developing an SDI application, you must implement a DeleteContents() function that sets all document object data members to zero or null values. Otherwise, you will find data from previous editing sessions in your current document.
You have already seen how a document object can contain objects that implement their own serialization code. To create a serializable class, perform the following steps:
The following code illustrates a serializable class declaration:
// MyClass.h class CMyClass : public CObject { DECLARE_SERIAL(CMyClass) public: CMyClass() {;} // Default constructor virtual void Serialize(CArchive& ar); }; |
IMPLEMENT_SERIAL(CMyClass, CObject, 1) |
The schema number allows you to implement a versioning system for your document files. You will likely change your application documents' object structure between releases. These changes will most likely cause errors for a user that tries to use a new version of your application to open a document created with an older version.
You can assign a different schema number to an object's IMPLEMENT_ SERIAL macro for each release that changes the object's structure. This action allows you to add code that detects discrepancies between application and document versions, and takes appropriate actions, such as displaying an error message or running a document format conversion routine.
MFC's templated collection classes CArray, CList and CMap, implement their own versions of the Serialize() function that serialize all of the elements in the collection.
Suppose your document class contains a collection of integer values as shown in the following code sample:
CList<int, int &> m_intList; |
This collection can be serialized by adding the following line to the document's Serialize() function:
m_intList.Serialize(ar); |
This line of code is all that is required to serialize a collection of simple types. CList::Serialize() calls the global helper function template SerializeElements(), which has the following signature:
template<class TYPE> void AFXAPI SerializeElements(CArchive& ar, TYPE* pElements, int nCount); |
The compiler generates an appropriate instantiation of this template for the collection class element type. The SerializeElements() function's default behavior is to perform a bitwise copy of the data contained in the collection (referenced by the pointer pElements) to or from the archive.
This default behavior is fine for simple objects, but is problematic for more complicated object structures. Suppose that your document class contains the member as shown in the following example code:
CList<CMyClass, CMyClass &> m_objList; |
CMyClass would be defined as follows:
class CmyClass { DECLARE_SERIAL(CMyClass) public: CMyClass() {;} int m_int; DWORD m_dw; CString m_string; virtual void Serialize(CArchive& ar); } |
Attempting to serialize the m_objList collection by adding the following line to the document object's Serialize() function will cause errors:
m_objList.Serialize(ar); |
Such errors will result because the CMyClass objects contain CStrings; which are complex objects that use custom memory allocation and reference-counting techniques.
The default SerializeElements() function generated for the m_objlist collection will attempt to read or write a bitwise copy of the collection elements to or from the archive, therefore bypassing the custom serialization routines built in the << and >> operators defined for the CString class.
In this case, you must write your own version of the SerializeElements() function. Assuming that CMyClass has been properly constructed as a serializable class, the corresponding SerializeElements() function might look as follows:
template <> void AFXAPI SerializeElements <CMyClass> (CArchive& ar, CMyClass * pNewMC, int nCount) { for (int i = 0; i < nCount; i++, pNewMC++) { // Serialize each CMyClass object pNewMC->Serialize(ar); } } |
NOTE
You do not have to provide your own version of SerializeElements() for a simple collection of CString objects, as MFC provides one as part of the CArchive source code.
The MFC application framework implements a technology called serialization that enables you to preserve your application data's object structure when saving it to, and restoring it from, a persistent archive. All classes that implement serialization must be derived from the CObject class, and additionally must overload the CObject::Serialize() function.
The Serialize() function stores and retrieves persistent data members to and from a CArchive class object. This object acts as an intermediary between the object to be serialized and the storage medium, which is usually a disk file encapsulated as a CFile class object. In addition, a CArchive object provides a type-safe buffering mechanism for writing and reading serializable objects to or from a CFile object. Separate CArchive objects must be used for storing and retrieving data. Once the archive object has been created, its role (determined by whether the CArchive::IsStoring() function returns TRUE or FALSE) cannot be changed.
The document object begins application data serialization in response to user commands to load or save files. A CArchive object is created by the framework, and passed as a parameter to the document object's Serialize() function.
The AppWizard creates a stub Serialize() function for your document class. You must add code to this function to store or retrieve persistent data members to, or from, the archive. The CArchive class defines the << insertion operator and the >> extraction operator, which can be used to store or retrieve various C++ and MFC data types. If an object contains other serializable objects, you call the Serialize() function for these objects. To make a class serializable, you must:
To serialize an instance of an MFC collection template class, simply call the collection object's serialize function. It is important to be aware that the collection template classes implement serialization by calling the instantiation of the SerializeElements() function template that is generated for the element type of the collection class. The default behavior of the SerializeElements() function is to perform a bitwise copy of the data contained in the collection to or from the archive. If this behavior is not appropriate for the element type of your collection, you should provide your own implementation of the SerializeElements() function template.