http://www.codeproject.com/Articles/1115/The-Right-Way-To-Do-Object-Serialization
Introduction
Object serialization is one of the most powerful features of MFC. With it you can store your objects into a file and let MFC recreate your objects from that file. Unfortunately there are some pitfalls in using it correctly. So it happened that yesterday I spent a lot of time in understanding what I did wrong while programming a routine that wrapped up some objects for a drag and drop operation. After a long debugging session I understood what happened and so I think that it might be a good idea to share my newly acquired knowledge with you.
The Wrong Way
Let's start with my wrong implementation of object serialization. A very convenient way to implement drag and drop and clipboard operations is to wrap up your objects in a COleDataSource
object. This object can hold any data with a defined format or with a format that you can define and then it can be passed either to the clipboard by callingCOleDataSource::SetClipboard
or to the drag and drop handler by calling COleDataSource::DoDragDrop
.
If you want to transfer a set of known objects between different parts of your applications, it is convenient to serialize them in a memory file and to wrap them up in a COleDataSource
.
Let's start: This is my object:
class CLeopoldoOb : public CObject { DECLARE_SERIAL(CLeopoldoOb) // construction/destruction public: CLeopoldoOb(); // .... protected: // some data.... // some methods... };
And this is my first version of the routine that wrapped up a set of those objects in the COleDataSource
:
... COleDataSource* pDataSource = new COleDataSource; if ( !pDataSource ) { return NULL; } // this is the memory file... CSharedFile sf (GMEM_MOVEABLE|GMEM_DDESHARE|GMEM_ZEROINIT); // this archive works on the memory file CArchive ar (&sf, CArchive::store); // serialize the quantity ar << m_nObjectCount; for ( int i = 0; i < m_nObjectCount; i++ ) { CLeopoldoOb leo; // now prepare the contents of the leopoldo object ... // object contains all data we need // serialize the object ar << &leo } ar.Flush (); ar.Close (); pDataSource->CacheGlobalData (g_cfLeopoldo, sf.Detach ()); pDataSource->DoDragDrop (); if ( pDataSource->m_dwRef <= 1 ) { delete pDataSource; } else { pDataSource->ExternalRelease (); // !!! } ...
And let's see the routine that unwraps the objects when they are dropped:
... BOOL CMainFrame::OnDrop (COleDataObject* pDataObject, DROPEFFECT dropEffect, CPoint point) { if ( pDataObject->IsDataAvailable (g_cfLeopoldo) ) { CLeopoldoOb *pOb; HGLOBAL hMem = pDataObject->GetGlobalData (g_cfLeopoldo); CMemFile mf; UINT nCount; mf.Attach ((BYTE *)::GlobalLock (hMem), ::GlobalSize (hMem)); CArchive ar (&mf, CArchive::load); // get the object count ar >> nCount; for ( UINT n = 0; n < nCount; n++ ) { try { // create the object out of the archive ar >> pOb; // now do what you have to do with the object .... // we do not need it any more... delete pOb; } catch { CException *pEx ) { // do some error handling pEx->Delete (); } } ar.Close (); mf.Detach (); ::GlobalUnlock (hMem); ::GlobalFree (hMem); return nCount > 0; } return FALSE; }
What happened? Apparently this stuff worked, if you had only one object wrapped up. OK. Sometimes the application crashed but basically it worked. If instead more than one object was wrapped up, two strange things happened:
- Only the first object was unwrapped
- A
CArchiveException
withbadIndex
sometimes occurred
When tested under the debugger, I saw that the creation of the object from the serialization failed starting from the second time. Since there was definitively no error in the load routine, I reached the conclusion that the archive data must be messed up.
What Went Wrong?
After debugging in the profundities of CArchive
I discovered that the process of storing dynamic objects is not as simple as I imagined it should be.
Obviously my error was during the creation of the archive. There are two important rules:
- The objects must be dynamically created (with
CRuntimeClass::CreateObject
). - The objects must remain valid until the serialization has been finished.
If you are asking why, take a look into the source of CArchive
and you will see that the archive stores additional information about those objects during its life. Furthermore the entire MFC seems to have knowledge about all dynamically created CObject
s.
The Right Way
First of all we need a little helper class that will make life simpler:
class CAllocatedObArray : public CObArray { public: CAllocatedObArray () { } virtual ~CAllocatedObArray () { RemoveAll (); } public: void RemoveAll (); }; void CAllocatedObArray::RemoveAll () { for ( int i = 0; i < GetSize (); i++ ) { CObject *pObject = GetAt (i); if ( pObject ) { delete pObject; } } }
The following is the modified wrapper routine. You will see that all objects are created dynamically and stored into this array. After the serialization has finished, the array goes out of scope and our allocated objects will be automatically be destroyed.
... COleDataSource* pDataSource = new COleDataSource; if ( !pDataSource ) { return NULL; } // this is the memory file... CSharedFile sf (GMEM_MOVEABLE|GMEM_DDESHARE|GMEM_ZEROINIT); // this archive works on the memory file CArchive ar (&sf, CArchive::store); // this array holds our objects CAllocatedObArray tmpArray; // This is needed to access the runtime class CLeopoldoOb leo; // serialize the quantity ar << m_nObjectCount; for ( int i = 0; i < m_nObjectCount; i++ ) { CLeopoldoOb *pLeo = (CLeopoldoOb *) leo.GetRuntimeClass ()->CreateObject (); // store it in the array tmpArr.Add (pLeo); // now prepare the contents of the leopoldo object ... // object contains all data we need // serialize the object ar << pLeo } ar.Flush (); ar.Close (); pDataSource->CacheGlobalData (g_cfLeopoldo, sf.Detach ()); pDataSource->DoDragDrop (); if ( pDataSource->m_dwRef <= 1 ) { delete pDataSource; } else { pDataSource->ExternalRelease (); // !!! } ...
This works. Probably this will be nothing new for all experts among you, but since I did make this error after years of programming MFC, I hope that this article will help some beginner.