This page has been translated into Spanish by Maria Ramos from Webhostinghub.com/support/edu.

1. Overview

Sweet Persist is a C++ library that serializes data to and from streams in XML, JSON, Lua, and binary formats. It compiles with Microsoft Visual Studio 2008 (MSVC 9.0), MinGW (GCC 4.6.2), and Xcode (LLVM-GCC 4.2.1).

Features:

  • Serialization of basic types including narrow and wide character strings.

  • Conversion of enumerations and bit masks between their integer values and text.

  • Conversion between absolute and relative paths.

  • Polymorphism.

  • Serialization of arbitrary objects via intrusive or non intrusive mechanisms.

  • Multiple and possibly cyclical references to objects.

  • The STL containers vector, deque, list, set, multiset, map, and multimap.

  • The smart pointers auto_ptr, scoped_ptr, intrusive_ptr, shared_ptr, and weak_ptr.

  • Backwards compatibility through versioning.

  • UTF-8 encoded XML archives.

  • JSON archives.

  • Lua archives.

  • Binary archives.

Anti features:

  • The library uses templates so has longer compile times and larger binaries.

  • Multiple inheritance is not supported.

1.1. Boost Serialization vs Sweet Persist

The Boost Serialization Library is written by Robert Ramey. Documentation is available a http://www.boost.org/libs/serialization/doc/index.html.

The main difference between Boost Serialization and Sweet Persist is the way that they handle shared data. Boost Serialization automatically tracks pointers to shared data and writes out objects by value the first time they are encountered and then by reference for any further occurences. Sweet Persist allows the application to specify when an object is written out by reference and when it is written out by value. This is a deliberate design decision to simplify the implementation and to allow control of where data shows up in an archive - in our opinion this makes them easier to transform using XSLT and easier to read. The cost of doing it this way is that the application is then responsible for making sure that all referenced objects are written to an archive and that objects aren’t written out by value more than once.

Sweet Persist supports weak_ptrs and intrusive_ptrs while Boost Serialization does not. The support for shared_ptrs in Sweet Persist does not depend on their implementation as it does in Boost Serialization. Sweet Persist supports conversion of enumerations and bit masks to useful text values for text based archives. Sweet Persist also supports the conversion of paths - allowing absolute paths to be converted to relative paths when persisted in an archive and back to absolute paths when the archive is read back in.

Boost Serialization handles multiple inheritance, classes that contain reference member variables, can work without RTTI, is far more portable, supports boost::optional and boost::variant, and has been reviewed and tested extensively by the Boost community.

In summary Boost Serialization will be better if you don’t need control over where your data appears when written to an XML archive, you don’t need your enumerations and bit masks converted to text, you don’t need relative paths in your archives, or you don’t want the responsibility of determining which objects to write by value and which to write out by reference.

1.2. libs11n vs Sweet Persist

The libs11n Library is written by Stephan Beal. Documentation is available at http://s11n.net/.

The libs11n Library has excellent documentation and supports a wide variety of data formats including persistence to MySQL and SQLite databases. However it doesn’t support wide characters, binary archives, or use as a static library.

In summary the libs11n implementation will probably be better for you if you want to support serialization to a wide variety of data formats or SQL databases. On the other hand if you need to support wide characters, binary archives, or don’t like dynamic libraries then you might prefer Boost Serialization or Sweet Persist.

2. Installation

Sweet Persist is built and installed by downloading the latest version from http://www.sweetsoftware.co.nz/, extracting the archive onto your computer, and then running "build.bat" (on Windows) or "sh ./build.sh" (on MacOSX) from the top level directory. This builds all variants and a Visual Studio solution or Xcode project to browse through the source.

You’ll need to add the top level directory (e.g. "c:\sweet\sweet_persist") and the library directory (e.g. "c:\sweet\sweet_persist\lib") to your compiler’s header and library search paths respectively.

If you want Sweet Persist built to another location or with different variants and/or settings then you’ll need to edit the settings in sweet/build.lua and rebuild.

3. Usage

Sweet Persist serializes arbitrary data to and from an iostream. Typically this is a file but can be a buffer in memory or any more complex combination supported by iostreams.

The library provides writer and reader objects to serialize data. Different types of writers and readers are used serialize archives in XML (XmlWriter, XmlReader), JSON (JsonWriter, JsonReader), Lua (LuaWriter, LuaReader), and binary (BinaryWriter, BinaryReader) formats.

