Into

Modules

Documentation

Making a Class Serializable

Basic Serialization

The simplest way of making a class serializable is to implement a serialize() member function:

 class MyClass
 {
 private:
   int _i;
   double _d;

   // Serialization declarations
   friend struct PiiSerialization::Accessor;
   template <class Archive> void serialize(Archive& archive, const unsigned int version)
     {
        archive & _i;
        archive & _d;
     }
 };

The friend declaration isn't actually necessary, but makes it possible to hide the serialization function from the cruel world. The Serialization library invokes the same serialization function for both input and output archives. Since they both have the operator& defined, the same function works both in saving and loading data. Note that serialization depends on the order of reading / writing. If you write _i first, you must read it back first.

Now, let's assume you need to serialize a class whose code you cannot or don't want to alter. Its public interface looks like this:

 class ProprietaryClass
 {
 public:
   int value() const;
   void setValue(int value);
 };

Since modification of the class declaration is not feasible, one needs to implement an external serialization function. The problem is that the member variables cannot be directly accessed. In saving, the value() member function must be used. In loading, one needs its setValue() counterpart. The library provides a macro, PII_SEPARATE_SAVE_LOAD_FUNCTIONS, to conveniently solve this problem. The macro creates a serialize() function that invokes either save() or load(), depending on the archive type.

 PII_SEPARATE_SAVE_LOAD_FUNCTIONS(ProprietaryClass)

 namespace PiiSerialization
 {
   template <class Archive> inline void save(Archive& ar, ProprietaryClass& obj, const unsigned int version)
     {
        int value = obj.value();
        ar << value;
     }

   template <class Archive> inline void load(Archive& ar, ProprietaryClass& obj, const unsigned int version)
     {
        int value;
        ar >> value;
        obj.setValue(value);
     }
 }

Save and load can be separated also for internal serialization, if needed:

 class MyClass
 {
 private:
   friend struct PiiSerialization::Accessor;
   PII_SEPARATE_SAVE_LOAD_MEMBERS
   template <class Archive> void save(Archive& archive, const unsigned int version)
     {
        // ...
     }
   template <class Archive> void load(Archive& archive, const unsigned int version)
     {
        // ...
     }
 };

Serializing Base Class

When derived objects are serialized, their base class also needs to be serialized. Since serialization of an object must always be controlled by the Serialization library, one should never call the serialize() member function of a base class directly. Keeping the serialization function private as suggested helps in this.

PiiBaseClass.h provides means of serializing base classes. The easiest way of serializing the base class is the PII_SERIALIZE_BASE macro:

 class MyDerivedClass : public MyClass
 {
 private:
   float _f;

   friend struct PiiSerialization::Accessor;
   template <class Archive> void serialize(Archive& archive, const unsigned int version)
     {
       PII_SERIALIZE_BASE(archive, MyClass);
       archive & _f;
     }
 };

This approach has the advantage that the subclass does not need to know how the superclass has serialization implemented. It may be either internal or external, but we don't need to care.

The serialization of the base class goes through a wrapper for which the tracking and classinfo traits are turned off. Thus, the wrapper itself leaves no marks into the archive. The version number of the base class will be stored, which makes it possible to change the storage format on all levels of the inheritance hierarchy.

Serializing through Base Class Pointer

So far we have been dealing with types we know at compile time. Serialization of such types can be resolved by the compiler, provided that proper declarations for the serialization function templates are available.

Imagine a class called Garage whose purpose is to store a collection of Cars. Car is an (abstract) superclass for Truck, Mazda, and Volvo. Truck is inherited by Sisu and a number of other makes that will be available upon loading of dynamic libraries. The best Garage can do is to keep a list of pointers to Cars, because it has no way of knowing all possible types at compile time. The class declarations look like this:

 class Garage
 {
 public:
   // I'm a collector; there is no removeCar() function.
   void addCar(Car* car) { _lstCars.apppend(car); }
 private:
   QList<Car*> _lstCars;
 };

 // We omit the public interface. Just private stuff is shown.
 class Car
 {
   int _iWheels;
 };

 class Truck : public Car
 {
   bool _bHasTrailer;
 };

 class Sisu : public Truck
 {
   double _dExtraWidthForEURoads;
 };

 class Mazda : public Car
 {
   bool _bIsRusty;
 };

 class Volvo : public Car
 {
   enum Sound { TractorSound, BulldozerSound, TruckSound };
   Sound _sound;
 };

Now, how do we ensure that the members of the derived classes are saved when a Garage is stored?

The Serialization library uses PiiMetaObject to store information related to a type. Currently, such information include class name and version number. Since requesting the meta object for Car would not help in our situation, there must be a way of finding the "most derived" class. The only way this can be performed across dynamic library boundaries is to implement a virtual piiMetaObject() function. (Yes, we have seen the vtable tricks in boost::serialization.)

