Data Persistence Examples

Introduction

This page gives examples of how to use the persistence library to create a persistent file format for any well-structured data structure.

Persistence of Basic Types

To start with, I'll demonstrate how to dump and restore a simple data type containing only simple C types. The following class will be used for the demonstration:

class point
{
private:
  int m_x;
  int m_y;
  int m_z;
public:
  ...
};

The required parameter profile of the dump/restore functions is:

void dump_type(stlplus::dump_context&, const type&);
void restore_type(stlplus::restore_context&, type&);

These functions should be declared as stand-alone functions and not methods. In this case this will be done by making them friends of the class, meaning they are not methods but can access the data members even though the members are declared as private.

So, here is the point class with the persistence functions' declarations added:

#include "persistent_contexts.hpp"

class point
{
private:
  int m_x;
  int m_y;
  int m_z;
public:
  ...
  friend void dump_point(stlplus::dump_context& context, const point& pt);
  friend void restore_point(stlplus::restore_context& context, point& pt);
};

void dump_point(stlplus::dump_context& context, const point& pt);
void restore_point(stlplus::restore_context& context, point& pt);

The dump and restore functions are written using the existing dump and restore functions for int, the type used for the three dimensions of a point:

#include "persistent_int.hpp"

void dump_point(stlplus::dump_context& context, const point& pt)
{
  stlplus::dump_int(context, pt.m_x);
  stlplus::dump_int(context, pt.m_y);
  stlplus::dump_int(context, pt.m_z);
}

void restore_point(stlplus::restore_context& context, point& pt)
{
  stlplus::restore_int(context, pt.m_x);
  stlplus::restore_int(context, pt.m_y);
  stlplus::restore_int(context, pt.m_z);
}

Note that neither the dump nor the restore actually do any file I/O themselves, it is all delegated to the pre-written functions provided in persistent_basic.hpp for type int. Note also that to access the values directly I have declared the functions as friends.

A complete test program demonstrating this example can be downloaded here.

Persistence of Enumeration Types

Consider the following example. The enum defines a traffic light sequence:

enum traffic_light {red, red_amber, green, amber};

This can be used with stlplus::dump_enum and stlplus::restore_enum directly, but is is better style to write dump and restore functions that call the template functions, thus hiding the use of the template:

#include "persistent_enum.hpp"

void dump_traffic_light(stlplus::dump_context& context, const traffic_light& lights)
{
  stlplus::dump_enum(context, lights);
}

void restore_traffic_light(stlplus::restore_context& context, traffic_light& lights)
{
  stlplus::restore_enum(context, lights);
}

A complete test program demonstrating this example can be downloaded here.

Persistence of Multi-Level Types

A real data structure of course has many layers. The persistence functions are designed to be used in a layered way. The dump/restore functions written above can be used stand-alone to dump a single point, but they can also be used to dump a point stored as part of a different data structure. In this way, dump and restore routines can be built up a layer at a time.

The example will represent an edge as two points:

#include "persistent_contexts.hpp"

class edge
{
private:
  point m_begin;
  point m_end;
public:
  ...
  friend void dump_edge(stlplus::dump_context& context, const edge& pt);
  friend void restore_edge(stlplus::restore_context& context, edge& pt);
};

void dump_edge(stlplus::dump_context& context, const edge& pt);
void restore_edge(stlplus::restore_context& context, edge& pt);

Once again the dump/restore functions can be written in terms of the dump/restore functions for the data members:

void dump_edge(stlplus::dump_context& context, const edge& e)
{
  dump_point(context,e.m_begin);
  dump_point(context,e.m_end);
}

void restore_edge(stlplus::restore_context& context, edge& e)
{
  restore_point(context,e.m_begin);
  restore_point(context,e.m_end);
}

Once again, I have accessed the point elements directly by making the two functions friends of the class.

In this case, to dump an edge means dumping two points which uses the dump function for the point class written in the last section. This layering can be continued ad infinitum.

A complete test program demonstrating this example can be downloaded here.