Data is serialized out by creating a writer and calling its "write()" method. This recursively traverses the data model being serialized using function templates that describe the relationships in the data model. Similarly data is read in by creating a reader and calling its "read()" method. This reconstructs the data in memory using the same function templates.

The function templates to traverse the data model are typically provided as "persist()" member function templates of the class being serialized. This is the intrusive form. It is the simplest form and most classes use it to serialize their data.

template <class Archive>
void persist( Archive& archive );

The function templates can also be provided in a non-intrusive form using "save()", "load()", and "resolve()" functions overloaded on the the type being serialized if the class to be serialized is not able to be modified or if special case handling such as linking are required (see the links section below). The simpler intrusive form is implemented using the non-intrusive form by having the default overload for the non-intrusive functions call the "persist()" member function template of the object being serialized.

template <class Archive, class Type>
void save( Archive& archive, int mode, const char* name, Type& object );

template <class Archive, class Type>
void load( Archive& archive, int mode, const char* name, Type& object );

template <class Archive, class Type>
void resolve( Archive& archive, int mode, Type& object );

For example the classic "Hello World!" using Sweet Persist:

#include <sweet/persist/persist.hpp>
#include <string>

using std::string;
using namespace sweet::persist;

struct HelloWorld
{
    string message;

    template <class Archive> void persist( Archive& archive )
    {
        archive.enter( "HelloWorld", 1, *this );
        archive.value( "message", message );
    }
};

void persist_hello_world_example()
{
    HelloWorld hello_world;
    hello_world.message = "Hello World!";
    XmlWriter xml_writer;
    xml_writer.write( "persist_hello_world_example.xml", "hello_world", hello_world );

    hello_world.message.clear();
    XmlReader xml_reader;
    xml_reader.read( "persist_hello_world_example.xml", "hello_world", hello_world );
    SWEET_ASSERT( hello_world.message == "Hello World!" );
}

The example program produces the following output:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<hello_world format="HelloWorld" version="1" message="Hello World!" address="0x27ff0c"/>

3.1. Cyclic References

Sweet Persist supports serialization of cyclic references by allowing objects to be persisted by reference instead of by value.

Serializing by reference uses the address of an object as a unique identifier that can be written out in place of the object’s content and used to resolve back to the equivalent object when the serialized archive is read back into memory. Any referenced object must be serialized somewhere within the archive so that references to it can be properly recovered.

When a serialized archive is read back in it is read using two passes. The first pass loads the data from the archive back into memory and builds a mapping from each object’s address when it was written into the archive to the address that it has been allocated at when read in. The second pass traverses the data in memory to resolve references made using the old addresses to objects at their new addresses in memory.

To serialize an object by reference instead of by value the application calls the "refer()" function on the archive rather than the "value()" function. The obvious usage of this is in serializing a raw pointer to an object but it may also be applied to objects with pointer like semantics (e.g. smart pointers) or to containers. In the latter case the contents of the container are serialized as references rather than as values.

For example a simple network of people with cyclic references from each person to their friends:

#include <sweet/persist/persist.hpp>
#include <sweet/persist/vector.hpp>
#include <string>
#include <vector>

using std::vector;
using std::string;
using namespace sweet::persist;

struct Person
{
    string name_;
    vector<Person*> friends_;

    Person()
    : name_(),
      friends_()
    {
    }

    Person( const string& name )
    : name_( name ),
      friends_()
    {
    }

    void add_friend( Person* person )
    {
        friends_.push_back( person );
    }

    template <class Archive> void persist( Archive& archive )
    {
        archive.value( "name", name_ );
        archive.refer( "friends", "friend", friends_ );
    }
};

void persist_cyclic_references_example()
{
    vector<Person> people;
    people.push_back( Person("Alfred") );
    people.push_back( Person("Ben") );
    people.push_back( Person("Camilla") );
    people.push_back( Person("Denise") );

    Person& alfred = people[0];
    Person& ben = people[1];
    Person& camilla = people[2];
    Person& denise = people[3];

    alfred.add_friend( &ben );
    alfred.add_friend( &camilla );
    ben.add_friend( &alfred );
    ben.add_friend( &denise );
    camilla.add_friend( &alfred );
    camilla.add_friend( &ben );
    camilla.add_friend( &denise );
    denise.add_friend( &camilla );

    XmlWriter xml_writer;
    xml_writer.write( "persist_cyclic_references_example.xml", "people", "person", people );

    people.clear();
    XmlReader xml_reader;
    xml_reader.read( "persist_cyclic_references_example.xml", "people", "person", people );
}