The downside of the virtual meta object function is that the declaration of the base class must be accessible. If this is not the case, one needs to create an empty subclass just for serialization purposes. The empty subclass declares the virtual metaobject function. To make Cars serializable through the base class, we make the following change:

 class Car
 {
   friend struct PiiSerialization::Accessor;
   PII_VIRTUAL_METAOBJECT_FUNCTION

   // The serialization function
   template <class Archive> void serialize(Archive& archive, const unsigned int)
     {
       PII_SERIALIZE(archive, _iWheels);
     }

   int _iWheels;
 };

 // ...

 class Volvo : public Car
 {
   friend struct PiiSerialization::Accessor;
   PII_VIRTUAL_METAOBJECT_FUNCTION

   template <class Archive> void serialize(Archive& archive, const unsigned int)
     {
       PII_SERIALIZE_BASE(archive, Car);
       PII_SERIALIZE(archive, _sound);
     }

   enum Sound { TractorSound, BulldozerSound, TruckSound };
   Sound _sound;
 };

 // Specialize meta object for this type to call the virtual function
 PII_SERIALIZATION_VIRTUAL_METAOBJECT(Car)

The PII_VIRTUAL_METAOBJECT_FUNCTION macro must be present in all class declarations that are serialized through a base class pointer, and the base class itself. The base class must be annotated with the PII_SERIALIZATION_VIRTUAL_METAOBJECT macro. In our example, only Car needs it. If the base class is abstract, one needs to use the PII_SERIALIZATION_ABSTRACT macro.

Now that the Serialization library knows the meta object it must find a serializer and a factory (an object that can create a class instance given a key) for the corresponding class. The class name is used as a look-up key for this. Every archive type has its own serializer map to which serializers must be registered by the the class name. The PII_INSTANTIATE_SERIALIZER macro registers a serializer for an archive type. The PII_INSTANTIATE_FACTORY creates the factory, which is not specific to any archive type. But before the factory and the serializers can be created we need to name the type. This is done with the PII_SERIALIZATION_NAME macro.

We need to instantiate the serializers and factories for all new types derived from Car:

 PII_SERIALIZATION_NAME(Car);
 PII_INSTANTIATE_SERIALIZER(PiiTextInputArchive, Car);
 PII_INSTANTIATE_SERIALIZER(PiiTextOutputArchive, Car);
 PII_INSTANTIATE_FACTORY(Car);

 // Repeat for other types

Making a Class Serializable the Easy Way

Now that we are familiar with the concepts or registering serializable types we can just forget the manual way of doing things. All one really needs to do is to define some configuration values and include a single file called PiiSerializableRegistration.h. Car could be registered simply this way:

 #define PII_SERIALIZABLE_CLASS Car
 #define PII_VIRTUAL_METAOBJECT
 #include <PiiSerializableRegistration.h>

PiiSerializableRegistration.h can be included as many times as necessary.

Versioning

It sometimes happens that a class changes format while one still has to support previously stored archives. This is possible if a class has the PiiSerializationTraits::ClassInfo trait turned on (it is by default). In deserialization, the serialize() function will be given the version number of the stored object. The Serialization library checks that the version number of valid and will fail if it is larger than the current version number of the object.

Let us assume you add a _iDoors member to the Car class. The new declaration and registration for the class is as follows:

 class Car
 {
   friend struct PiiSerialization::Accessor;
   PII_VIRTUAL_METAOBJECT_FUNCTION

   // The serialization function
   template <class Archive> void serialize(Archive& archive, const unsigned int version)
     {
       PII_SERIALIZE(archive, _iWheels);
       // Serialize _iDoors only if the version number is not zero
       if (version > 0)
         PII_SERIALIZE(archive, _iDoors);
     }

   int _iWheels;
   int _iDoors;
 };

 #define PII_SERIALIZABLE_CLASS Car
 #define PII_VIRTUAL_METAOBJECT
 // The current version number of Car is 1
 #define PII_SERIALIZABLE_CLASS_VERSION 1
 #include <PiiSerializableRegistration.h>

Whenever you make changes that affect the archive format and still want to support old archives, increase the version number. Note that the Serialization uses only eight bits (256 different values) to store the version number to save space. If you need more, store the version number as an integer in the beginning of your serialization function.

Non-default Constructors

Although it is usually bad practice not to provide a default constructor for a class, there may be situations where a serializable class must be created with a non-default constructor.

The serialization library uses the PiiSerialization::Constructor struct to create new class instances. The default implementation returns new Type for a non-abstract Type, and a null pointer for abstract types. Specializing the PiiSerialization::Constructor structure either fully or partially makes it possible to change the way object instances are created. For convenience, the PII_SERIALIZATION_CONSTRUCTOR and PII_SERIALIZATION_NORMAL_CONSTRUCTOR macros are provided.

Notes (0)

Add a note

Not a single note added yet. Be the first, add yours.