Persistence of Templates

The template classes provided by the STL and the template classes provided by STLplus have been made persistent using template dump/restore functions.

The persistence functions for templates are themselves templates, so are automatically adapted to the type that the container holds. For example, stlplus::dump_vector which is the dump routine for the STL vector, will adapt to the type being held in the vector. If the vector contains int, then the dump_vector function will dump ints by calling the dump function defined for int. If the vector contains std::strings then the dump_vector function will dump strings. The template function does this by taking the name of the element dump function as a parameter.

To demonstrate, a vector of edges will be used. In this case we need a dump function for a single edge. This has already been written in the last section. Therefore, the dump function for a vector of edges is very simple to write, as is the restore function:

#include "persistent_contexts.hpp"
#include "persistent_vector.hpp"

void dump_edge_vector(stlplus::dump_context& context, const std::vector<edge>& e)
{
  stlplus::dump_vector(context, e, dump_edge);
}

void restore_edge_vector(stlplus::restore_context& context, std::vector<edge>& e)
{
  stlplus::restore_vector(context, e, restore_edge);
}

A complete test program demonstrating this example can be downloaded here.

Single-Level STL Structures

This example shows how to make a map persistent. This is a one-layer data structure because the map only contains the basic types int and string (conceptually a string is an atomic type, even if its implementation just happens to be quite complicated - don't confuse implementation with concept).

The example is based on a test program which is used to test the persistence functions. It creates a data structure, dumps it to a file, restores the file into another data structure and then confirms that the two structures are identical.

The example requires the following set of includes. The reason for each include will be explained as the example unfolds:

#include <string>
#include <map>
#include "persistent_contexts.hpp"
#include "persistent_string.hpp"
#include "persistent_int.hpp"
#include "persistent_map.hpp"
#include "persistent_shortcuts.hpp"
#include "strings.hpp"
#include "dprintf.hpp"
#include "file_system.hpp"
#include "build.hpp"

The following definition is used to define a type that maps an int onto a string, allowing only one mapping for each int (use a multi map if you want multiple mappings of each int):

typedef std::map<int,std::string> int_string_map;

The persistence functions are then instantiations of the template persistence functions for map, using the persistence functions for int and string as parameters:

void dump_int_string_map(stlplus::dump_context& context, const int_string_map& data)
{
  stlplus::dump_map(context, data, stlplus::dump_int, stlplus::dump_string);
}

void restore_int_string_map(stlplus::restore_context& context, int_string_map& data)
{
  stlplus::restore_map(context, data, stlplus::restore_int, stlplus::restore_string);
}

The pre-defined persistence functions can do the whole job and the programmers task is to simply wrap the template functions in non-template wrapper functions. The dump_map function dumps the map by calling dump on the key and data types. The key type is int, which already has a dump function defined. The data type is string, which also has a dump function defined.

For the test we need to be able to compare a restored map with an original one, so here is a comparison function:

bool compare(const int_string_map& left, const int_string_map& right)
{
  bool result = true;
  if (left.size() != right.size())
  {
    std::cerr << "different size - left = " << left.size() << " right = " << right.size() << std::endl;
    result = false;
  }
  int_string_map::const_iterator j, k;
  for (j = left.begin(), k = right.begin(); j != left.end() && k != right.end(); j++, k++)
  {
    if (*j != *k)
    {
      std::cerr << "left = \"" << j->first << "\" is different from right = \"" << k->first << "\"" << std::endl;
      result = false;
    }
  }
  return result;
}

We also need to be able to print out the map, so print functions can be built up using the STLplus strings library::

void print_int(std::ostream& str, int data)
{
  return stlplus::print_int(str, data);
}

std::ostream& operator<< (std::ostream& str, const int_string_map& data)
{
  stlplus::print_map(str, data, print_int, stlplus::print_string, ":", ",");
  return str;
}

The local version of print_int is needed because the STLplus version has extra formatting parameters and the way the print templates work means there must be exactly two parameters to a function passed as an argument.

Finally, a test program can be written:

#define NUMBER 100
#define DATA "map_test.tmp"
#define MASTER "map_test.dump"

int main(int argc, char* argv[])
{
  bool result = true;
  std::cerr << stlplus::build() << " testing " << NUMBER << " mappings" << std::endl;

  try
  {
    // build the sample data structure
    std::cerr << "creating" << std::endl;
    int_string_map data;
    for (unsigned i = 0; i < NUMBER; i++)
      data[i] = stlplus::dformat("%d",i);
    std::cerr << data << std::endl;
    // now dump to the file
    std::cerr << "dumping" << std::endl;
    stlplus::dump_to_file(data,DATA,dump_int_string_map,0);

    // now restore the same file and compare
    std::cerr << "restoring" << std::endl;
    int_string_map restored;
    stlplus::restore_from_file(DATA,restored,restore_int_string_map,0);
    result &= compare(data,restored);

    // compare with the master dump if present
    if (!stlplus::file_exists(MASTER))
      stlplus::file_copy(DATA,MASTER);
    else
    {
      std::cerr << "restoring master" << std::endl;
      int_string_map master;
      stlplus::restore_from_file(MASTER,master,restore_int_string_map,0);
      result &= compare(data,master);
    }
  }
  catch(std::exception& except)
  {
    std::cerr << "caught standard exception " << except.what() << std::endl;
    result = false;
  }
  catch(...)
  {
    std::cerr << "caught unknown exception" << std::endl;
    result = false;
  }

  if (!result)
    std::cerr << "test failed" << std::endl;
  else
    std::cerr << "test passed" << std::endl;
  return result ? 0 : 1;
}

The object called data will be used to store the data to be saved in a file. First, I fill the map with number sequnce mapped onto the string representation of the number, just to demonstrate the data persistence.

To save this data structure to file I use the shortcut function dump_to_file, which does the whole job in one step.

Then the data structure is restored using the complementarry shortcut function restore_from_file.

The rest of the program compares the two data structures to confirm they are identical. It also compares with a pre-saved master copy.

In practice, it is clearer if you use a typedef to create a named type, as in this case, and then write a trivial pair of functions called dump_type and restore_type to hide the use of the template functions. This also means that you can always remember the name of the persistence functions for any type you have designed - because they are always called dump and restore followed by the name of the type. The functions in this case are:

void dump_int_string_map(stlplus::dump_context& context, const int_string_map& data)
{
  stlplus::dump_map(context, data, stlplus::dump_int, stlplus::dump_string);
}

void restore_int_string_map(stlplus::restore_context& context, int_string_map& data)
{
  stlplus::restore_map(context, data, stlplus::restore_int, stlplus::restore_string);
}

A complete test program demonstrating this example can be downloaded here.

Multi-level STL Structures

This example will show how to make a data structure with more than one level of structure persistent.

The example uses a vector of a user-defined class and makes it persistent. It shows how to add persistence functions to a user-defined class so that it can be used with the pre-defined vector persistence functions.

The example requires the following set of includes. The reason for each include will become clear as the example unfolds:

#include <string>
#include <vector>
#include "persistent_string.hpp"
#include "persistent_int.hpp"
#include "persistent_vector.hpp"

The user-defined data structure is a class for storing email addresses. The class without persistence functions is:

class address
{
private:
  std::string m_name;
  std::string m_email;
  int m_age;
public:
  address(void) : m_age(0) { }
  address(const std::string& name, const std::string& email, int age) : m_name(name), m_email(email), m_age(age) {}

  const std::string& name(void) const {return m_name;}
  const std::string& email(void) const {return m_email;}
  int age(void) const {return m_age;}
};

To add persistence, it is only necessary to add a dump and restore function which use the pre-defined dump and restore for string and int. The functions are made friends of the class so that they can access the private data fields directly:

class address
{
  ...
  friend void dump_address(stlplus::dump_context& context, const address& data);
  friend void restore_address(stlplus::restore_context& context, address& data);
};

The function bodies are then declared outside the class:

void dump_address(stlplus::dump_context& context, const address& data)
{
  stlplus::dump_string(context, data.m_name);
  stlplus::dump_string(context, data.m_email);
  stlplus::dump_int(context, data.m_age);
}

void restore_address(stlplus::restore_context& context, address& data)
{
  stlplus::restore_string(context, data.m_name);
  stlplus::restore_string(context, data.m_email);
  stlplus::restore_int(context, data.m_age);
}

The next stage is to define an address book, which is simply an unsorted vector of addresses:

typedef std::vector<address> address_book;

This type is already persistent - there is a pre-defined pair of template functions dump_vector and restore_vector defined in persistent_vector.hpp. However, it is more consistent to provide overloaded non-template dump and restore functions for the address_book type:

void dump_address_book(stlplus::dump_context& context, const address_book& data)
{
  stlplus::dump_vector(context, data, dump_address);
}

void restore_address_book(stlplus::restore_context& context, address_book& data)
{
  stlplus::restore_vector(context, data, restore_address);
}

The following test program shows how an address book can be created and dumped, then restored to another address_book object:

#include "persistent_contexts.hpp"
#include <fstream>

<all the code for the address book above>

int main(int argc, char* argv[])
{
  // create and populate an address book
  address_book addresses;
  addresses.push_back(address("Andy Rushton", "ajr1@ecs.soton.ac.uk", 40));
  addresses.push_back(address("Andrew Brown", "adb@ecs.soton.ac.uk", 85));
  addresses.push_back(address("Mark Zwolinski", "mz@ecs.soton.ac.uk", 21));

  // dump the address book
  std::ofstream output("test.tmp", std::ios_base::out | std::ios_base::binary);
  stlplus::dump_context dumper(output);
  dump_address_book(dumper,addresses);
  output.close();

  // restore the address book to a different object
  address_book restored;
  std::ifstream input("test.tmp", std::ios_base::in | std::ios_base::binary);
  stlplus::restore_context restorer(input);
  restore_address_book(restorer,restored);

  return 0;
}

In this case I'm using persistence to a file, so I've used the fstream devices ofstream and ifstream. This program has no output in its present form - you could then add iostream output methods to print out the restored address book and confirm that it is identical to the input.

Polymorphic Classes using Interfaces

The concepts relating to making polymorphic types persistent are explained in the relevant page on persistent polymorphs. This section will work through an example.

To make a polymorphic class persistent, the first stage is to derive the base class of your family of polymorphic classes from the persistent interface.

#include "persistent_interface.hpp"

class base : public stlplus::persistent

The persistent interface defines three abstract methods and a destructor that you must provide for all subclasses to be made persistent:

class stlplus::persistent
{
public:
  virtual void dump(stlplus::dump_context&) const throw(persistent_dump_failed) = 0;
  virtual void restore(stlplus::restore_context&)  throw(persistent_restore_failed) = 0;
  virtual stlplus::persistent* clone(void) const = 0;
  virtual ~persistent(void) {}
};

Note: The clone method is also required by the smart_ptr_clone container which is also used to store polymorphic classes, so once you've made a class persistent, you've automatically made it suitable for use in this smart pointer.

In order to demonstrate the way polymorphic classes are made persistent, consider the following example:

class base
{
  int m_value;
public:
  base(int value = 0) : m_value(value) {}
  virtual ~base(void) {}

  virtual int value (void) const {return m_value;}
  virtual void set(int value = 0) {m_value = value;}
};

class derived : public base
{
  std::string m_image;
public:
  derived(int value = 0) : base(value), m_image(dformat("%d",value)) {}
  virtual ~derived(void) {}

  virtual void set(int value = 0) {m_image = dformat("%d",value); base::set(value);}
};

In order to make these two classes persistent, the base class must inherit from the persistent interface and then both classes must have the three abstract methods clone, dump and restore added.

Here's these classes with the additions:

class base : public persistent
{
  int m_value;
public:
  base(int value = 0) : m_value(value) {}
  virtual ~base(void) {}

  virtual int value (void) const {return m_value;}
  virtual void set(int value = 0) {m_value = value;}

  persistent* clone(void) const
  {
    return new base(*this);
  }
  void dump(stlplus::dump_context& context) const throw(stlplus::persistent_dump_failed)
  {
    stlplus::dump_int(context,m_value);
  }
  void restore(stlplus::restore_context& context) throw(stlplus::persistent_restore_failed)
  {
    stlplus::restore_int(context,m_value);
  }
};

class derived : public base
{
  std::string m_image;
public:
  derived(int value = 0) : base(value), m_image(to_string(value)) {}
  derived(string value) : base(to_int(value)), m_image(value) {}
  virtual ~derived(void) {}

  virtual void set(int value = 0) {m_image = dformat("%d",value); base::set(value);}

  persistent* clone(void) const
  {
    return new derived(*this);
  }
  void dump(stlplus::dump_context& context) const throw(stlplus::persistent_dump_failed)
  {
    base::dump(context);
    stlplus::dump_string(context,m_image);
  }
  void restore(stlplus::restore_context& context) throw(stlplus::persistent_restore_failed)
  {
    base::restore(context);
    stlplus::restore_string(context,m_image);
  }
};

Note the use of a common strategy here. The subclass dumps its superclass by simply calling the superclass's dump method (in this case, base::dump). This is in keeping with the general C++ convention that subclasses should never use knowledge of the internals of the superclass. This convention is easy to follow: call the dump/restore method of the immediate superclass first, then dump/restore the subclass-specific data.

The STLplus approach to persistence of Polymorphic classes requires that every subclass be registered with the dump_context or restore_context before the dump or restore operation commences. Furthermore, where there are many polymorphic types being handled, the order of registration must be the same for the restore operation as it was for the dump operation.

Consider first the dump operation. The dump_context class provides the following method for registration:

unsigned short dump_context::register_interface(const std::type_info& info);

This is called once for each polymorphic subclass to be dumped. So, for the example above it is called twice:

stlplus::dump_context context(output);

context.register_interface(typeid(base));
context.register_interface(typeid(derived));

The typeid operator is built-in to C++ and provides a means of getting the type name from a type or expression as a char*. This is mapped internally onto a magic key which is an integer value unique to that subclass. The return value of the register_type method is the magic key for that type and is used in the dump to differentiate between the different classes. There's no real reason for capturing this key except maybe for debugging the data stream. Keys are allocated in the order of registration of class types. This is why class types must be registered in the same order for both the dump and restore operations.

For the restore operation it is necessary to register a sample object of the class. This is because the restore operation creates objects of the class by cloning the sample. The sample is stored in a smart_ptr_clone:

typedef stlplus::smart_ptr_clone<persistent> stlplus::persistent_ptr;

The restore_context class provides the following registration function:

unsigned short restore_context::register_interface(const stlplus::persistent_ptr&);

The objects are registered in the same order as the types were registered into the dump context, because it is this ordering that provides the mapping from the unique key used in the dump to the correct sample object used in the restore. During the dump, the class base was registered first, then class derived. The sample objects are therefore registered in the same order for the restore:

stlplus::restore_context context(input);

context.register_interface(stlplus::persistent_ptr(base()));
context.register_interface(stlplus::persistent_ptr(derived()));

An alternative way of registering these interfaces is to wrap their registration up in an installer function. This installer can then be used to install all classes in a single step.

In fact, two installer functions are required - one for dumping and one for restoring. It is up to you to check that these installer functions install their callbacks in the same order. The type profiles for these installer functions are:

void (*dump_context::installer)(stlplus::dump_context&);
void (*restore_context::installer)(stlplus::restore_context&);

In other words, the installer type for a dump_context is a pointer to a function that takes a dump_context& and returns void. Similarly the installer type for a restore_context is a pointer to a function that takes a restore_context& and returns void. For the above example they might look like this:

void register_base_dump(stlplus::dump_context& context)
{
  context.register_interface(typeid(base));
  context.register_interface(typeid(derived));
}

void register_base_restore(stlplus::restore_context& context)
{
  context.register_interface(stlplus::persistent_ptr(base()));
  context.register_interface(stlplus::persistent_ptr(derived()));
}

The functions can be called whatever you like, but I prefer to give them a name that starts with register, reflects the base class name and ends in dump/restore. In use, after creating a dump or restore context, call the method register_all with the above installer as the argument. For example, using the earlier example again, rewritten to use an installer:

stlplus::dump_context context(output);
context.register_all(register_base_dump);

Now that the classes are registered, the actual dump and restore of a superclass pointer is handled by the following functions:

template<typename T>
void dump_interface(dump_context& str, const T*& data);

template<typename T>
void restore_interface(restore_context& str, T*& data);

For example, given the above example using classes base and derived, specific dump and restore functions can be written that simply call the above template functions:

#include "persistent_interface.hpp"

void dump_base(stlplus::dump_context& context, const base*& ptr)
{
  stlplus::dump_interface(context,ptr);
}

void restore_base(stlplus::restore_context& context, base*& ptr)
{
  stlplus::restore_interface(context,ptr);
}

Alternatively, a smart_ptr_clone can be used. This class is specifically designed to point to a polymorphic type which uses the clone method for copying. Furthermore, the persistence functions for smart_ptr_clone call the persistence functions for polymorphic types using the clonable interface. For example, say you have the following type declarations:

typedef stlplus::smart_ptr_clone<base> base_ptr;
typedef std::vector<base_ptr> base_vector;

These types can be made persistent in the usual way, by creating layers of dump and restore functions building up from the low-level contained type to the composite type by calling the template functions for vector and smart_ptr_clone.

We already have persistence of base* handled by the callbacks installed above. To support stlplus::smart_ptr_clone<base> which contains a base* is simply a case of writing a function that calls the template dump/restore for the smart pointer class:

#include "persistent_smart_ptr.hpp"

void dump_base_ptr(stlplus::dump_context& context, const base_ptr& ptr)
{
  stlplus::dump_smart_ptr_clone_interface(context,ptr);
}

void restore_base_ptr(stlplus::restore_context& context, base_ptr& ptr)
{
  stlplus::restore_smart_ptr_clone_interface(context,ptr);
}

Note how the interface variant of the dump/restore functions have been used. Also, the functions for dumping and restoring the smart_ptr_clone do not require parameters naming functions to call for the stored element. This is because the dump/restore methods will be used.

The final stage is to make a vector of these persistent:

#include "persistent_vector.hpp"

void dump_base_vector(stlplus::dump_context& context, const base_vector& vec)
{
  stlplus::dump_vector(context,vec,dump_base_ptr);
}

void restore(stlplus::restore_context& context, base_vector& vec)
{
  stlplus::restore_vector(context,vec,restore_base_ptr);
}

Polymorphic Classes using Callbacks

The previous section described how polymorphic types could be made persistent in an object-oriented way through inheritance and virtual methods. However, it is not always possible to use this approach. For example, you might want to make a class persistent that you cannot change. The details of how this callback approach works is explained in the relevant page on persistence of polymorphic types. This section works through an example.

Consider the following example:

class base
{
  int m_value;
public:
  base(int value = 0) : m_value(value) {}
  virtual ~base(void) {}

  virtual int value (void) const {return m_value;}
  virtual void set(int value = 0) {m_value = value;}
};

class derived : public base
{
  std::string m_image;
public:
  derived(int value = 0) : base(value), m_image(dformat("%d",value)) {}
  virtual ~derived(void) {}

  virtual void set(int value = 0) {m_image = dformat("%d",value); base::set(value);}
};

In order to make these two classes persistent, each one must have three callbacks added. These callbacks can be completely separate from the classes if it is not possible to change the class definitions, but if possible it is easier to add the functions as friends of the class so that they have direct access to the data fields. The three functions are the create, dump and restore callbacks. The convention is to call them create_class, dump_class and restore_class, where class is the name of the class that they act on.

The parameter profiles of the three callbacks is:

void dump_class(stlplus::dump_context& context, const void* data)
void* create_class(void)
void restore_class(stlplus::restore_context& context, void*& data)

For this example, these functions are added to the classes as friends:

#include "persistent_int.hpp"
#include "persistent_string.hpp"

class base
{
  ...
  friend void dump_base(stlplus::dump_context& context, const void* data)
  {
    stlplus::dump_int(context,((const base*)data)->m_value);
  }
  friend void* create_base(void)
  {
    return new base;
  }
  friend void restore_base(stlplus::restore_context& context, void*& data)
  {
    stlplus::restore_int(context,((base*)data)->m_value);
  }
};

class derived
{
  ...
  friend void dump_derived(stlplus::dump_context& context, const void* data)
  {
    dump_base(context,data);
    stlplus::dump_string(context,((const derived*)data)->m_image);
  }
  friend void* create_derived(void)
  {
    return new derived;
  }
  friend void restore_derived(stlplus::restore_context& context, void*& data)
  {
    restore_base(context,data);
    stlplus::restore_string(context,((derived*)data)->m_image);
  }
};

Note the use of a common strategy here. The subclass dumps its superclass by simply calling the superclass's callback (in this case, dump_base). This is in keeping with the general C++ convention that subclasses should not use knowledge of the internals of the superclass. This convention is easy to follow: call the dump/restore callback of the immediate superclass of the subclass first, then dump/restore the subclass-specific data.

The solution for persistence of Polymorphic classes requires that every polymorphic class be registered with the dump_context or restore_context before the dump or restore operation commences. Furthermore, where there are many polymorphic types being handled, the order of registration must be the same for the restore operation as it was for the dump operation.

Consider first the dump operation. The dump_context class provides the following method for registration:

unsigned short dump_context::register_type(const std::type_info& info, dump_callback);

This is called once for each polymorphic type to be dumped. So, for the example above it is called twice:

stlplus::dump_context context(output);

context.register_type(typeid(base),dump_base);
context.register_type(typeid(derived),dump_derived);

The typeid operator is built-in to C++ and provides a means of getting the type name from a type or expression as a char*. This is mapped internally onto a magic key which is an integer value unique to that subclass.

For the restore operation it is necessary to register both a create callback and a restore callback with the restore context. The restore_context class provides the following registration function:

unsigned short restore_context::register_type(create_callback,restore_callback);

The callbacks are registered in the same order as the types were registered into the dump context, because it is this ordering that provides the mapping from the unique key used in the dump to the correct create callback used in the restore. During the dump, the class base was registered first, then class derived. The callbacks are therefore registered in the same order for the restore:

stlplus::restore_context context(input);

context.register_type(create_base,restore_base);
context.register_type(create_derived,restore_derived);

An alternative way of registering these callbacks is to wrap their registration up in an installer function. This installer can then be used to install all callbacks in a single step.

In fact, two installer functions are required - one for dumping and one for restoring. It is up to you to check that these installer functions install their callbacks in the same order. The type profiles for these installer functions are:

void (*dump_context::installer)(stlplus::dump_context&);
void (*restore_context::installer)(stlplus::restore_context&);

In other words, the installer type for a dump_context is a pointer to a function that takes a dump_context& and returns void. Similarly the installer type for a restore_context is a pointer to a function that takes a restore_context& and returns void. For the above example they might look like this:

void register_base_dump(stlplus::dump_context& context)
{
  context.register_type(typeid(base),dump_base);
  context.register_type(typeid(derived),dump_derived);
}

void register_base_restore(stlplus::restore_context& context)
{
  context.register_type(create_base,restore_base);
  context.register_type(create_derived,restore_derived);
}

The functions can be called whatever you like, but I prefer to give them a name that starts with register, reflects the base class name and ends in dump/restore. In use, after creating a dump or restore context, call the method register_all with the above installer as the argument. For example, using the earlier example again, rewritten to use an installer:

stlplus::dump_context context(output);
context.register_all(register_base_dump);

Now that the callbacks are registered, the actual dump and restore of a superclass pointer is handled by the following functions:

template<typename T>
void stlplus::dump_polymorph(stlplus::dump_context& str, const T*& data);

template<typename T>
void stlplus::restore_polymorph(stlplus::restore_context& str, T*& data);

For example, given the above example using classes base and derived, specific dump and restore functions can be written that simply call the above template functions:

#include "persistent_polymorph.hpp"

void dump_base_ptr(stlplus::dump_context& context, const base*& ptr)
{
  stlplus::dump_polymorph(context,ptr);
}

void restore_base_ptr(stlplus::restore_context& context, base*& ptr)
{
  stlplus::restore_polymorph(context,ptr);
}

Note: since polymorphic types are handled in C++ via pointers, the same behaviour is implemented for multiple pointers to the same object as was implemented for simple pointers. When two pointers to the same object are dumped, they will be restored as pointers to the same object.

Using Shortcut Functions

See the page on shortcut functions for an explanation of what these functions do.

This section gives an example of how to use the shortcut functions.

Here's an example that dumps and restores a vector of string to and from a file. First, I need to write a dump/restore pair of functions that make a vector of string persistent using a function with two parameters:

#include <string>
#include <vector>
#include "persistent_contexts"
#include "persistent_vector"
#include "persistent_string"
#include "persistent_shortcuts"

void dump_string_vector(stlplus::dump_context& context, const std::vector<std::string>& data)
{
  stlplus::dump_vector(context, data, stlplus::dump_string);
}

void restore_string_vector(stlplus::restore_context& context, std::vector<std::string>& data)
{
  stlplus::restore_vector(context, data, stlplus::restore_string);
}

Now here's a trivial application that takes the command-line arguments represented by argv and puts them into a vector of strings, then dumps them to a file:

int main (unsigned argc, char* argv[])
{
  if (argc == 1)
    std::cerr << "usage: " << argv[0] << " <strings>" << std::endl;
  else
  {
    // collect argv into a vector of strings
    std::vector<std::string> source;
    for (unsigned i = 1; i < argc; i++)
      source.push_back(std::string(argv[i]));

    // now dump them to a file
    stlplus::dump_to_file(source, "strings.dat", dump_string_vector, 0);
  }
  return 0;
}

Here's a complementary application that restores the file and prints the results to standard output:

int main (unsigned argc, char* argv[])
{
  if (argc != 1)
    std::cerr << "usage: " << argv[0] << std::endl;
  else
  {
    // restore the file
    std::vector<std::string> copy;
    stlplus::restore_from_file("strings.dat", copy, restore_string_vector, 0);

    // now print the strings to the terminal
    for (unsigned i = 0; i < copy.size(); i++)
      std::cout << "[" << i << "] " << copy[i] << std::endl;
  }
  return 0;
}

Persistence can also be implemented in-memory by using a std::string as the target. To illustrate this, I'll use the same example as above for file-based persistence. This example dumps and restores a vector of string to and from a string. Since I've already written the dump/restore pair of functions for the previous example, there's no need to do it again.

Now here's a trivial application that takes the command-line arguments represented by argv and puts them into a vector of strings, then dumps them to a string, restores them from that string and finally compares them to confirm that the two data structures are identical:

int main (unsigned argc, char* argv[])
{
  if (argc == 1)
    std::cerr << "usage: " << argv[0] << " <strings>" << std::endl;
  else
  {
    // collect the arguments into a vector of strings
    std::vector<std::string> source;
    for (unsigned i = 1; i < argc; i++)
      source.push_back(std::string(argv[i]));

    // now convert to the persistence format
    std::string binary;
    stlplus::dump_to_string(source, binary, dump_string_vector, 0);

    // restore form the persistence format
    std::vector<std::string> copy;
    stlplus::restore_from_string(binary, copy, restore_string_vector, 0);
  }
  return 0;
}