The example program produces the following output:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<people>
    <person name="Alfred" address="0x31f08">
        <friends>
            <friend address="0x31f18"/>
            <friend address="0x31f28"/>
        </friends>
    </person>
    <person name="Ben" address="0x31f18">
        <friends>
            <friend address="0x31f08"/>
            <friend address="0x31f38"/>
        </friends>
    </person>
    <person name="Camilla" address="0x31f28">
        <friends>
            <friend address="0x31f08"/>
            <friend address="0x31f18"/>
            <friend address="0x31f38"/>
        </friends>
    </person>
    <person name="Denise" address="0x31f38">
        <friends>
            <friend address="0x31f28"/>
        </friends>
    </person>
</people>

3.2. Polymorphism

Sweet Persist supports serializing polymorphic objects. To do so polymorphic types must be declared with the reader or writer that is doing the serialization so that the "persist()" function of the correct type can be called when the reader or writer is passed a base class pointer to an object.

Polymorphic types are declared with a reader or writer using the "declare()" function. The "declare()" function is a function template that takes the true type as a template parameter and the name to use to uniquely identify the type when serialized.

template <class Type>
void declare( const char* name, int flags );

For example a contrived example with left and right object inheriting from a base object. It’s no worse than using animals in an example.

#include <string>
#include <sweet/persist/persist.hpp>
#include <sweet/persist/vector.hpp>

using namespace std;
using namespace sweet::persist;

struct Object
{
    string name_;

    Object()
    : name_()
    {
    }

    Object( const string& name )
    : name_(  name )
    {
    }

    virtual ~Object()
    {
    }

    template <class Archive> void persist( Archive& archive )
    {
        archive.value( "name", name_ );
    }
};

struct LeftObject : public Object
{
    int left_value_;

    LeftObject()
    : Object(),
      left_value_( 0 )
    {
    }

    LeftObject( const string& name, int left_value )
    : Object( name ),
      left_value_( left_value )
    {
    }

    template <class Archive> void persist( Archive& archive )
    {
        Object::persist( archive );
        archive.value( "left_value", left_value_ );
    }
};

struct RightObject : public Object
{
    float right_value_;

    RightObject()
    : Object(),
      right_value_( 0.0f )
    {
    }

    RightObject( const string& name, float right_value )
    : Object( name ),
      right_value_( right_value )
    {
    }

    template <class Archive> void persist( Archive& archive )
    {
        Object::persist( archive );
        archive.value( "right_value", right_value_ );
    }
};

struct Polymorphism
{
    vector<Object*> objects_;

    public:
        Polymorphism()
        : objects_()
        {
        }

        ~Polymorphism()
        {
            while ( !objects_.empty() )
            {
                delete objects_.back();
                objects_.pop_back();
            }
        }

        void create()
        {
            objects_.push_back( new Object("object_0") );
            objects_.push_back( new LeftObject("left_0", 0) );
            objects_.push_back( new RightObject("right_0", 0.0f) );
            objects_.push_back( new LeftObject("left_1", 1) );
            objects_.push_back( new LeftObject("left_2", 2) );
            objects_.push_back( new RightObject("right_1", 1.0f) );
            objects_.push_back( new RightObject("right_2", 2.0f) );
            objects_.push_back( new LeftObject("left_3", 3) );
        }

        template <class Archive> void persist( Archive& archive )
        {
            archive.enter( "Polymorphism", 1, *this );
            archive.value( "objects", "object", objects_ );
        }
};

template <class Archive> void declare( Archive& archive )
{
    archive.declare<Object>( "Object", PERSIST_POLYMORPHIC );
    archive.declare<LeftObject>( "LeftObject", PERSIST_POLYMORPHIC );
    archive.declare<RightObject>( "RightObject", PERSIST_POLYMORPHIC );
}

