A Simple C++ Object Loader

Published October 06, 2009 by Francis Xavier, posted by Myopic Rhino
Do you see issues with this article? Let us know.
Advertisement
[size="5"]Introduction

This is a guide to using Daabli: a simple serialization framework for C++. At this point it's worth mentioning that Daabli is only a deserialization framework; serialization support is currently under development. Still, I thought it would be worth writing a guide on how to use it for deserialization, instead of waiting for completion of serialization support. If your application needs to load objects and data from human readable text files, then Daabli could be useful to you. The currently supported features are:
  • loads objects from a "C" style format which is easy to read and edit using any plain text editor
  • extremely simple to integrate and use
  • portable code; implemented in standard C++
  • does not require loadable types to derive from a specific base class
  • does not add any overhead (in terms of space/time) to the types it can load
  • non-intrusive loading support for types which cannot be modified (e.g., library types)
  • built-in support for the following STL containers: [font="Courier New"]string[/font], [font="Courier New"]vector[/font], [font="Courier New"]list[/font], [font="Courier New"]deque[/font], [font="Courier New"]pair[/font], [font="Courier New"]map[/font], [font="Courier New"]multimap[/font], [font="Courier New"]set[/font], [font="Courier New"]multiset[/font]
  • supports loading enumerations[sup][1][/sup]
  • supports loading pointers
    • supports data-structures which form graphs
    • supports forward pointers to objects
    • supports (polymorphic and non-polymorphic) pointers to objects of derived types
  • handles multiple inheritance (although it cannot handle virtual base classes as yet)
  • does not require RTTI to be enabled
  • does not require exceptions to be enabled
  • If the above mentioned features are enough to load your objects, then let's proceed to getting started with Daabli.

    [size="3"]Getting started

    First we need to get the source code. Using any SVN client (see note below), checkout the code from this repository URL. Alternatively, you can download and extract an archive of the code from here. You can place the code anywhere you like. Apart from the framework source code, there are also some samples which demonstrate how to use it.
    [bquote]
    Note:
    If you don't have an SVN client and you're on Windows, then I'd recommend getting TortoiseSVN. If you're on Linux, then you could try eSvn
    [/bquote]
    If the path where you've placed the downloaded code is [font="Courier New"]INSTALLPATH[/font], then you need to add the folder [font="Courier New"]INSTALLPATH/Daabli/[/font] to the list of source code folders which your development environment searches. Also, you must add the file [font="Courier New"]INSTALLPATH/Daabli/Daabli.cpp[/font] to each project which uses Daabli and make sure it gets compiled.

    Now that we're all setup to use Daabli, let's take a look at how we can use Daabli to load objects.

    [size="5"]Reading objects

    Objects are read using a Reader object. The following functions are commonly used for reading:
    • [font="Courier New"]const bool FromFile(const string &fileName);[/font]
      • Specifies which file the Reader should read from.
  • [font="Courier New"]const bool Read(T &obj);[/font]
    • Reads a required object of any type.
    • Data format: [font="Courier New"][type]object[/type][/font]
  • [font="Courier New"]const bool Read(const string &name, T &obj);[/font]
    • Reads a required object of any type, associated with an identifier (like a name/value pair).
    • Data format: [font="Courier New"][type]identifier[/type] = [type]object[/type];[/font]
  • [font="Courier New"]const bool Read(const string &name, T &obj, const T &defaultValue);[/font]
    • Reads an optional object of any type, associated with an identifier; if not present, the specified default value is used.
    • Data format: [font="Courier New"][[type]identifier[/type] = [type]object[/type];][/font]
  • Any required cleanup is automatically done when the Reader object goes out of scope.

    [size="5"]Basic types

    Daabli has built-in support for loading the following basic types: [font="Courier New"]bool[/font], [font="Courier New"]char[/font], [font="Courier New"]int[/font], [font="Courier New"]unsigned int[/font], [font="Courier New"]long[/font], [font="Courier New"]unsigned long[/font], [font="Courier New"]float[/font], [font="Courier New"]double[/font].

    Numeric objects are described directly (without decoration), character objects can also be described as a literal enclosed in single quotes, and boolean objects are described with either [font="Courier New"]true[/font] or [font="Courier New"]false[/font]. For e.g., 15 could describe an [font="Courier New"]int[/font], 3.14159 could describe a [font="Courier New"]double[/font], true could describe a [font="Courier New"]bool[/font], and 65 or 'A' could describe a [font="Courier New"]char[/font].

    Let's look at an example of how to load some properties of a space ship from a description.

    In main.cpp:

    #include "Daabli.h"
    #include

    struct SpaceShip
    {
    char _type;
    float _maxSpeed;
    float _mass;
    bool _invulnerable;
    };

    int main(int /*argc*/, char * /*argv*/[])
    {
    // Create a Reader object
    Daabli::Reader r;

    // Specify which file to read from
    if( !r.FromFile( "input.txt" ) )
    return -1;

    SpaceShip ship;

    // Read the space ship's properties
    if( !r.Read( "type", ship._type ) || // required
    !r.Read( "maxSpeed", ship._maxSpeed ) || // required
    !r.Read( "mass", ship._mass, 10.0f ) || // optional
    !r.Read( "invulnerable", ship._invulnerable, false ) ) // optional
    return -1;

    // Display the space ship's properties
    std::cout << "type : " << ship._type << std::endl;
    std::cout << "maxSpeed : " << ship._maxSpeed << std::endl;
    std::cout << "mass : " << ship._mass << std::endl;
    std::cout << "invulnerable : " << ship._invulnerable << std::endl;

    return 0;
    }

    Given the description in input.txt:

    /* Player ship properties */
    type = 'A';
    maxSpeed = 10.2;
    mass = 50.7;
    //invulnerable = true;

    The output generated would be:

    [font="Courier New"]type : A
    maxSpeed : 10.2
    mass : 50.7
    invulnerable : 0[/font]

    Note from the input.txt file that Daabli supports C/C++ style comments.

    [size="5"]STL containers

    Daabli has built-in support for loading the following STL containers: [font="Courier New"]string[/font], [font="Courier New"]vector[/font], [font="Courier New"]list[/font], [font="Courier New"]deque[/font], [font="Courier New"]pair[/font], [font="Courier New"]map[/font], [font="Courier New"]multimap[/font], [font="Courier New"]set[/font], [font="Courier New"]multiset[/font].

    All the containers (except for [font="Courier New"]string[/font]) are described by elements enclosed in curly brackets and separated by commas. Strings are described by a string literal enclosed in double quotes. For example, "Hello World!" describes a [font="Courier New"]string[/font], {1, 2, 3} could describe a [font="Courier New"]vector[type]int[/type][/font], {"Hello", "World"} could describe a [font="Courier New"]pair[type]string, string[/type][/font], and {{"One", 1}, {"Two",2}} could describe a [font="Courier New"]map[type]string, int[/type][/font].

    Let's look at an example which loads data into some STL containers.

    In main.cpp:

    #include "Daabli.h"
    #include

    typedef std::map LevelPriceMap;
    typedef std::vector Row;
    typedef std::vector Matrix;

    struct Item
    {
    std::string _name;
    LevelPriceMap _levelPrices;
    Matrix _transform;
    };

    int main(int /*argc*/, char * /*argv*/[])
    {
    Daabli::Reader r;
    if( !r.FromFile( "input.txt" ) )
    return -1;

    Item item;

    // Read the item properties
    if( !r.Read( "name", item._name ) ||
    !r.Read( "levelPrices", item._levelPrices ) ||
    !r.Read( "transform", item._transform ) )
    return -1;

    // Display the item properties
    {
    std::cout << "Item Properties" << std::endl;
    std::cout << "Name: " << item._name << std::endl;

    for(LevelPriceMap::const_iterator itr = item._levelPrices.begin();
    itr != item._levelPrices.end(); ++itr)
    {
    std::cout << "Price at Level " << itr->first << ": " << itr->second << std::endl;
    }

    std::cout << "Transformation: " << std::endl;
    for(std::size_t j=0; j < item._transform.size(); ++j)
    {
    for(std::size_t i=0; i < item._transform[j].size(); ++i)
    std::cout << item._transform[j] << " ";

    std::cout << std::endl;
    }
    }

    return 0;
    }

    Given the description in input.txt:

    /* Item properties */

    name = "Small Healing Potion";

    levelPrices = // Level => Price
    {
    { 1, 10.5 },
    { 2, 15 },
    { 3, 16.5 }, // modest price increase at this level
    { 4, 20 }
    };

    transform = // identity matrix; no transformation
    {
    { 1, 0, 0 },
    { 0, 1, 0 },
    { 0, 0, 1 }
    };

    The output generated would be: [font="Courier New"]

    Item Properties
    Name: Small Healing Potion
    Price at Level 1: 10.5
    Price at Level 2: 15
    Price at Level 3: 16.5
    Price at Level 4: 20
    Transformation:
    1 0 0
    0 1 0
    0 0 1[/font]

    Note how nested containers (like Matrix in the above example) are handled automatically without the user having to write any extra code.

    [size="5"]Custom types

    Daabli supports two methods for loading all types: Intrusive and Non-intrusive.

    The intrusive method is easier to implement for a type, but requires modification of the type (a public [font="Courier New"]Read[/font] function of a specified signature must be added). The non-intrusive method is a bit more difficult to implement, but doesn't require modification of the type; hence it's the only option for types which cannot be modified (like library types for example). Note that the non-intrusive method requires that the type can be loaded using only its public interface (since private members are inaccessible).

    When Daabli has to read a type, it first checks if a non-intrusive method for reading the type exists. If it does, then the type is read using the non-intrusive method, otherwise the type is read using the intrusive method. Let's take a look at the two methods of reading in detail.

    [size="3"]Custom types - Intrusive method

    For a type to support the intrusive method of reading, it must have a public [font="Courier New"]Read[/font] function (which reads its members) with the following signature (const-correctness is optional):

    [font="Courier New"]const bool Read(Daabli::Reader &r) const; [/font]

    Let's modify the space ship example from earlier, to support the intrusive method of reading. This time, we'll read a list of space ships instead of just one.

    In main.cpp:

    #include "Daabli.h"
    #include

    struct SpaceShip
    {
    char _type;
    float _maxSpeed;
    float _mass;
    bool _invulnerable;

    // Support the intrusive reading method
    const bool Read(Daabli::Reader &r)
    {
    return
    r.Read( "type", _type ) &&
    r.Read( "maxSpeed", _maxSpeed ) &&
    r.Read( "mass", _mass ) &&
    r.Read( "invulnerable", _invulnerable, false );
    }
    };

    int main(int /*argc*/, char * /*argv*/[])
    {
    Daabli::Reader r;
    if( !r.FromFile( "input.txt" ) )
    return -1;

    std::list ships;

    // Read the list of space ships
    if( !r.Read( "ships", ships ) )
    return -1;

    // Display the list of space ships
    for(std::list::const_iterator itr = ships.begin(); itr != ships.end(); ++itr)
    {
    std::cout << "type : " << (*itr)._type << std::endl;
    std::cout << "maxSpeed : " << (*itr)._maxSpeed << std::endl;
    std::cout << "mass : " << (*itr)._mass << std::endl;
    std::cout << "invulnerable : " << (*itr)._invulnerable << std::endl;
    std::cout << std::endl;
    }

    return 0;
    }

    Given the description in input.txt:

    ships =
    {
    // Drone ship
    {
    type = 'D';
    maxSpeed = 10.2;
    mass = 50.7;
    },

    // Fighter ship
    {
    type = 'F';
    maxSpeed = 22.4;
    mass = 25.1;
    },

    // Shop ship
    {
    type = 'S';
    maxSpeed = 0;
    mass = 100;
    invulnerable = true;
    }
    };

    The output generated would be:

    [font="Courier New"]type : D
    maxSpeed : 10.2
    mass : 50.7
    invulnerable : 0

    type : F
    maxSpeed : 22.4
    mass : 25.1
    invulnerable : 0

    type : S
    maxSpeed : 0
    mass : 100
    invulnerable : 1[/font]

    Note that custom types read using the intrusive method can be (optionally) enclosed in curly brackets.

    Derived classes must read their base object parts inside their own [font="Courier New"]Read[/font] methods in the manner shown below:

    class A
    {
    /* A's members go here */
    public:
    const bool Read(Daabli::Reader &r)
    {
    /* Read A's members here */
    return true;
    }
    };

    class B
    {
    /* B's members go here */
    public:
    const bool Read(Daabli::Reader &r)
    {
    /* Read B's members here */
    return true;
    }
    };

    class C : public A, public B
    {
    /* C's members go here */
    public:
    const bool Read(Daabli::Reader &r)
    {
    // Read bases
    if( !r.Read( *this ) ||
    !r.Read( *this ) )
    return false;

    /* Read C's members here */
    return true;
    }
    };


    Note how the derived class C reads its base object parts inside its [font="Courier New"]Read[/font] method. It doesn't directly call the [font="Courier New"]Read[/font] functions of its base classes, but rather, reads them like they were normal members. That is the correct way to read base class object parts. Calling the [font="Courier New"]Read[/font] method of the base classes directly might seem to work, but it bypasses essential book-keeping code (like object-tracking) and hence is not recommended.

    [size="3"]Custom types - Non-intrusive method

    The non-intrusive method uses a traits class[sup][2][/sup] [font="Courier New"]ObjectReader[/font], which is specialized partially[sup][3][/sup] by the user to provide reading support for the required types. The following is the skeleton of the [font="Courier New"]ObjectReader[/font] traits class:

    namespace Daabli
    {
    template struct ObjectReader
    {
    // Must be true in all specializations
    enum { exists = true };
    // Could be true or false as required in specializations
    enum { group = false };

    // Define this function for specializations
    static const bool Read(T &obj, Reader &r);
    };
    }

    The [font="Courier New"]'exists'[/font] enumerator must be set to true in all specializations (Daabli uses this value to check if the specialization exists or not). The '[font="Courier New"]group[/font]' enumerator can be configured as required; if it is set to true, then Daabli expects object descriptions for this type to be enclosed in curly brackets in the input. If it is set to false, then no enclosing curly brackets are expected. The 'Read' function should be defined by the user to read objects of that type.

    Let's look at an example. Suppose we have a Point structure defined in some library:

    struct Point
    {
    int x;
    int y;
    };

    Then we could provide reading support for it by specializing the [font="Courier New"]ObjectReader[/font] traits class like so:

    namespace Daabli
    {
    template <> struct ObjectReader
    {
    enum { exists = true };
    enum { group = false };

    static const bool Read(Point &obj, Reader &r)
    {
    // Read a Point of the form: ,
    return
    r.Read( obj.x ) && r.ReadSeparator() &&
    r.Read( obj.y );
    }
    };
    }

    Since the [font="Courier New"]ObjectReader[/font] template is declared under the Daabli namespace, we put its specialization also under the same. The '[font="Courier New"]exists[/font]' enumerator must be defined as true (as stated before). The '[font="Courier New"]group[/font]' enumerator is defined as false here, because we do not wish the description to be enclosed in curly brackets in the input. The '[font="Courier New"]ReadSeparator[/font]' function is new here; it reads a separator from the input (currently defined as the comma character). Now let's put all this together into a small program and see how it works.
    In main.cpp:

    #include "Daabli.h"
    #include

    struct Point
    {
    int x;
    int y;
    };

    namespace Daabli
    {
    template <> struct ObjectReader
    {
    enum { exists = true };
    enum { group = false };

    static const bool Read(Point &obj, Reader &r)
    {
    // Read a Point of the form: ,
    return
    r.Read( obj.x ) && r.ReadSeparator() &&
    r.Read( obj.y );
    }
    };
    }

    int main(int /*argc*/, char * /*argv*/[])
    {
    Daabli::Reader r;
    if( !r.FromFile( "input.txt" ) )
    return -1;

    Point p;

    // Read the Point
    if( !r.Read( "point", p ) )
    return -1;

    // Display the point
    std::cout << "Point: " << p.x << ", " << p.y << std::endl;

    return 0;
    }

    Given the description in input.txt:

    point = 10, 20;
    The output generated would be:

    [font="Courier New"]Point: 10, 20[/font]

    Simple enough; but what if our Point structure was templated? Would we need to partially specialize [font="Courier New"]ObjectReader[/font] for each instantiation of Point template? No we wouldn't! We could specialize [font="Courier New"]ObjectReader[/font] for all instantiations of Point template. So suppose our Point template looked like this:

    template
    struct Point
    {
    T x;
    T y;
    };

    Then our [font="Courier New"]ObjectReader[/font] specialization to support reading of any Point template instantiation would be:

    namespace Daabli
    {
    template struct ObjectReader >
    {
    enum { exists = true };
    enum { group = false };

    static const bool Read(Point &obj, Reader &r)
    {
    // Read a Point of the form: ,
    return
    r.Read( obj.x ) && r.ReadSeparator() &&
    r.Read( obj.y );
    }
    };
    }

    And then we could read objects of any Point template instantiation:

    int main(int /*argc*/, char * /*argv*/[])
    {
    Daabli::Reader r;
    if( !r.FromFile( "input.txt" ) )
    return -1;

    Point p1;
    Point p2;
    Point p3;

    // Read the points
    if( !r.Read( "point", p1 ) ||
    !r.Read( "point", p2 ) ||
    !r.Read( "point", p3 ) )
    return -1;

    // Display the points
    std::cout << "Point : " << p1.x << ", " << p1.y << std::endl;
    std::cout << "Point : " << p2.x << ", " << p2.y << std::endl;
    std::cout << "Point : " << p3.x << ", " << p3.y << std::endl;

    return 0;
    }

    Given the description in input.txt:

    point = 10, 20;
    point = 10.2, 20.4;
    point = "Hello", "World";

    The output generated would be:

    [font="Courier New"]Point[type]int[/type] : 10, 20
    Point[type]float[/type] : 10.2, 20.4
    Point[type]string[/type] : Hello, World[/font]

    The Point is a somewhat silly example, but you get the idea; reading works recursively. You could have a Point > and it would work without any extra code. In fact, all built-in types in Daabli (basic types, STL containers) are supported via the non-intrusive method. If you look at the source, you'll find that all the built-in types have [font="Courier New"]ObjectReader[/font] specializations.

    [size="5"]Enumerations

    Adding support for loading enumerations (even library ones whose source cannot be modified) is quite easy. Daabli borrows from the article "Stringizing C++ Enums"[sup][1][/sup] to convert enumerators to their string representations and vice versa (though only vice versa is important for loading purposes right now). The steps for adding loading support for enumerations are:
    1. Re-declare the enumeration in a different format for conversion support to/from string (see section "String conversion support").
    2. Declare the enumeration as 'readable' by Daabli.
    For illustration purposes, let's bring back the Furious Five Master enumeration example from the article "Stringizing C++ Enums"[sup][1][/sup]:

    // Furious Five Master
    enum Master
    {
    Tigress = 5,
    Viper = 3,
    Monkey = 4,
    Mantis = 1,
    Crane = 2
    };

    To add reading support, we first add string conversion support (using helper macros):

    // String conversion support
    Daabli_Begin_Enum( Master )
    {
    Daabli_Enum( Tigress );
    Daabli_Enum( Viper );
    Daabli_Enum( Monkey );
    Daabli_Enum( Mantis );
    Daabli_Enum( Crane );
    }
    Daabli_End_Enum;

    And then we simply declare the enumeration as readable by Daabli:

    // Reading support
    Daabli_Readable_Enum( Master );

    And we're done! Now you can read Master enumerations just like any other object:

    [/cint main(int /*argc*/, char * /*argv*/[])
    {
    Daabli::Reader r;
    if( !r.FromFile( "input.txt" ) )
    return -1;

    Master myMaster;

    if( !r.Read( "myMaster", myMaster ) )
    return -1;

    std::cout << "myMaster: " << myMaster << std::endl;

    return 0;
    }

    Given the description in input.txt:

    myMaster = Monkey; The output generated would be:

    [font="Courier New"]myMaster: 4[/font]

    The above program outputs 4 which is the value for the Monkey enumerator. As you might have guessed, enumerations are supported via the non-intrusive method; the macro [font="Courier New"]Daabli_Readable_Enum[/font] expands to an [font="Courier New"]ObjectReader[/font] specialization for the enumeration type.

    [size="5"]Pointers

    In C++, runtime polymorphism is most often associated with dynamic allocation and pointers/references. So itAEs important that weAEre able to work with pointers on an object description level. Daabli has native support for pointers, making it easy to describe and load them.

    In Daabli, a pointer can be described as a reference to an object. To refer to a particular object, the object must be given a name. Names (which must be unique) are assigned to objects by prefixing the object with the name enclosed in square brackets. Once an object is named, pointers which point to it can be described as the name enclosed in square brackets. LetAEs look at a simple example:

    value = [myValue] 10; // Integer object named 'myValue'
    pValue = [myValue]; // Integer pointer, pointing to the object whose name is 'myValue'.

    And some code to read the above data:

    int value;
    int *pValue;

    if( !r.Read( "value", value ) ||
    !r.Read( "pValue", pValue ) )
    return -1;

    std::cout << "&value: " << &value << std::endl; // Outputs address of 'value'
    std::cout << "pValue: " << pValue << std::endl; // Outputs NULL

    if( !r.ResolvePointers() )
    return -1; // Handle error

    std::cout << "pValue: " << pValue << std::endl; // Outputs address of 'value'

    Pointers read using Read are not usable immediately; they are assigned [font="Courier New"]NULL[/font]. The [font="Courier New"]ResolvePointers[/font] function is new here; it sets all recently read pointers to point to the required objects. Why is this required, one might ask? The reason is that a pointer might be referencing an object which has not yet been read and hence cannot be assigned to it as yet. Hence the user should postpone calling [font="Courier New"]ResolvePointers[/font] until he/she is sure that all referenceable objects have been loaded, or until a pointer actually needs to be used.

    To create an object dynamically, specify the type of the object to be created within parenthesis, followed by the object itself. Dynamically created objects can also be named like normal objects, to allow other pointers to point to them. As an example:

    pValue1 = (int) 10; // Pointer to dynamically allocated int
    pValue2 = (int) [myValue] 20; // Pointer to dynamically allocated int, named 'myValue'
    pValue3 = [myValue]; // Pointer to the object whose name is 'myValue'

    And some code to read the above data:

    int *pValue1;
    int *pValue2;
    int *pValue3;

    if( !r.Read( "pValue1", pValue1 ) ||
    !r.Read( "pValue2", pValue2 ) ||
    !r.Read( "pValue3", pValue3 ) )
    return -1;

    // Resolve pointers
    if( !r.ResolvePointers() )
    return -1; // Handle error

    std::cout << "value1: " << (*pValue1) << std::endl; // Outputs 10
    std::cout << "value2: " << (*pValue2) << std::endl; // Outputs 20
    std::cout << "value3: " << (*pValue3) << std::endl; // Outputs 20

    delete pValue1;
    delete pValue2;
    //delete pValue3; // Same as pValue2

    Custom types must be registered with Daabli before pointers to them can be used. This is done using one of the [font="Courier New"]Daabli_Register_Type[_Base(n)][/font] macros which registers the custom type with a modified version of the factory presented in the article "Super Factory"[sup][4][/sup]. This allows Daabli to create the required objects at runtime. For more details on how to use the macros, see "Super Factory"[sup][4][/sup].
    Let's look at an example which demonstrates how to use custom type pointers with Daabli - a list of heterogeneous item objects.

    In main.cpp:

    #include "Daabli.h"
    #include

    We first create an Item class which will be the base class of all items. We also register it with Daabli, so that we can use pointers to it.

    struct Item
    {
    virtual ~Item() =0
    {
    }
    virtual void Display() =0;
    };

    Daabli_Register_Type( Daabli_Abstract, Item );

    Then we create a few items which derive from Item, and have properties of their own. A sword item:

    class Sword : public Item
    {
    std::string _name;
    public:
    virtual void Display()
    {
    std::cout << "Sword: " << _name << std::endl;
    }
    const bool Read(Daabli::Reader &r)
    {
    return r.Read( "name", _name );
    }
    };

    Daabli_Register_Type_Base1( Daabli_Concrete, Sword, Item );

    A healing potion item:

    class HealingPotion : public Item
    {
    int _pointsHealed;
    public:
    virtual void Display()
    {
    std::cout << "Healing Potion: " << _pointsHealed << std::endl;
    }
    const bool Read(Daabli::Reader &r)
    {
    return r.Read( "pointsHealed", _pointsHealed );
    }
    };

    Daabli_Register_Type_Base1( Daabli_Concrete, HealingPotion, Item );

    And an armor item:

    struct Armor : public Item
    {
    float _damageReduction;
    public:
    virtual void Display()
    {
    std::cout << "Armor: " << _damageReduction << std::endl;
    }
    const bool Read(Daabli::Reader &r)
    {
    return r.Read( "damageReduction", _damageReduction );
    }
    };

    Daabli_Register_Type_Base1( Daabli_Concrete, Armor, Item );

    Then in main, we load a list of items from a description:

    int main(int /*argc*/, char * /*argv*/[])
    {
    Daabli::Reader r;
    if( !r.FromFile( "input.txt" ) )
    return -1;

    typedef std::list Items;

    // Read a list of items
    Items items;
    if( !r.Read( "myItems", items ) )
    return -1;

    // Resolve pointers
    if( !r.ResolvePointers() )
    return -1;

    // Display the list of items
    for(Items::const_iterator itr = items.begin(); itr != items.end(); ++itr)
    (*itr)->Display();

    // Delete all the items in the list
    for(Items::const_iterator itr = items.begin(); itr != items.end(); ++itr)
    delete (*itr);

    return 0;
    }

    Given the description in input.txt:

    myItems =
    {
    (HealingPotion) { pointsHealed = 10; },
    (HealingPotion) { pointsHealed = 12; },
    (Sword) { name = "Great Stinging Sword"; },
    (Armor) { damageReduction = 50; }
    };

    The output generated would be:

    [font="Courier New"]Healing Potion: 10
    Healing Potion: 12
    Sword: Great Stinging Sword
    Armor: 50[/font]

    Note that since the Item base class was registered with Daabli as being abstract, and it didn't have any members of it's own to read, we didn't have to define a Read function for it.

    [size="5"]Error Messages

    Sometimes the loading process doesnAEt quite go so smoothly and errors occur. When this happens, DaabliAEs policy is to abort reading at the first error that occurs and return failure (false). Since this is rarely enough information, Daabli also provides an error/warning log with more information for the user to chew over.

    The Log member of the Reader class contains this information and can be examined by the user at any time. The following code demonstrates how this information can be accessed:

    #include "Daabli.h"
    #include

    int main(int /*argc*/, char * /*argv*/[])
    {
    Daabli::Reader r;

    if( !r.FromFile( "input.txt" ) )
    return -1;

    // Read an integer
    int value;
    r.Read( "myValue", value );

    // Display error messages if any
    Daabli::MessageLog::MessageList::const_iterator itr = r.Log.Messages().begin();
    while( itr != r.Log.Messages().end() )
    {
    std::cout << "At line number : " << itr->first.first << std::endl;
    std::cout << "At column number : " << itr->first.second << std::endl;
    std::cout << "Error message : " << itr->second << std::endl;
    std::cout << std::endl;

    ++itr;
    }

    return 0;
    }

    Given the description in input.txt:

    myValue = 10.2;
    The output generated would be:

    [font="Courier New"]At line number : 2
    At column number : 10
    Error message : invalid int value: 10.2

    At line number : 2
    At column number : 10
    Error message : failed to read object[/font]

    Actually, when the Reader object goes out of scope, it dumps all log messages to the standard error output. So even if the user hadn't manually output the contents of the log, the following would have been dumped to the standard error output:

    [font="Courier New"][Ln 2, Col 10] invalid int value: 10.2
    [Ln 2, Col 10] failed to read object[/font]

    Try changing the input and see the error messages which get generated; also experiment with the other examples we've encountered so far.

    [size="3"]Custom error messages

    Apart from being examined, the log can also be written to at any time. Overloaded insertion operators allow the user to write string and character data to the log. This is useful for logging custom error messages when the user writes [font="Courier New"]ObjectReader[/font] specializations for custom types. This is also useful for logging data validation failure errors. Let's look at an example:

    #include "Daabli.h"
    #include

    struct ClownHero
    {
    int _health;
    int _ammo;

    const bool Read(Daabli::Reader &r)
    {
    // Read and validate health
    {
    if( !r.Read( "health", _health ) )
    return false;

    if((_health < 1) || (_health > 100))
    {
    r.Log << "Invalid value for health (1 to 100): "
    << Daabli::ToString(_health) << Daabli::endl;
    return false;
    }
    }

    // Read and validate ammo
    {
    if( !r.Read( "ammo", _ammo ) )
    return false;

    if((_ammo < 0) || (_ammo > 1000))
    {
    r.Log << "Invalid value for ammo (0 to 1000): "
    << Daabli::ToString(_ammo) << Daabli::endl;
    return false;
    }
    }

    return true;
    }
    };

    int main(int /*argc*/, char * /*argv*/[])
    {
    Daabli::Reader r;
    if( !r.FromFile( "input.txt" ) )
    return -1;

    // Read a hero
    ClownHero hero;
    if( !r.Read( "hero", hero ) )
    return -1;

    // Display the hero's properties
    std::cout << "Clown Hero Properties" << std::endl;
    std::cout << "Health: " << hero._health << std::endl;
    std::cout << "Ammo : " << hero._ammo << std::endl;

    return 0;
    }

    Given the description in input.txt:

    hero =
    {
    health = 100;
    ammo = 1500;
    };

    The output generated would be:

    [font="Courier New"][Ln 5, Col 16] Invalid value for ammo (0 to 1000): 1500
    [Ln 5, Col 16] failed to read object[/font]

    String and character data can be streamed to the [font="Courier New"]MessageLog[/font], as well as an endl manipulator which indicates end of line. Other types of data must be converted to string before being streamed to the log. In the above example, [font="Courier New"]_health[/font] and [font="Courier New"]_ammo[/font] were converted to string using the ToString function (see section "String conversion support").

    [size="5"]String conversion support


    Daabli includes a string conversion utility which can be used for converting objects to/from strings. This is handy not only for converting simple objects like ints, floats, doubles etc. to/from string, but is also extensible enough to provide string conversion support for custom types. Objects are converted to/from string using the following functions:
    • [font="Courier New"]const string ToString(const T &val);[/font]
      • Converts an object to its string representation; returns the string.
  • [font="Courier New"]const bool FromString(T &val, const string &str);[/font]
    • Converts a string to an object; returns true if the conversion was successful, false otherwise.
  • The following code snippet demonstrates how to convert a simple integer to/from string:

    const int value = 256;
    std::cout << value << std::endl; // outputs 256

    const std::string valueStr = Daabli::ToString( value );
    std::cout << valueStr << std::endl; // outputs 256

    int newValue = 0;
    Daabli::FromString( newValue, valueStr );
    std::cout << newValue << std::endl; // outputs 256

    Recall from the "Enumerations" section that the first step in providing loading support for an enumeration was to re-declare it for conversion support to/from string. In fact, if only string conversion support is required for an enumeration and not loading support, then only the first step need be used. After that, values of the enumeration can be converted to/from string just like any other basic object. Let's look at an example:

    #include "Daabli.h"
    #include

    // Weekend day
    enum Weekend
    {
    Sunday,
    Saturday
    };

    // String conversion support
    Daabli_Begin_Enum( Weekend )
    {
    Daabli_Enum( Sunday );
    Daabli_Enum( Saturday );
    }
    Daabli_End_Enum;

    int main(int /*argc*/, char * /*argv*/[])
    {
    Weekend day = Sunday;

    // Convert the string "Saturday" into a day
    if( !Daabli::FromString( day, "Saturday" ) )
    return -1;

    // Convert the day back into a string
    const std::string dayStr = Daabli::ToString( day );

    std::cout << dayStr << std::endl; // outputs Saturday

    return 0;
    }

    String conversion support through the common [font="Courier New"]ToString/FromString[/font] interface can be provided for custom types by using a traits class[sup][2][/sup] [font="Courier New"]StringConverter[/font], which is specialized partially[sup][3][/sup] by the user to provide support for the required types. The following is the skeleton of the [font="Courier New"]StringConverter[/font] traits class:

    namespace Daabli
    {
    template struct StringConverter
    {
    // Must be true in all specializations
    enum { exists = true };

    // Define these functions for specializations
    static const std::string ToString(const T &val);
    static const bool FromString(T &val, const std::string &str);
    };
    }

    The '[font="Courier New"]exists[/font]' enumerator must be set to true in all specializations (Daabli uses this value to check if the specialization exists or not). The [font="Courier New"]ToString[/font] and [font="Courier New"]FromString[/font] functions should be defined by the user to convert objects of that type to and from string.

    Let's look at an example. Suppose we have the following Color class:

    #include "Daabli.h"
    #include

    struct Color
    {
    typedef unsigned char Byte;

    Byte _r;
    Byte _g;
    Byte _b;

    const bool IsRed() const { return (_r == 255) && (_g == 0 ) && (_b == 0 ); }
    const bool IsGreen() const { return (_r == 0 ) && (_g == 255) && (_b == 0 ); }
    const bool IsBlue() const { return (_r == 0 ) && (_g == 0 ) && (_b == 255); }

    void Set(const Byte r, const Byte g, const Byte http://public.gamedev.net/public/style_emoticons/
    [/url]<#EMO_DIR#>/cool.gif' class='bbc_emoticon' alt='B)' />
    {
    _r = r; _g = g; _b = b;
    }
    };
    Then we could provide reading support for it by specializing the [font="Courier New"]StringConverter[/font] traits class like so:

    namespace Daabli
    {
    template <> struct StringConverter
    {
    enum { exists = true };

    static const std::string ToString(const Color &val)
    {
    if( val.IsRed() ) return std::string( "Red" );
    if( val.IsGreen() ) return std::string( "Green" );
    if( val.IsBlue() ) return std::string( "Blue" );
    return std::string( "Unknown" );
    }

    static const bool FromString(Color &val, const std::string &str)
    {
    if( str.compare( "Red" ) == 0 ) { val.Set( 255, 0, 0 ); return true; }
    if( str.compare( "Green" ) == 0 ) { val.Set( 0, 255, 0 ); return true; }
    if( str.compare( "Blue" ) == 0 ) { val.Set( 0, 0, 255 ); return true; }
    return false;
    }
    };
    }

    Since the [font="Courier New"]StringConverter[/font] template is declared under the Daabli namespace, we put its specialization also under the same. The '[font="Courier New"]exists[/font]' enumerator must be defined as true (as stated before). For illustration purposes, the [font="Courier New"]ToString[/font] and [font="Courier New"]FromString[/font] functions have been defined to convert color objects to and from some simple color names. Now that this is done, we can convert color objects to and from strings, just like any other basic object:

    int main(int /*argc*/, char * /*argv*/[])
    {
    Color color = { 0, 0, 0 };

    // Convert the string "Red" into a color
    if( !Daabli::FromString( color, "Red" ) )
    return -1;

    // Convert the color back into a string
    const std::string colorStr = Daabli::ToString( color );

    std::cout << colorStr << std::endl; // outputs Red

    return 0;
    }

    Note that although string conversion support is not directly related to deserialization, I thought I'd cover it here anyway as it comes along with Daabli; why let it go to waste.

    [size="5"]Closing

    Apart from the samples presented in this guide, do go through those which are included with the Daabli source code; they demonstrate most of the features of Daabli in a more complete manner. The source code is released under the MIT license[sup][5][/sup] and has been tested on the following compilers:

    StringConverter, which is specialized partially
    • Microsoft Visual C++ 2005/2008
    • GCC 4.3.2
    I hope you enjoyed reading this guide as much as I did writing it, and that Daabli (or at least parts/concepts of it) will be useful to you. There is much potential for improvement; if you make changes to the code, improve it, or have some better ideas, I would love to know. I can be reached by email at francisxavierjp [at] gmail [dot] com. Comments and suggestions are always welcome!

    [size="3"]References

    [1] http://www.gamedev.n...cppstringizing/
    [2] http://en.wikipedia....iki/Trait_class
    [3] http://en.wikipedia...._specialization
    [4] http://www.gamedev.n...res/supFactory/
    [5] http://en.wikipedia....iki/MIT_License
    Cancel Save
    0 Likes 0 Comments

    Comments

    Nobody has left a comment. You can be the first!
    You must log in to join the conversation.
    Don't have a GameDev.net account? Sign up!

    A guide to deserializing objects using Daabli. Demonstrates how easy it is to load objects, enumerations, pointers and STL containers using Daabli. Also discusses Daabli's flexible string conversion mechanism

    Advertisement
    Advertisement