We want our C++ objects to be persistent:
Every object should know how to save its 'state' into a file and read it when the process starts so it can continue from where it left off.
However, Serialization implementation in C++ is not trivial (unlike C#) and not all of us want to rely on
MFC framework or boost serialization (which uses templates and could result in a huge executable !).
Here are some of my points for serialization implementation:
1. You should have an abstract class for cloneable objects.
2. You should have a factory class that creates your cloneable objects given a class name
(hint :use Singleton implementation).
3. You don't have to use templates at all !
4. Automatic registration of a class into the factory isn't trivial since C++ doesn't have a static constructor.
This can be done by a macro which adds a static member into your class.
5. You should have an abstract Archive class since you don't know where you store your objects
(file, pipes, memory ..).
6. Use auto_ptr to make sure you delete your objects if the serialization has failed or an exception has been thrown.
Below is a simple but full implementation framework for serialization. Description of the files:
- Dynamics.h - Singleton implementation of cloneable collection.
- Persistent.h/.cpp - Serialization implementation which uses Dynamics.h.
Dynamics.h :
We start by declaring a base class for all cloneable objects. Every cloneable class should be derived from 'Clonable' and implement
createObj
method. createObj
should return a new object of your class.
class Clonable
{
public:
virtual ~Clonable() {}
virtual Clonable* createObj() const = 0;
};
We should declare a collection of cloneable objects. Given a name, we should be able to create an instance of every cloneable class.
For example:
string className = "MyComplexClass";
Clonable* instance = Clonables::Instance().create(className);
MyComplexClass* pCmplx = dynamic_cast<MyComplexClass*>(instance);
Below is a our cloneable collection class. Please notice the Singleton implementation.
class Clonables {
private:
typedef map<string, const Clonable*> NameToClonable;
NameToClonable __clonables;
private:
Clonables() {}
Clonables(const Clonables&); Clonables& operator=(const Clonables&); ~Clonables()
{
for(NameToClonable::const_iterator it = __clonables.begin(); it != __clonables.end(); it++){
const Clonable* clone = it->second;
delete clone;
}
__clonables.clear();
}
public:
static Clonables& Instance()
{
static Clonables instance; return instance; }
public:
void addClonable(const char* className, const Clonable* clone)
{
string name = className;
NameToClonable::const_iterator it = __clonables.find(name);
if(it == __clonables.end()) {
__clonables[name] = clone;
}
}
Clonable* create(const char *className)
{
string name = className;
NameToClonable::const_iterator it = __clonables.find(name);
if(it == __clonables.end()) return NULL;
const Clonable* clone = it->second;
return clone->createObj();
}
};
Cloneable derived classes can add a static member of the following class to allow registration into '<code>Clonables
' (our cloneable collection).
class AddClonable {
public:
AddClonable(const char* className, const Clonable* clone){
Clonables::Instance().addClonable(className, clone);
}
};
Persist.h :
We are not sure where our persistent objects would be saved. So we must implement a base class for streaming (with storing and loading support). An archive can be a file or a pipe or anything which stores our object.
class Archive
{
private:
bool _isStoring;
public:
Archive(bool isStoring = true) : _isStoring(isStoring) {}
virtual ~Archive() {}
virtual void write(const void* buffer, size_t length) {}
virtual void read(void* buffer, size_t length) {}
Archive& operator<<(const string& str);
Archive& operator>>(string& str);
Archive& operator<<(int val);
Archive& operator>>(int& val);
bool isStoring() const { return _isStoring; }
void setDirection(bool isStoring) { _isStoring = isStoring; }
};
Let's define a specific 'Archive' class which uses STL iostream.
class ArchiveFile: public Archive
{
private:
iostream* _stream;
public:
ArchiveFile(iostream* stream) : _stream(stream) {}
virtual ~ArchiveFile() {}
virtual void write(const void *buffer, size_t length);
virtual void read (void* buffer, size_t length);
};
Persistent classes are derived from the below 'Persistent' class and implement the 'serialize' method.
Notice that persistent objects are also cloneables.
class Persistent : public Clonable
{
public:
virtual ~Persistent() {}
static Persistent* load(Archive& stream);
void store(Archive& stream) const;
protected:
virtual void serialize(Archive& stream) {}
virtual int version() const { return 0; }
};
We want an automatic implementation of the createObj
method and an automatic registration
of our class into the cloneables collection. This can be done with the following macro declerations:
PERSISTENT_DECL
macro implements the createObj
method of the 'Clonable' class for us. It also adds 'AddClonable
' static member to our class : this makes our persistent class register itself to the cloneable collection. This should be added to our .h class definition (see examples below).
#define PERSISTENT_DECL(className) \
public: \
virtual Clonable* createObj() const \
{ \
return new className(); \
} \
private: \
static AddClonable _addClonable;
PERSISTENT_IMPL
simply initializes this static member. This should be added to our .cpp class implementation.
#define PERSISTENT_IMPL(className) \
AddClonable className::_addClonable(#className, new className());
Example of how to use: Event.h defines a simple 'Event'
class which should be persistent.
class Event : public Persistent {
private:
int _id;
public:
Event() : _id(0) {}
virtual ~Event() {}
int getId() const { return _id; }
protected:
virtual void serialize(Archive& stream)
{
if(stream.isStoring())
stream << _id;
else
stream >> _id;
}
PERSISTENT_DECL(Event)
};
Event.cpp
#include "Event.h"
PERSISTENT_IMPL(Event)
Before we dive into '<code>Archive'
and '<code>Persistent'
class implementations, here is an example of how to serialize our 'Event' object into a binary file on desktop and then read it back into a new object with the same content.
void serialize_example()
{
auto_ptr<Event> event(new Event());
fstream file("C:\\Users\\Gilad\\Desktop\\try.data",
ios::out | ios::in | ios::binary | ios::trunc);
ArchiveFile stream(&file);
if(! file)
throw "Unable to open file for writing";
event->store(stream);
file.seekg(0, ios::beg);
Event* newEvent = dynamic_cast<Event*>(Persistent::load(stream));
event.reset(newEvent);
file.close();
}
<event>
Persistent.cpp:
We begin our implementation with some basic 'int' and 'string' archiving:
Archive& Archive::operator<<(int val)
{
write(&val, sizeof(int));
return *this;
}
Archive& Archive::operator>>(int& val)
{
read(&val, sizeof(int));
return *this;
}
Archive& Archive::operator<<(const string& str)
{
int length = str.length();
*this << length;
write(str.c_str(), sizeof(char) * length);
return *this;
}
Archive& Archive::operator>>(string& str)
{
int length = -1;
*this >> length;
vector<char> mem(length + 1);
char* pChars = &mem[0];
read(pChars, sizeof(char) * length);
mem[length] = NULL;
str = pChars;
return *this;
}
Now Let's add a specific STL iostream archiving implementation :
void ArchiveFile::write(const void* buffer, size_t length)
{
_stream->write((const char*)buffer,length);
if(! *_stream)
throw "ArchiveFile::write Error";
}
void ArchiveFile::read(void* buffer, size_t length)
{
_stream->read((char*)buffer, length);
if(! *_stream)
throw "ArchiveFile::read Error";
}
This is how we store our persistent object into an archive :
- Save the object's class name.
- Save the version of the class.
- Call the object to serialize itself.
void Persistent::store(Archive& stream) const
{
string className = typeid(*this).name();
className = className.substr(className.find(' ') + 1);
stream << className;
int ver = version();
stream << ver;
stream.setDirection(true);
const_cast<Persistent *>(this)->serialize(stream);
}
This is how we load an object from an archive:
- Read the class name from the archive
- Create the object using our cloneable collection and a simple cast.
- Make sure the version is valid.
- Let our object deserialize itself.
Notice the use of auto_ptr : if an exception is thrown (like from the serialize method), our persistent object would get deleted.
Persistent* Persistent::load(Archive& stream)
{
string className;
stream >> className;
Clonable* clone = Clonables::Instance().create(className.c_str());
if(clone == NULL)
throw "Persistent::load : Error creating object";
auto_ptr<Clonable> delitor(clone);
Persistent * obj = dynamic_cast<Persistent *>(clone);
if(obj == NULL) {
throw "Persistent::load : Error creating object";
}
int ver = -1;
stream >> ver;
if(ver != obj->version())
throw "Persistent::load : unmatched version number";
stream.setDirection(false);
obj->serialize(stream);
delitor.release();
return obj;
}