void persist_polymorphism_example()
{
    Polymorphism polymorphism;
    polymorphism.create();
    JsonWriter json_writer;
    declare( json_writer );
    json_writer.write( "persist_polymorphism_example.json", "polymorphism", polymorphism );

    Polymorphism other_polymorphism;
    JsonReader json_reader;
    declare( json_reader );
    json_reader.read( "persist_polymorphism_example.json", "polymorphism", other_polymorphism );
}

The example program produces the following output:

{
    "polymorphism": {
        "format": "Polymorphism",
        "version": 1,
        "address": "0017FF00",
        "objects": {
            "object": {
                "class": "Object",
                "name": "object_0",
                "address": "00CA79C8"
            },
            "object": {
                "class": "LeftObject",
                "name": "left_0",
                "left_value": 0,
                "address": "00CA7A28"
            },
            "object": {
                "class": "RightObject",
                "name": "right_0",
                "right_value": 0,
                "address": "00CA52D8"
            },
            "object": {
                "class": "LeftObject",
                "name": "left_1",
                "left_value": 1,
                "address": "00CA57F0"
            },
            "object": {
                "class": "LeftObject",
                "name": "left_2",
                "left_value": 2,
                "address": "00CA58A8"
            },
            "object": {
                "class": "RightObject",
                "name": "right_1",
                "right_value": 1,
                "address": "00CA5968"
            },
            "object": {
                "class": "RightObject",
                "name": "right_2",
                "right_value": 2,
                "address": "00CA59D0"
            },
            "object": {
                "class": "LeftObject",
                "name": "left_3",
                "left_value": 3,
                "address": "00CA5E98"
            }
        }
    }
}

3.3. Context

Each archive is able to store void pointers to context that can be used by serialization code during reading or writing. Typically the type parameter is provided using the "SWEET_RTTI_TYPEID()" macro on the type of context being provided.

void set_context( const rtti::Type& type, void* context );
void* get_context( const rtti::Type& type ) const;

3.4. Filters

Sweet Persist is able to provide conversion on serialized values with filters. Filters are provided to convert bitmasks between integer and string values, enumerated values between integer and string values, and paths between absolute and relative.

Mask filters treat integer values like bitmasks and convert the integral value to and from an "|" separated string of application supplied mask values when serializing.

Enum filters treat integer or enumerated values as enumerations and convert the integral value to and from an application supplied string when serializing.

Path filters convert between an absolute path in the application and a relative path in the archive. The relative path can be relative to any path but it is often most useful to make it relative to the path to the archive. This allows the archive to be moved around (assuming that any files that it references are also moved so that they remain in the same relative position).

For example a contacts database that uses a mask filter to store the categories of a contact and an enum filter to store the status of a contact:

#include <sweet/persist/persist.hpp>
#include <sweet/persist/vector.hpp>
#include <string>
#include <vector>

using std::vector;
using std::string;
using namespace sweet::persist;

enum Category
{
    CATEGORY_NONE = 0x00,
    CATEGORY_SOCIAL = 0x01,
    CATEGORY_WORK = 0x02,
    CATEGORY_CUSTOMER = 0x04
};

enum Status
{
    STATUS_UNKNOWN,
    STATUS_ACTIVE,
    STATUS_INACTIVE
};

struct Contact
{
    string email_;
    int categories_;
    Status status_;

    public:
        Contact()
        : email_(),
          categories_(),
          status_()
        {
        }

        Contact( const string& email, int categories, Status status )
        : email_( email ),
          categories_( categories ),
          status_( status )
        {
        }

        template <class Archive> void persist( Archive& archive )
        {
            static const MaskFilter::Conversion CATEGORY[] =
            {
                { CATEGORY_NONE, "None" },
                { CATEGORY_SOCIAL, "Social" },
                { CATEGORY_WORK, "Work" },
                { CATEGORY_CUSTOMER, "Customer" },
                { 0, 0 }
            };

            static const EnumFilter::Conversion STATUS[] =
            {
                { STATUS_UNKNOWN, "Unknown" },
                { STATUS_ACTIVE, "Active" },
                { STATUS_INACTIVE, "Inactive" },
                { 0, 0 }
            };

            archive.value( "email", email_ );
            archive.value( "categories", categories_, mask_filter(CATEGORY) );
            archive.value( "status", status_, enum_filter(STATUS) );
        }
};

