Professional Documents
Culture Documents
7 Serialization and Collection Classesssdfsdf
7 Serialization and Collection Classesssdfsdf
Introduction
The basic idea of serialization is that an object should be able to write its current state, usually indicated by the value of its member variables, to persistent storage. Later, the object can be recreated by reading, or deserializing, the objects state from the storage. Serialization handles all the details of object pointers and circular references to objects that are used when you serialize an object. A key point is that the object itself is responsible for reading and writing its own state. Serialization is an important concept in MFC programming because it is the basis for the frameworks ability to open and save document in document / view architecture. This chapter covers all the details of serialization and how to make a class serializable.
CObject
The basic serialization protocol and functionality are defined in the CObject class. By deriving your class from CObject (or from a class derived from CObject), you gain access to the serialization protocol and functionality of CObject. The Serialize member function, which is defined in the CObject class, is responsible for actually serializing the data necessary to capture an objects current state. The Serialize
Page 96
function has a CArchive argument (which will be covered soon ) that it uses to read and write the object data. The CArchive object has a member function, IsStoring, which indicates whether Serialize is storing (writing data) or loading (reading data). Using the results of IsStoring as a guide, you either insert your objects data in the CArchive object with the insertion operator (<<) or extract data with the extraction operator (>>). One of the most important feature of CObject is run-time class information that lets the developer determine the information about an object such as class name and parent at runtime. The code to support RTCI resides in the macros like
The class derived from CObject can call IsKindOf( ) function which takes an argument that is created by another macro , RUNTIME_CLASS. The CRuntimeClass contains a data member m_wSchema. Serialization uses the WORD to give a class version. If , serialization is not supported then m_wSchema is oxffff. You can specify your own Schema through the IMPLEMENT_SERIAL macro as the third argument. The CObjects IsSerializable() function makes sure if the user has specified the correct macros for Serialization by checking that the m_wSchema for the CRuntimeClass is not 0xffff. CObject :: IsSerializable( ) BOOL CObject :: IsSerializable( ) const { return ( GetRuntimeClass( ) -> m_wSchema ! = 0xffff) ; }
Does the MFC architecture with CObject as a root cause performance problems ? C++ has to index the class virtual table at runtime to determine the correct function to execute. CObject has five virtual functions , two of which are in the debug builds only , so they dont really affect the release- build performance. The largest overhead with using virtual function is the increased instance size from a virtual table. Thus, the trade-off in deriving from CObject is 3 extra virtual functions added to your classes virtual table in exchange for RTCI, dynamic creation, serialization and memory diagnostics.
CFile
CFile is the base class for Microsoft Foundation file classes. It directly provides unbuffered, binary disk input/output services, and it indirectly supports text files and memory files through its derived classes. CFile works in conjunction with the CArchive class to support serialization CFile family consists of the following : CStdioFile , CMemFile ,CFile derivative class called CSharedFile and CFile itself. CStdioFile : It is buffered , reading and writing non-binary ASCII files. CMemFile : It provides a base class for implementing shared memory through CFile
Page 97
interface. CSharedFile : Provides some memory sharing. The hierarchical relationship between this class and its derived classes allows your program to operate on all file objects through the polymorphic CFile interface. A memory file, for example, behaves like a disk file. Normally, a disk file is opened automatically on CFile construction and closed on destruction. When you create a CFile Object , you specify a filename and file open mode. E.g. CFile myfile(filename, CFile :: modeCreate|CFile :: modeWrite);
CArchive
CArchive does not have a base class. MFC uses an object of the CArchive class as an intermediary between the object to be serialized and the storage medium. This object is always associated with a CFile object, from which it obtains the necessary information for serialization, including the file name and whether the requested operation is a read or write. The object that performs a serialization operation can use the CArchive object without regard to the nature of the storage medium. CArchive object uses overloaded insertion ( <<) and extraction (>>) operators to perform writing and reading operations. There are two ways to create a CArchive object: 1) Implicit creation of a CArchive object via the framework 2) Explicit creation of a CArchive object
Page 98
Page 99
Serialization Macros
The DECLARE_SERIAL macro is required in the declaration of classes that will support serialization, as shown here: class CPerson : public CObject { DECLARE_SERIAL( CPerson ) // rest of declaration follows... };
The IMPLEMENT_SERIAL macro is used to define the various functions needed when you derive a serializable class from CObject. You use this macro in the implementation file (.CPP) for your class. The first two arguments to the macro are the name of the class and the name of its immediate base class. The third argument to this macro is a schema number. The schema number is essentially a version number for objects of the class. Use an integer greater than or equal to 0 for the schema number. The MFC serialization code checks the schema number when reading objects into memory. If the schema number of the object on disk does not match the schema number of the class in memory, the library will throw a CArchiveException, preventing your program from reading an incorrect version of the object. If you want your Serialize member function to be able to read multiple versions that is, files written with different versions of the application you can use the value VERSIONABLE_SCHEMA as an argument to the IMPLEMENT_SERIAL macro. The following example shows how to use IMPLEMENT_SERIAL for a class, CPerson, that is derived from CObject: IMPLEMENT_SERIAL( CPerson, CObject, 1 )
Page 100
CDocument :: OnSaveDocument() [ doccore.cpp] CArchive :: Carchive() CMyDocument :: Serialize() CArchive :: IsStoring() operator << ( CArchive& , CObject* ) CArchive :: WriteObject () CArchive :: WriteClass () CMyClass:: Serialize() CArchive :: IsStoring ( ) CArchive :: operator << ( WORD) CArchive :: operator << ( dWORD) operator << ( CArchive& , point ) CArchive :: Write ( ) CArchive :: Close( )
In first step CDocument :: OnSaveDocument( ) creates a CArchive object in store mode and attaches to a file already opened by the Common Dialog. Next OnSaveDocument( ) calls the documents Serialize( ) method and finally closes the archive. In steps 3 - 5 the CMyDocument :: OnSaveDocument( ) is called which checks if the archive is storing via IsStoring( ) and uses the insertion operator for writing , as shown in the fifth step. In steps 6- 8 the insertion operator calls the CArchive::WriteObject ( ) which calls the WriteClass(), then Serialize( ) for the CMyDocument object. From steps 9-13 the actual data is written into the output buffer. Finally the Archive is closed by CDocument :: OnSaveDocument( ) as shown above.
Page 101
CArchive :: CArchive ( ) CMyDocument :: Serialize ( ) CArchive :: IsStoring ( ) FALSE operator >> ( CArchive& , CMyClass* ) CArchive :: ReadObject ( ) CArchive :: Serialize ( ) CMyClass:: IsStoring ( ) CArchive :: operator >> ( WORD) CArchive :: operator >> (DWORD) operator >> ( CArchive& , point ) CArchive :: Read( ) CArchive :: Close( )
The framework unwinds what is wrote in the save operation when reading. The framework can make sure that the data is read in the order that is written.
Steps :
1) 2) 3) 4) 5) 6) Deriving your class from CObject (or from some class derived from CObject). Using the DECLARE_SERIAL macro in the class declaration. Overriding the Serialize member function. Defining a constructor that takes no arguments. Using the IMPLEMENT_SERIAL macro in the implementation file for your class. If you call Serialize directly rather than through the >> and << operators of CArchive, the last three steps are not required for serialization.
Page 102
class CPerson : public CObject { DECLARE_SERIAL (CPerson); CString m_fname; WORD m_lname; public: virtual void Serialize ( CArchive& ar) }; void CPerson ::Serialize ( CArchive& ar) { CObject::Serialize ( ar ); //call base class //function first if ( ar.IsStoring ( ) ){ ar << m_fname; ar<< m_lname; } else ar >> m_fname; ar >> m_lname; }
Collection Classes
MFC contains a number of ready-to-use lists, arrays, and maps that are referred to as collection classes. A collection is a very useful programming idiom for holding and processing groups of class objects or groups of standard types. A collection object appears as a single object. Class member functions can operate on all elements of the collection. MFC supplies two kinds of collection classes: Collection classes based on templates Collection classes not based on templates
The collection template classes are based on C++ templates, but the original collection classes released with MFC version 1.0 not based on templates are still available. Most collections can be archived or sent to a dump context. The Dump and Serialize member functions for CObject pointer collections call the corresponding functions for each of their elements. Some collections cannot be archived for example, pointer collections. When you program with the application framework, the collection classes will be especially useful for implementing data structures in your document class. The Microsoft Foundation Class Library provides collection classes to manage groups of objects. These classes are of two types: Collection classes created from C++ templates Collection classes not created from templates
Page 103
MFC predefines two categories of template-based collections: Simple array, list, and map classes Arrays, lists, and maps of typed pointers CArray, CList, CMap CTypedPtrArray,CTypedPtrList,
CTypedPtrMap
The simple collection classes are all derived from class CObject, so they inherit the serialization, dynamic creation, and other properties of CObject. The typed pointer collection classes require you to specify the class you derive from which must be one of the nontemplate pointer collections predefined by MFC, such as CPtrList or CPtrArray. Your new collection class inherits from the specified base class, and the new classs member functions use encapsulated calls to the base class members to enforce type safety.
The first example declares an array collection, myArray, which contains ints. The second example declares a list collection, myList, which stores CPerson objects. Certain member functions of the collection classes take arguments whose type is specified by the ARG_TYPE template parameter. For example, the Add member function of class CArray takes an ARG_TYPE argument: CArray<CPerson, CPerson&> myArray; CPerson person; myArray->Add( person );
Page 104
If the type of ARG_VALUE is a structure or class, the ARG_VALUE parameter is typically a reference to the type specified in VALUE. For example: CMap< int, int, MY_STRUCT, MY_STRUCT& > myMap1; CMap< CString, LPCSTR, CPerson, CPerson& > myMap2;
The first example stores MY_STRUCT values, accesses them by int keys, and returns accessed MY_STRUCT items by reference. The second example stores CPerson values, accesses them by CString keys, and returns references to accessed items. This example might represent a simple address book, in which you look up persons by last name. Because the KEY parameter is of type CString and the KEY_TYPE parameter is of type LPCSTR, the keys are stored in the map as items of type CString but are referenced in functions such as SetAt through pointers of type LPCSTR. For example: CMap< CString, LPCSTR, CPerson, CPerson& > myMap2; CPerson person; LPCSTR lpstrName = "Jones"; myMap2->SetAt( lpstrName, person );
The first example declares a typed-pointer array, myArray, derived from CObArray. The array stores and returns pointers to CPerson objects (where CPerson is a class derived from CObject). You can call any CObArray member function, or you can call the new type-safe GetAt and ElementAt functions or use the type-safe [ ] operator. The second example declares a typed-pointer list, myList, derived from CPtrList. The list stores and returns pointers to MY_STRUCT objects. A class based on CPtrList is used for storing pointers to objects not derived from CObject. CTypedPtrList has a number of typesafe member functions: GetHead, GetTail, RemoveHead, RemoveTail, GetNext, GetPrev, and GetAt.
Page 105
The typed-pointer map class, CTypedPtrMap, takes three parameters: BASE_CLASS, KEY, and VALUE. The BASE_CLASS parameter specifies the class from which to derive the new class: CMapPtrToWord, CMapPtrToPtr, CMapStringToPtr, CMapWordToPtr, CMapStringToOb, and so on. KEY is analogous to KEY in CMap: it specifies the type of the key used for lookups. VALUE is analogous to VALUE in CMap: it specifies the type of object stored in the map. For example: CTypedPtrMap<CMapPtrToPtr, CString, MY_STRUCT*> myPtrMap; CTypedPtrMap<CMapStringToOb, CString, CMyObject*> myObjectMap;
The first example is a map based on CMapPtrToPtr it uses CString keys mapped to pointers to MY_STRUCT. You can look up a stored pointer by calling a type-safe Lookup member function. You can use the [ ] operator to look up a stored pointer and add it if not found. And you can iterate the map using the type-safe GetNextAssoc function. You can also call other member functions of class CMapPtrToPtr. The second example is a map based on CMapStringToOb it uses string keys mapped to stored pointers to CMyObject objects. You can use the same type-safe members described in the previous paragraph, or you can call members of class CMapStringToOb.
Note
If you specify a class or struct type for the VALUE parameter, rather than a pointer or reference to the type, the class or structure must have a copy constructor. The Microsoft Foundation Class Library provides predefined type-safe collections based on C++ templates. Because they are templates, these classes provide type safety and ease of use without the type-casting and other extra work involved in using a nontemplate class for this purpose.
Serializing Elements
The CArray, CList, and CMap classes call SerializeElements to store collection elements to or read them from an archive. The default implementation of the SerializeElements helper function does a bitwise write from the objects to the archive, or a bitwise read from the archive to the objects, depending on whether the objects are being stored in or retrieved from the archive. Override SerializeElements if this action is not appropriate. If your collection stores objects derived from CObject and you use the IMPLEMENT_SERIAL macro in the implementation of the collection element class, you can take advantage of the serialization functionality built into CArchive and CObject CPerson : public CObject { . . . }; CArray< CPerson, CPerson& > personArray; void SerializeElements( CArchive& ar, CPerson* pNewPersons, int nCount ) { for ( int i = 0; i < nCount; i++, pNewPersons++ ) { // Serialize each CPerson object pNewPersons->Serialize( ar ); } }
The overloaded insertion operators for CArchive call CObject::Serialize (or an override of
Page 106
Page 107
These wrapper functions provide a type-safe way to add and retrieve CPerson objects from the derived list. You can see that for the GetHeadPerson function, you are simply encapsulating the type casting. You can also add new functionality by defining new functions that extend the capabilities of the collection rather than just wrapping existing functionality in type-safe wrappers. For example, the article Collections: Deleting All Objects in a CObject Collection describes a function to delete all the objects contained in a list. This function could be added to the derived class as a member function. The MFC array collection classes both template-based and not use indexes to access their elements. The MFC list and map collection classes both template-based and not use an indicator of type POSITION to describe a given position within the collection. To access one or more members of these collections, you first initialize the position indicator and then repeatedly pass that position to the collection and ask it to return the next element. The collection is not responsible for maintaining state information about the progress of the iteration. That information is kept in the position indicator. But, given a particular position, the collection is responsible for returning the next element.
Page 108
To iterate an array
Use sequential index numbers with the GetAt member function: CTypedPtrArray<CObArray, CPerson*> myArray; for( int i = 0; i < myArray.GetSize();i++ ) { CPerson* thePerson = myArray.GetAt( i ); ... }
This example uses a typed pointer array that contains pointers to CPerson objects. The array is derived from class CObArray, one of the nontemplate predefined classes. GetAt returns a pointer to a CPerson object. For typed pointer collection classes arrays or lists the first parameter specifies the base class; the second parameter specifies the type to store. The CTypedPtrArray class also overloads the [ ] operator so that you can use the customary array-subscript syntax to access elements of an array. An alternative to the statement in the body of the for loop above is CPerson* thePerson = myArray[ i ];
This operator exists in both const and non-const versions. The const version, which is invoked for const arrays, can appear only on the right side of an assignment statement.
To iterate a list
Use the member functions GetHeadPosition and GetNext to work your way through the list: CTypedPtrList<CObList, CPerson*> myList; POSITION pos = myList.GetHeadPosition(); while( pos != NULL ) { CPerson* thePerson = myList.GetNext( pos ); ... }
This example uses a typed pointer list to contain pointers to CPerson objects.
To iterate a map
Use GetStartPosition to get to the beginning of the map and GetNextAssoc to repeatedly get the next key and value from the map, as shown by the following example: CMap<CString, LPCTSTR, CPerson*, CPerson*> myMap; POSITION pos = myMap.GetStartPosition(); while( pos != NULL ) { CPerson* pPerson; CString string; // Get key ( string ) and value ( pPerson ) myMap.GetNextAssoc( pos, string, pPerson ); // Use string and pPerson }
Page 109
This example uses a simple map template (rather than a typed pointer collection) that uses CString keys and stores pointers to CPerson objects. When you use access functions such as GetNextAssoc, the class provides pointers to CPerson objects. If you use one of the nontemplate map collections instead, you must cast the returned CObject pointer to a pointer to a CPerson.
Note
For nontemplate maps, the compiler requires a reference to a CObject pointer in the last parameter to GetNextAssoc. On input, you must cast your pointers to that type, as shown in the next example. The template solution is cleaner and provides better type safety. The nontemplate code is more complicated, as you can see here: CMapStringToOb myMap; //A nontemplate collection class POSITION pos = myMap.GetStartPosition( ); while( pos != NULL ) { CPerson* pPerson; CString string; // Gets key (string) and value (pPerson) myMap.GetNextAssoc(pos,string,(CObject*&)pPerson); ASSERT(pPerson->IsKindOf(RUNTIME_CLASS(CPerson))); // Use string and pPerson }
The last function call, RemoveAll, is a list member function that removes all elements from the list. The member function RemoveAt removes a single element.
Page 110
Notice the difference between deleting an elements object and removing the element itself. Removing an element from the list merely removes the lists reference to the object. The object still exists in memory. When you delete an object, it ceases to exist and its memory is reclaimed. Thus, it is important to remove an element immediately after the elements object has been deleted so that the list wont try to access objects that no longer exist.
Like the list example above, you can call RemoveAll to remove all elements in an array or RemoveAt to remove an individual element.
You can call RemoveAll to remove all elements in a map or RemoveKey to remove an individual element with the specified key.
Page 111
Stacks
Because the standard list collection has both a head and a tail, it is easy to create a derived list collection that mimics the behavior of a last-in-first-out (LIFO) stack. A stack is like a stack of trays in a cafeteria. As trays are added to the stack, they go on top of the stack. The last tray added is the first to be removed. The list collection member functions AddHead and RemoveHead can be used to add and remove elements specifically from the head of the list; thus the most recently added element is the first to be removed.
Note that this approach exposes the underlying CObList class. The user can call any CObList member function, whether it makes sense for a stack or not.
Queues
Because the standard list collection has both a head and a tail, it is also easy to create a derived list collection that mimics the behavior of a first-in-first-out queue. A queue is like a line of people in a cafeteria. The first person in line is the first to be served. As more people come, they go to the end of the line to wait their turn. The list collection member functions AddTail and RemoveHead can be used to add and remove elements specifically from the head or tail of the list; thus the most recently added element is always the last to be removed.
Page 112
The following example shows how you can append member functions to add an element to the end of the queue and get the element from the front of the queue. class CPerson : public CObject { ... }; class CQueue:public CTypedPtrList< CObList, CPerson* > { public: // Go to the end of the line void AddToEnd( CPerson* newPerson ) { AddTail( newPerson ); } // End of the queue // Get first element in line CPerson* GetFromFront() { return IsEmpty() ? NULL : RemoveHead(); } };
Summary
Serialization is the process of writing or reading an object to or from a persistent storage medium, such as a disk file. MFC supplies built-in support for serialization in the class CObject. MFC uses an object of the CArchive class as an intermediary between the object to be serialized and the storage medium. A CArchive object uses overloaded insertion (<<) and extraction (>>) operators to perform writing and reading operations. MFC supplies two kinds of collection classes
MFC predefines two categories of template-based collections: Simple array, list, and map classes
Quiz
3) 4) 5) 6) 7) 8) What is serialization? Which are the classes involved in serialization? What are the five main steps to make a class serializable? What is the significance of schema number in serialization? How to iterate an collection class (CArray, CList, CMap)? How to delete object from collection class (CArray, CList, CMap)?