Jump to content

  • Log In with Google      Sign In   
  • Create Account

FREE SOFTWARE GIVEAWAY

We have 4 x Pro Licences (valued at $59 each) for 2d modular animation software Spriter to give away in this Thursday's GDNet Direct email newsletter.


Read more in this forum topic or make sure you're signed up (from the right-hand sidebar on the homepage) and read Thursday's newsletter to get in the running!


Like
2Likes
Dislike

A Simple C++ Object Loader

By Francis Xavier | Published Oct 06 2009 12:19 AM in General Programming

objects code object daabli bool load support const read
If you find this article contains errors or problems rendering it unreadable (missing images or files, mangled code, improper text formatting, etc) please contact the editor so corrections can be made. Thank you for helping us improve this resource

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: string, vector, list, deque, pair, map, multimap, set, multiset
  • supports loading enumerations[1]
  • 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.

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.

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

If the path where you've placed the downloaded code is INSTALLPATH, then you need to add the folder INSTALLPATH/Daabli/ to the list of source code folders which your development environment searches. Also, you must add the file INSTALLPATH/Daabli/Daabli.cpp 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.

Reading objects

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

Basic types

Daabli has built-in support for loading the following basic types: bool, char, int, unsigned int, long, unsigned long, float, double.

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 true or false. For e.g., 15 could describe an int, 3.14159 could describe a double, true could describe a bool, and 65 or 'A' could describe a char.

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 <iostream>

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:

type : A
maxSpeed : 10.2
mass : 50.7
invulnerable : 0


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

STL containers

Daabli has built-in support for loading the following STL containers: string, vector, list, deque, pair, map, multimap, set, multiset.

All the containers (except for string) 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 string, {1, 2, 3} could describe a vector<int>, {"Hello", "World"} could describe a pair<string, string>, and {{"One", 1}, {"Two",2}} could describe a map<string, int>.

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

In main.cpp:

#include "Daabli.h"
#include <iostream>

typedef std::map<int, float> LevelPriceMap;
typedef std::vector<float> Row;
typedef std::vector<Row>   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][i] << " ";

            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:

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


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

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 Read 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.

Custom types - Intrusive method

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

const bool Read(Daabli::Reader &r) const;

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 <iostream>

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<SpaceShip> ships;

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

    // Display the list of space ships
    for(std::list<SpaceShip>::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:

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


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 Read 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<A>( *this ) ||
            !r.Read<B>( *this ) )
            return false;

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

Note how the derived class C reads its base object parts inside its Read method. It doesn’t directly call the Read 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 Read 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.

Custom types - Non-intrusive method

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

namespace Daabli
{
    template <class T> 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 'exists' enumerator must be set to true in all specializations (Daabli uses this value to check if the specialization exists or not). The 'group' 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 ObjectReader traits class like so:

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

        static const bool Read(Point &obj, Reader &r)
        {
            // Read a Point of the form: <x>, <y>
            return
                r.Read( obj.x ) && r.ReadSeparator() &&
                r.Read( obj.y );
        }
    };
}
Since the ObjectReader template is declared under the Daabli namespace, we put its specialization also under the same. The 'exists' enumerator must be defined as true (as stated before). The 'group' enumerator is defined as false here, because we do not wish the description to be enclosed in curly brackets in the input. The 'ReadSeparator' 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 <iostream>

struct Point
{
    int x;
    int y;
};

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

        static const bool Read(Point &obj, Reader &r)
        {
            // Read a Point of the form: <x>, <y>
            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:

Point: 10, 20

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

template <class T>
struct Point
{
    T   x;
    T   y;
};
Then our ObjectReader specialization to support reading of any Point template instantiation would be:

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

        static const bool Read(Point<T> &obj, Reader &r)
        {
            // Read a Point of the form: <x>, <y>
            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<int>          p1;
    Point<float>        p2;
    Point<std::string>  p3;

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

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

    return 0;
}
Given the description in input.txt:

point<int>    = 10, 20;
point<float>  = 10.2, 20.4;
point<string> = "Hello", "World";
The output generated would be:

Point<int> : 10, 20
Point<float> : 10.2, 20.4
Point<string> : Hello, World


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 ObjectReader specializations.

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"[1] 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:
  • Re-declare the enumeration in a different format for conversion support to/from string (see section “String conversion support”).
  • 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"[1]:

// 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:

myMaster: 4

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 Daabli_Readable_Enum expands to an ObjectReader specialization for the enumeration type.

Pointers

In C++, runtime polymorphism is most often associated with dynamic allocation and pointers/references. So itÆs important that weÆre 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. LetÆs 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 NULL. The ResolvePointers 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 ResolvePointers 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 Daabli_Register_Type[_Base(n)] macros which registers the custom type with a modified version of the factory presented in the article “Super Factory”[4]. This allows Daabli to create the required objects at runtime. For more details on how to use the macros, see “Super Factory”[4].
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 <iostream>

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<Item*> 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:

Healing Potion: 10
Healing Potion: 12
Sword: Great Stinging Sword
Armor: 50


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.

Error Messages

Sometimes the loading process doesnÆt quite go so smoothly and errors occur. When this happens, DaabliÆs 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:

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


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:

[Ln 2, Col 10] invalid int value: 10.2
[Ln 2, Col 10] failed to read object


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

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 ObjectReader specializations for custom types. This is also useful for logging data validation failure errors. Let’s look at an example:

#include "Daabli.h"
#include <iostream>

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:

[Ln 5, Col 16] Invalid value for ammo (0 to 1000): 1500
[Ln 5, Col 16] failed to read object


String and character data can be streamed to the MessageLog, 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, _health and _ammo were converted to string using the ToString function (see section “String conversion support”).

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:
  • const string ToString(const T &val);
    • Converts an object to its string representation; returns the string.
  • const bool FromString(T &val, const string &str);
    • 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 <iostream>

// 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 ToString/FromString interface can be provided for custom types by using a traits class[2] StringConverter, which is specialized partially[3] by the user to provide support for the required types. The following is the skeleton of the StringConverter traits class:

namespace Daabli
{
    template <class T> 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 'exists' enumerator must be set to true in all specializations (Daabli uses this value to check if the specialization exists or not). The ToString and FromString 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 <iostream>

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 <img src='[url="[url="http://public.gamedev.net/public/style_emoticons/"]http://public.gamedev.net/public/style_emoticons/[/url]"][url="http://public.gamedev.net/public/style_emoticons/"]http://public.gamedev.net/public/style_emoticons/[/url][/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 StringConverter traits class like so:

namespace Daabli
{
    template <> struct StringConverter<Color>
    {
        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 StringConverter template is declared under the Daabli namespace, we put its specialization also under the same. The 'exists' enumerator must be defined as true (as stated before). For illustration purposes, the ToString and FromString 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.

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[5] 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!

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





Comments

Note: Please offer only positive, constructive comments - we are looking to promote a positive atmosphere where collaboration is valued above all else.




PARTNERS