void persist_filters_example()
{
    vector<Contact> contacts;
    contacts.push_back( Contact("alfred@example.com", CATEGORY_NONE, STATUS_ACTIVE) );
    contacts.push_back( Contact("ben@example.com", CATEGORY_SOCIAL, STATUS_INACTIVE) );
    contacts.push_back( Contact("camilla@example.com", CATEGORY_SOCIAL | CATEGORY_WORK, STATUS_INACTIVE) );
    contacts.push_back( Contact("denise@example.com", CATEGORY_WORK | CATEGORY_CUSTOMER, STATUS_ACTIVE) );

    JsonWriter json_writer;
    json_writer.write( "persist_filters_example.json", "people", "person", contacts );

    contacts.clear();
    JsonReader json_reader;
    json_reader.read( "persist_filters_example.json", "people", "person", contacts );
}

The example program produces the following output:

{
    "people": {
        "person": {
            "email": "alfred@example.com",
            "categories": "None",
            "status": "Active",
            "address": "00CA5980"
        },
        "person": {
            "email": "ben@example.com",
            "categories": "Social",
            "status": "Inactive",
            "address": "00CA59A8"
        },
        "person": {
            "email": "camilla@example.com",
            "categories": "Social|Work",
            "status": "Inactive",
            "address": "00CA59D0"
        },
        "person": {
            "email": "denise@example.com",
            "categories": "Work|Customer",
            "status": "Active",
            "address": "00CA59F8"
        }
    }
}

Path filters, non-intrusive serialization, and stored context can be combined to serialize objects as links to data stored in external files rather than as data stored directly in the serialized archive itself.

For example during the development of a game using textures a serialized archive can be written and read that refers to textures stored in files external to the serialized archive itself. The textures themselves are managed using a cache of textures that ensures that if the same texture is referenced multiple times each reference refers to the same texture.

Non-intrusive serialization functions "save()", "load()", and "resolve()" are written to serialize the textures. The loading function retrieves a TextureCache to retrieve a texture from using the archive’s context function. Path filters are used to convert between the absolute paths used to refer to textures in memory and paths relative to the serialized archive in the archive.

#include <sweet/persist/persist.hpp>
#include <map>

using std::map;
using std::string;
using namespace sweet::persist;

class TextureCache;

class Texture
{
    string filename_;
    TextureCache* manager_;

    public:
        Texture( const string& filename, TextureCache* manager )
        : filename_( filename ),
          manager_( manager )
        {
        }

        const string& filename() const
        {
            return filename_;
        }
};

class TextureCache
{
    map<string, Texture*> textures_;

    public:
        TextureCache()
        : textures_()
        {
        }

        Texture* texture( const string& filename )
        {
            Texture* texture = NULL;
            map<string, Texture*>::iterator i = textures_.find( filename );
            if ( i != textures_.end() )
            {
                texture = i->second;
            }
            else
            {
                texture = new Texture( filename, this );
                textures_.insert( make_pair(filename, texture) );
            }
            return texture;
        }

        void release( Texture* texture )
        {
            if ( texture )
            {
                SWEET_ASSERT( textures_.find(texture->filename()) != textures_.end() );
                map<string, Texture*>::iterator i = textures_.find( texture->filename() );
                textures_.erase( i );
                delete texture;
            }
        }
};

template <class Archive>
void save( Archive& archive, int mode, const char* name, Texture* texture )
{
    ObjectGuard<Archive> guard( archive, name, NULL, mode, texture ? 1 : 0 );
    if ( texture )
    {
        string filename = texture->filename();
        archive.value( "filename", filename, path_filter(archive.get_path()) );
    }
}

template <class Archive>
void load( Archive& archive, int mode, const char* name, Texture*& texture )
{
    SWEET_ASSERT( texture == NULL );

    ObjectGuard<Archive> guard( archive, name, NULL, mode );
    if ( archive.is_object() )
    {
        string filename;
        archive.value( "filename", filename, path_filter(archive.get_path()) );

        TextureCache* texture_manager = reinterpret_cast<TextureCache*>( archive.get_context(SWEET_STATIC_TYPEID(TextureCache)) );
        SWEET_ASSERT( texture_manager );
        texture = texture_manager->texture( filename );
    }
}

template <class Archive>
void resolve( Archive& archive, int mode, Texture*& texture )
{
    ObjectGuard<Archive> guard( archive, NULL, NULL, mode );
}

class Model
{
    TextureCache texture_cache_;
    Texture* texture_;
    Texture* other_texture_;

