29.3 Serialize , operator , and operator

29.3 Serialize , operator<< , and operator>>

Rather than working directly with files in MFC, we work with CArchive objects, which are friendly wrappers that have files buried down inside them. The general process of writing from or reading into a class such as CPopDoc or cCritter gets encapsulated into a method known as Serialize . Serialize is a polymorphic method that's declared way up inside CObject as virtual void Serialize( CArchive& ar ) .

One of the useful things about a CArchive is that it ' knows ' whether you're writing to it or reading from it. That is, if you have SomeClass which is perhaps a child of ParentClass , the SomeClass::Serialize function will normally have the form

 void SomeClass::Serialize( CArchive& ar )  {      if (ar.IsStoring()) //Writing data.          // write to ar.      else //Reading data.          // read from ar.  } 

How do we read and write data from a CArchive ? If the data is a class that has its own Serialize method, you can call that. And for primitive data you use operator<< and operator>> . Only a class object can have a Serialize method. If you've worked with C++ file writing before, you're familiar with the overloaded 'extraction' operator<< and the 'insertion' operator>> . In MFC, these operators are friends of the CArchive class. For all the basic types like int , float , and double , and CString , there are operators like this.

  CArchive& cArchive::operator <<( int i );   CArchive& cArchive::operator >>( int i );  

For many MFC classes like CString , there are similar operators, though these are viewed as binary rather than unary operators. Examples are

  CArchive& operator <<( CArchive& ar, const CString& string );   CArchive& operator >>( CArchive& ar, CString& string );  

The << is called 'extraction' which means that you use it to extract data from your class and put it into a CArchive . The >> is called 'insertion' because you use it to get data from a CArchive and insert it into your class.

For a simple utility class like a 2D cVector2 , we can overload the operators like this with these declarations inside the cVector2 class definition.

 friend CArchive& operator<<(CArchive& ar, const cVector2 &v);  friend CArchive& operator>>(CArchive& ar, cVector2 & v); 

And then the cVector2 implementations look like this.

 CArchive& operator<<(CArchive& ar, const cVector2 &v)  {      ar << v._x << v._y;      return ar;  }  CArchive& operator>>(CArchive& ar, cVector2 & v)  {      ar >> v._x >> v._y;      return ar;  } 

The reason that these operators return a CArchive& is so that you can chain them together, as we see in a line like ar << v._x << v._y. The C language associates expressions from the left, and parses this line as ( ar << v._x) << v._y , so we need for the first << call to return another CArchive& to use for the second << call. The argument type is CArchive& rather than CArchive , because these calls change the state of the CArchive .

Just for completeness, we define a Serialize method for cVector2 as well. As a general rule, we should, whenever possible, use Serialize rather than the overloaded operators << and >>. This is especially true for CArray objects, for which the overloaded operators << and >> don't work.

For a more complicated class like cCritter which we expect to use in serializing pointer-based cCritter objects from within other classes, we write the Serialize function first, and then add the special DECLARE_SERIAL and IMPLEMENT_SERIAL macros to tell the compiler to define operator<< and operator>> for the class. (This in fact might have been a good approach to use with the cVector class as well.)

The reason why we prefer to define Serialize and let the application framework generate the other operators is that, with a more complicated class, we really need two forms of the insertion and extraction operators. That is, we need the embedded forms:

operator<<(CArchive & ar, cCritter &critter).  Embedded  extraction.  
operator>>(CArchive & ar, cCritter &critter).  Embedded  insertion.  

And we need the pointer-based forms:

operator<<(CArchive & ar, const cCritter* pcritter).  Pointer-based  extraction.  
operator>>(CArchive & ar, cCritter *& pcritter).  Pointer-based  insertion.  

The idea behind the pointer-based forms is that we don't want to read and write the address that is the pointer. Instead we want the pointer-based extraction operator to extract data from a pointer-based object and write it to the archive. What we want the pointer-based insertion operator to do is more complex. We actually want it to turn the argument pointer into a pointer to a new object, and then to read the CArchive data into that.

If your class inherits from CObject and if the class definition and implementation use the DECLARE_SERIAL and IMPLEMENT_SERIAL macros, then you get all four of the special operators by writing one single function, the class's Serialize function.

How much of an object should we serialize? The best practice is not to worry about whether or not you really 'need' to serialize a variable. Perhaps it gets set by the constructor anyway? So maybe you don't have to serialize it? The best practice is to serialize everything in sight. Work your way down the list of declarations in the header file and serialize everything. Once you get the 'save' direction of the Serialize code written, block-copy it and turn all the << into >> to make sure you 'load' things in the same order.

Here's a small part of the cCritter::Serialize . A bit more of it is printed later in the chapter, and all of it's in critter.cpp of course.

 void cCritter::Serialize(CArchive &ar)  {      CObject::Serialize(ar);          //Call the base class method to save the CRuntimeClass info.      if (ar.IsStoring()) //Writing data.      {  //Personal variables      ar << _age << _lasthit_age<< _oldrecentlydamaged << _health <<          _shieldflag <<      //........ETCETERA..........      }      else //Reading data.      {  //Personal variables      ar >> _age >> _lasthit_age >> _oldrecentlydamaged >> _health >>          _shieldflag >>      //........ETCETERA..........      }  } 

For now, just focus on the fact that we write and read a sequence of things to ar in the same order.

The way we use the two SERIAL macros is that we start the cCritter class declaration like this in the file critter.h .

 class cCritter : public CObject  {  DECLARE_SERIAL(cCritter); 

And we put a line like this at the top of the critter.cpp file, right after the #include lines.

 IMPLEMENT_SERIAL( cCritter, CObject, 0 ); 

Thanks to DECLARE_SERIAL and IMPLEMENT_SERIAL , using the cCritter::Serialize , MFC will automatically define four overloaded operators that work something like this. What we put here is not real code, it's the idea behind the code, or what's sometimes called pseudocode .

 CArchive& operator<<(CArchive & ar, cCritter &critter)      // Embedded extraction.  {      //=======CAUTION:PSEUDOCODE, NOT REAL CODE============      //MFC sets ar so IsStoring() is TRUE      critter.Serialize(ar);      return ar;  }  CArchive& operator>>(CArchive & ar, cCritter &critter)      // Embedded insertion.  {      //=======CAUTION:PSEUDOCODE, NOT REAL CODE============      //MFC sets ar so IsStoring() is FALSE      critter.Serialize(ar);      return ar;  }  CArchive& operator<<(CArchive & ar, const cCritter* pcritter)  // Pointer-based extraction.  {      //=======CAUTION:PSEUDOCODE, NOT REAL CODE============      //MFC sets ar so IsStoring() is TRUE      pcritter->Serialize(ar);      return ar;  }  CArchive& operator>>(CArchive & ar, cCritter *& pcritter).      Pointer-based insertion.  {      //=======CAUTION:PSEUDOCODE, NOT REAL CODE============      //MFC sets ar so IsStoring() is FALSE      //MFC reads the CRuntimeClass information out of ar to get a      //cCritterChild class type.      pcritter = new cCritterChild();      pcritter->Serialize(ar);      return ar;  } 

If you use Edit Find in Files to root around in the MFC source code, you'll see that what the operator>>(CArchive & ar, cCritter *& pcritter) does is to make a call of the form pB = (cCritter *) ar.ReadObject(RUNTIME_CLASS(cCritter)) , which (a) reads off the class name ' cCritterChild ' and object size information for the next object in the archive, which makes it polymorphic, (b) calls pcritter = new cCritterChild() , and (c) calls pcritter->Serialize(ar) to copy the information from ar to *pcritter .

And what makes the critters get serialized in the first place? This results from the serialization of the _pbiota array in the CGame::Serialize method.

One gotcha to be aware of is that when you have as CArray type _myarray , you must serialize this object with calls to Serialize rather than trying to use operator<< and operator>> .

 _myarray.Serialize(ar); //Yes, this works.  ar << _myarray; //No, this won't compile.  ar >> _myarray; //No, this won't compile. 

The *& combination

The first time you see the CArchive operator>>(CArchive & ar, cCritter *& pcritter) declaration, you may perhaps be distressed by seeing the *& combination. Since you knew that * dereferences a pointer and that & generates a pointer reference to an object, you might have the feeling that the *& should cancel each other out. This would indeed be the case if you were to write something like this.

 int x, y;  x = 1;  y = *&x; 

The third line would indeed compile to exactly the same machine code as y = x; .

But when the & is used inside a function prototype's argument list it doesn't have the same meaning. Remember that if you write a prototype like void someFunction(AnyType &mutable) , what you mean is that you call someFunction with an AnyType argument and that someFunction is allowed to change the value of the AnyType argument.

 AnyType mutable = whatever;  someFunction(mutable);  //mutable may no longer be equal to whatever. 

So, logically, this means that if you write void otherFunction(cAnyClass* &mutablepointer) , what you mean is that you call otherFunction with a cAnyClass* argument, and that otherFunction can change the value of the cAnyClass* pointer that you feed into it.

 cAnyClass* mutablepointer = whateverpointer;  otherFunction(mutablepointer);  //mutablepointer may no longer be equal to whateverpointer. 

The reason that we want the operator>> to have a cCritter * &pcritter argument is that, typically, the pcritter argument we feed into it is going to be NULL , and we're going to be counting on the operator>> to construct a new cCritter , fill it with data, and set the value of pcritter equal to that nice new pointer.

Summing up, when you see a prototype like otherFunction(cAnyClass* &mutablepointer) , mentally insert some parentheses and read it as otherFunction((cAnyClass*) &mutablepointer).

Software Engineering and Computer Games
Software Engineering and Computer Games
Year: 2002
Pages: 272

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net