10.2. Serializing pointers

Object graphs that have cycles in them are supported. A cycle occurs when an object point to other objects which point back to the first object. Such graphs are correctly restored upon deserialization.

For pointers to objects, there are two library behaviours that can be controlled by clients: the tracking of pointers, and the handling of pointers to polymorphic objects.

10.2.1. Pointer tracking

What does pointer tracking do? When enabled, it does two things:

  1. It causes the pointer's data (ie, the object that is pointed at) to be stored only once in the database. After the first time that the object is encountered by the library, all other requests to save the same pointer will cause the library to write a database "pointer" to the same row of the same table as the first time the object was written.

  2. It causes any pointers to the same object to receive the same value when the data is retrieved from the database. If p and q are two pointers which have the same value (ie, point to the same object) and are then serialized to the database, then upon loading the serialized data, the same two pointers will end up pointing to the same deserialized object data. For example, if p and q have the address 0x1234 when they are stored, and later on their values are deserialized into a new object which has the address 0x5678, then p and q will both have the value 0x5678 after loading is completed. If pointer tracking was disabled for the type that p and q point to, then because the data would be written out twice (see [1]) then p and q will end up with different pointer values (addresses) after loading, even if they had the same pointer value (address) at the time of storing.

If your data has cycles in it then pointer tracking should not be disabled. If the library detects a cycle for a type that has pointer tracking disabled, it will throw a "cannot_serialize_cyclical_pointer_when_tracking_of_the_pointer_type_is_disabled_exception".

To disable pointer tracking for all types, specify WANT_POINTER_TRACKING as false for the traits class passed to storage (it defaults to true). See "Pointer tracking option" above.

To disable pointer tracking for a specific type, specify WANT_POINTER_TRACKING as true for the traits class passed to storage (it defaults to true), and then define a template specialization for that type, or use the CCS_DISABLE_POINTER_TRACKING_FOR macro at global scope. For example, if pointers to Person objects are being stored but you don't want pointer tracking for them (because you never store the same pointer more than once), then you can disable pointer tracking just for Person objects by doing this:

// this must be done at global scope and not inside a namespace {...}:
CCS_DISABLE_POINTER_TRACKING_FOR(Person);

or this:

namespace ccs {
  namespace serialization {
    template <>
    class want_pointer_tracking_for<Person> { public:
      enum { value = false };
    };
  }
}

Note that if Person is a base class for a (polymorphic) class hierarchy, then the above will disable pointer tracking for all instances of Person and all instances of subclasses of Person.

If you wish to have pointer tracking globally on but default to off for specific types, you'll need to change the source code for the serialization library. Search for the text "class want_pointer_tracking_for" in serialization.hpp and change the enum from true to false.

10.2.2. Polymorphic pointers

10.2.2.1. Requirements

Given a base type BT and a type DT that is derived from BT, then for a pointer to an object of type BT (ie, "BT* pointer;") which at runtime points to an object of type DT, the library will load and save the pointer polymorphically (ie, load and save a DT object) if these conditions are met:

  1. the compiler is set to compile with RTTI support enabled. Failure to do this will cause compile-time errors when the typeid() operator is invoked by the library.

  2. the value of the macro HAVE_RTTI is set to a non-zero value, typically one. The default value is one.

  3. global polymorphic support is on, ie, the trait class passed to storage<> specifies WANT_POLYMORPHIC_SUPPORT as true. This setting allows polymorphic handling on a per-object-set granularity (recall that a single database file can contain multiple object sets). The default is true if the value of the macro HAVE_RTTI is non-zero.

  4. BT polymorphic support is on, ie, ccs::serialization::want_polymorphic_pointer_handling_for<BT>::value is true. This setting allows polymorphic handling on a per-base-class granularity. The default is true.

  5. DT polymorphic support is on, ie, ccs::serialization::want_polymorphic_pointer_handling_for<DT>::value is true. This setting allows polymorphic handling on a per-class granularity. The default is true.

  6. DT has been registered as a type to be loaded/saved polymorphically when a pointer to its base class has been encountered, ie, "register_type<DT>();" has been called on the storage object prior to loading/saving. If DT has not been registered then DT objects will be saved as BT objects if BT is not abstract, otherwise a cannot_serialize_abstract_type_exception is thrown.

If any of the conditions are not met, then the pointer to a DT instance will be loaded and saved as a BT object.

Now since the global and per-type default are set to true for all types when HAVE_RTTI is non-zero, then by default, all types are treated as polymorphic types.

10.2.2.2. Example

Here is an example on how to serialize polymorphic pointers:

class DrawingContext;

class Shape {
public:
  virtual ~Shape() {}
  virtual void draw(DrawingContext& dc) = 0;
};

class Line : public Shape {
public:
  virtual void draw(DrawingContext& dc) {
    ...
  }

private:
  friend class ccs::serialization::access;

  template <typename S>
  void serialize(S& s) {
    s & left_ & top_ & width_ & height_;
  }

  float left_, top_, width_, height_;
};

class Rectangle : public Shape {
public:
  virtual void draw(DrawingContext& dc) {
    ...
  }

private:
  friend class ccs::serialization::access;

  template <typename S>
  void serialize(S& s) {
    s & left_ & top_ & width_ & height_;
  }

  float left_, top_, width_, height_;
};

class Circle : public Shape {
public:
  virtual void draw(DrawingContext& dc) {
    ...
  }

private:
  friend class ccs::serialization::access;

  template <typename S>
  void serialize(S& s) {
    s & x_ & y_ & radius_;
  }

  float x_, y_, radius_;
};

void
SaveShapes(const std::string& path, const std::vector<Shape*>& shapes)
{
  ccs::serialization::storage<> db(path);

  db.register_type<Line>();
  db.register_type<Rectangle>();
  db.register_type<Circle>();

  // alternatively:
  //db.register_type< ccs::tmp::tuple<Line, Rectangle, Circle> >();

  // Actual types will be used on saving; loading requires the same
  // registration calls.
  db << shapes;
}

10.2.2.3. Disabling polymorphic support

  • To disable polymorphic support for all types, specify WANT_POLYMORPHIC_SUPPORT as false for the traits class passed to storage (it defaults to true). See "Polymorphic pointer option" above.

  • To disable polymorphic support for a type BT and all of its descendent types:

    namespace ccs {
      namespace serialization {
        template <>
        class want_polymorphic_pointer_handling_for <BT> { public:
          // BT* pointers will be loaded/saved as BT objects no matter what the actual type is:
          enum { value = false };
        };
      }
    }
  • To disable polymorphic support for a descendent type DT:

    namespace ccs {
      namespace serialization {
        template <>
        class want_polymorphic_pointer_handling_for <DT> { public:
          // BT* pointers that point to DT objects will be loaded/saved as BT objects:
          enum { value = false };
        };
      }
    }