This article is the third of a 3 part tutorial on serialization.
- Part 1 introduces the basics of serialization.
- Part 2 explains how to gracefully handle reading invalid data stores and support versioning.
- Part 3 describes how to serialize complex objects.
In the previous two parts, we learned how to provide robust support for serialization in general. In this article, we'll learn the specific rules for serializing any kind of object. There are 4 general cases to consider. Each case builds on the previous one.
- Serializing a simple class
- Serializing a derived class
- Serializing a homogenous collection class
- Serializing a heterogenous collection class
Our serialize()
method will return one of these status codes:
Success
InvalidFormat
UnsupportedVersion
ReadError
WriteError
Serializing a Simple Class
A "simple class" is defined as an object that has no parent class and is not a collection class. To serialize a simple class, do the following:
- Serialize the object's signature and version
- Serialize the object's members (if any)
In the following example, the class Point
contains 2 int
members that represent the coordinates of a point. The object's signature and version are defined as static
members (m_strSignature
and m_nVersion
), since they apply to all instances of Point
.
int Point::serialize
(CArchive* pArchive)
{
ASSERT (pArchive != NULL);
int nVersion;
try {
if (pArchive->IsStoring()) {
(*pArchive) << Point::m_strSignature;
(*pArchive) << Point::m_nVersion;
} else {
CString strSignature;
(*pArchive) >> strSignature;
if (strSignature != Point::m_strSignature)
return (Status::InvalidFormat);
(*pArchive) >> nVersion;
if (nVersion > Point::m_nVersion;)
return (Status::UnsupportedVersion);
}
if (pArchive->IsStoring()) {
(*pArchive) << m_nX;
(*pArchive) << m_nY;
} else {
(*pArchive) >> m_nX;
(*pArchive) >> m_nY;
}
}
catch (CException* pException) {
pException->Delete();
if (pArchive->IsStoring())
return (Status::WriteError);
return (Status::ReadError);
}
return (Status::Success);
}
Serializing a Derived Class
For the purpose of this discussion, a derived class is one that is derived from a simple class and is not a collection class. To serialize a derived class, do the following:
- Serialize the object's signature and version
- Serialize the object's base class << additional step
- Serialize the object's members (if any)
In the following example, the class ColoredPoint
is derived from Point
and contains an additional int
member called m_nColor
, which specifies the point's color. Like all serializable classes, ColoredPoint
also defines a static
signature and version.
int ColoredPoint::serialize
(CArchive* pArchive)
{
ASSERT (pArchive != NULL);
int nVersion;
try {
if (pArchive->IsStoring()) {
(*pArchive) << ColoredPoint::m_strSignature;
(*pArchive) << ColoredPoint::m_nVersion;
} else {
CString strSignature;
(*pArchive) >> strSignature;
if (strSignature != ColoredPoint::m_strSignature)
return (Status::InvalidFormat);
(*pArchive) >> nVersion;
if (nVersion > ColoredPoint::m_nVersion;)
return (Status::UnsupportedVersion);
}
int nStatus = Point::serialize (pArchive);
if (nStatus != Status::Success)
return (nStatus);
if (pArchive->IsStoring())
(*pArchive) << m_nColor;
else
(*pArchive) >> m_nColor;
}
catch (CException* pException) {
pException->Delete();
if (pArchive->IsStoring())
return (Status::WriteError);
return (Status::ReadError);
}
return (Status::Success);
}
Serializing a Homogenous Collection Class
Homogenous collection classes are used to store dynamically sized collections of the same type of object. To serialize a homogenous collection class, do the following:
- Serialize the object's signature and version
- Serialize the object's base class (if any)
- Serialize the number of items in the collection << additional step
- Serialize each object in the collection << additional step
- Serialize the object's other members (if any)
In the following example, the class ColoredPointList
is a collection of ColoredPoint
objects. To keep things simple, ColoredPointList
uses a CPtrArray
to store objects. Like all serializable classes, ColoredPointList
also defines a static signature and version. Here's what ColoredPointList
looks like:
class ColoredPointList
{
public:
ColoredPointList::ColoredPointList();
virtual ColoredPointList::~ColoredPointList();
public:
static const CString m_strSignature;
static const int m_nVersion;
public:
int serialize (CArchive* pArchive);
protected:
CPtrArray m_coloredPoints;
}
And here's how we serialize it:
int ColoredPointList::serialize
(CArchive* pArchive)
{
ASSERT (pArchive != NULL);
int nStatus = Status::Success;
int nVersion;
try {
if (pArchive->IsStoring()) {
(*pArchive) << ColoredPointList::m_strSignature;
(*pArchive) << ColoredPointList::m_nVersion;
} else {
CString strSignature;
(*pArchive) >> strSignature;
if (strSignature != ColoredPointList::m_strSignature)
return (Status::InvalidFormat);
(*pArchive) >> nVersion;
if (nVersion > ColoredPointList::m_nVersion;)
return (Status::UnsupportedVersion);
}
int nItems = 0;
if (pArchive->IsStoring()) {
nItems = m_coloredPoints.GetSize();
(*pArchive) << nItems;
} else
(*pArchive) >> nItems;
for (int nObject=0; (nObject < nItems); nObject++) {
ColoredPoint* pColoredPoint = NULL;
if (pArchive->IsStoring())
pColoredPoint = (ColoredPoint *) m_coloredPoints.GetAt (nObject);
else
pColoredPoint = new ColoredPoint();
ASSERT (pColoredPoint != NULL);
nStatus = pColoredPoint->serialize (pArchive);
if (nStatus != Status::Success)
return (nStatus);
if (!pArchive->IsStoring())
m_coloredPoints.Add (pColoredPoint);
}
}
catch (CException* pException) {
pException->Delete();
if (pArchive->IsStoring())
return (Status::WriteError);
return (Status::ReadError);
}
return (Status::Success);
}
Serializing a Heterogenous Collection Class
Heterogenous collection classes are used to store dynamically sized collections of potentially different types of objects. To serialize a heterogenous collection class, do the following:
- Serialize the object's signature and version
- Serialize the object's base class (if any)
- Serialize the number of items in the collection
- For each object in the collection
- serialize that object's signature << additional step
- then serialize that object
- Serialize the object's other members (if any)
You'll notice the only additional step in serializing heterogenous collections is 4(a), where we serialize each object's signature before serializing the object itself. This comes in handy when reading back our data. When we serialized a homogenous collection, we dealt with objects of the same type (ColoredPoint
s in the previous example). To read in a ColoredPoint
, we constructed it on the heap and then called its serialize()
method.
ColoredPoint* pColoredPoint = new ColoredPoint();
nStatus = pColoredPoint->serialize (pArchive);
When we're dealing with heterogenous collections, we need to know the type of object we're reading back in, before actually serializing it. That's where the object's signature comes in. Since we saved the signature on the way out, we can construct an object of the appropriate type on the way in.
CString strSignature;
pArchive >> strSignature;
ISerializable* pObject = NULL;
if (strSignature == ColoredPoint::m_strSignature)
pObject = new ColoredPoint();
else
if (strSignature == Line::m_strSignature)
pObject = new Line();
else
if (strSignature == Rectangle::m_strSignature)
pObject = new Rectangle();
else
return (Status::InvalidFormat);
ASSERT (pObject != NULL);
nStatus = pObject->serialize (pArchive);
In the above code fragment, ColoredPoint
, Line
and Rectangle
are all (eventually) derived from a common base class ISerializable
, which is nothing more than an abstract
base class containing only pure virtual
methods (in other words, an "interface"). ISerializable
defines the methods getSignature()
, getVersion()
and serialize()
.
class ISerializable
{
public:
ISerializable::ISerializable()
{ }
virtual ISerializable::~ISerializable()
{ }
public:
virtual CString getSignature() = 0;
virtual int getVersion() = 0;
virtual int serialize (CArchive* pArchive) = 0;
}
OK, let's serialize our heterogenous collection. In the following example, the class ShapeList
is a collection of varying numbers of ColoredPoint
, Line
and Rectangle
objects, all of which derive from ISerializable
. You can look upon these classes as "implementing the ISerializable
interface".
int ShapeList::serialize
(CArchive* pArchive)
{
ASSERT (pArchive != NULL);
int nStatus = Status::Success;
int nVersion;
try {
if (pArchive->IsStoring()) {
(*pArchive) << ShapeList::m_strSignature;
(*pArchive) << ShapeList::m_nVersion;
} else {
CString strSignature;
(*pArchive) >> strSignature;
if (strSignature != ShapeList::m_strSignature)
return (Status::InvalidFormat);
(*pArchive) >> nVersion;
if (nVersion > ShapeList::m_nVersion;)
return (Status::UnsupportedVersion);
}
int nItems = 0;
if (pArchive->IsStoring()) {
nItems = m_shapes.GetSize();
(*pArchive) << nItems;
} else
(*pArchive) >> nItems;
for (int nObject=0; (nObject < nItems); nObject++) {
CString strSignature;
if (pArchive->IsStoring())
(*pArchive) << pObject->getSignature();
else
(*pArchive) >> strSignature;
ISerializable* pObject = NULL;
if (pArchive->IsStoring())
pObject = (ISerializable *) m_shapes.GetAt (nObject);
else {
if (strSignature == ColoredPoint::m_strSignature)
pObject = new ColoredPoint();
else
if (strSignature == Line::m_strSignature)
pObject = new Line();
else
if (strSignature == Rectangle::m_strSignature)
pObject = new Rectangle();
else
return (Status::InvalidFormat);
}
ASSERT (pObject != NULL);
nStatus = pObject->serialize (pArchive);
if (nStatus != Status::Success)
return (nStatus);
if (!pArchive->IsStoring())
m_shapes.Add (pObject);
}
}
catch (CException* pException) {
pException->Delete();
if (pArchive->IsStoring())
return (Status::WriteError);
return (Status::ReadError);
}
return (Status::Success);
}
Class Factory
You can replace the ugly if
statement in the code fragment by using a "class factory" to serve up a new instance of a class based on its signature. Here are some articles you might want to look at:
Although the articles vary in complexity, the basic idea behind them is the same. A class factory is little more than a class that offers an appropriately named static
method (e.g.: create()
) that serves up an object of a specific type. You could hide the nasty if
statement in the factory's create()
method, cleaning up the code a bit.
...
ISerializable* pObject = MyClassFactory::create (strSignature);
ASSERT (pObject != NULL);
...
Conclusion
Although this primer refers to MFC objects like CString
and CPtrArray
, serialization is not specific to MFC. It's a common operation performed by any software that needs to save and restore information to and from persistent storage.
Revision History
- 24 Jun 2005
Fixed typo in example code (thanks, J Roy!) - 13 Jun 2005
Fixed typos in example code (thanks, Mohammed Tarik!) - 26 Apr 2004
Fixed typo in example code (thanks, David Paul Jones!) - 24 Apr 2004
Added much needed examples - 18 Feb 2002
Initial version