    public:
        Model()
        : texture_cache_(),
          texture_( NULL ),
          other_texture_( NULL )
        {
        }

        ~Model()
        {
            texture_cache_.release( other_texture_ );
            other_texture_ = NULL;
            texture_cache_.release( texture_ );
            texture_ = NULL;
        }

        void create()
        {
            string cwd = narrow(sweet::path::current_working_directory().string());
            texture_ = texture_cache_.texture( cwd + "/textures/texture.tga" );
            other_texture_ = texture_cache_.texture( cwd + "/textures/other_texture.tga" );
        }

        template <class Archive> void enter( Archive& archive )
        {
            archive.set_context( SWEET_STATIC_TYPEID(TextureCache), &texture_cache_ );
        }

        template <class Archive> void persist( Archive& archive )
        {
            archive.enter( "Textures", 1, *this );
            archive.value( "texture", texture_ );
            archive.value( "other_texture", other_texture_ );
        }
};

void persist_links_example()
{
    Model model;
    model.create();
    JsonWriter json_writer;
    model.enter( json_writer );
    json_writer.write( "persist_links_example.json", "model", model );

    Model other_model;
    JsonReader json_reader;
    other_model.enter( json_reader );
    json_reader.read( "persist_links_example.json", "model", other_model );
}

The example program generates the following output:

{
    "model": {
        "format": "Textures",
        "version": 1,
        "address": "0017FEF0",
        "texture": {
            "filename": "textures/texture.tga"
        },
        "other_texture": {
            "filename": "textures/other_texture.tga"
        }
    }
}

3.6. Standard Template Library and Boost Support

Additional headers are provided for serializing the standard template library containers and Boost library smart pointers. The additional headers are given the same base name as the STL or Boost header that is included to use the actual type (e.g. include "<sweet/persist/list.hpp>" to persist STL lists or "<sweet/persist/shared_ptr.hpp>" to persist Boost shared pointers).

Additional headers only need to be included when their particular type needs to be persisted. If an additional header is left out then the compiler will generate many errors as it tries to match all of the overloaded save and load function templates.

3.7. Format and Version

Sweet Persist identifies serialized archives by format and version. The format of an archive is a string that must match the expected format when the archive is read in. The version is used to determine which action to take when loading in an older version of an archive.

The call to "enter()" provides the format and version of the archive that is to be serialized. When an archive is written the format and version are written to the archive. Later on when the archive is read back in the format and version are read in and checked against the format passed to the function. If the format doesn’t match an exception is thrown. Version checking is left to application code as it may wish to perform conversion or allow backwards compatibility.

The application is able to branch and provide different behaviour based on the version of the archive that is read in but this approach has the potential to generate spaghetti code over time. A nice alternative is to provide an external script (XSLT, Javascript for JSON, or Lua for Lua) that converts between versions that is executed by the application to convert older versions into newer versions.

3.8. Character Encoding

Sweet Persist supports both wide and narrow characters. When serializing text archives strings are converted to UTF-8 encoding in an intermediate tree representation. Narrow character strings are are assumed to be encoded as either ASCII or UTF-8 already. Wide character strings are assumed to be encoded as UCS-2 and are converted to UTF-8 by the library when they are put into the intermediate tree representation.

When saving and loading binary archives there is no intermediate represention and thus no conversion of character data. Strings are saved and loaded directly without conversion.

3.9. Error Handling And Exception Safety

Sweet Persist provides strong exception safety when saving objects. The objects that are being saved are never changed even if an exception is thrown. However if an exception is thrown the file system has potentially been changed and an incomplete archive may have been written to disk. If an exception is caught while writing it is recommended that the application removes any files that may have been created for the archive.

Only weak exception safety is provided when reading archives. If an exception is thrown then no resources are leaked but the model may be left in an inconsistent state.

Any pointers in the model that are serialized by value are guaranteed to be either as they were before the archive was loaded or pointing to valid heap allocated objects. It is possible to put the data model back in a consistent state without leaking resources by traversing and deleting any pointers that are serialized by value and that are not null. This assumes that the pointers were initialised to null before the archive was loaded.

It is easy to provide strong exception safety when reading archives by reading into temporary objects and swapping these with the actual objects after the read has succeeded. If the read fails the temporary objects are discarded and the actual objects remain unchanged.