Inheritance With Archivist
Posted by Patrick on Monday, February 07, 2011Mon, Feb 07, 2011

Two weeks ago I started a series of introductory posts on Archivist, a library for simple serialization in C++. I wrote Archivist for my game, Swivel, which is now in the App Store.

Today I'd like to show you how to deal with basic inheritance with Archivist. We'll get into polymorphism in a future post.

Let's extend our first example with some inheritance. Let's say our monster can have adorable little baby monsters that get bigger with time. It might look like this:

class BabyMonster : public Monster
{
private:
    float age;

public:
    ArchiveAttributes( age );

    BabyMonster( void )
    : Monster()
    {
        age = 1.0f;
    }
};

Now let's serialize it to disk:

BabyMonster babyMonster;
Archive::Save( "babymonster.plist", babyMonster.Encode() );

Only, that doesn't quite work. If you open the file in Property List Editor, you'll notice only the age member gets encoded. That's because the Encode() method created by ArchiveAttributes() can't possibly know about parent classes that might or might not be archivable themselves anyway.

This is important because Archivist takes the approach of least interference. It does not require you to inherit from some “Archivable” base class. Besides polluting your heirarchy, this gets real messy real fast in practice when you have multiple inheritance.

So how do we get BabyMonster to serialize it's super (base) class attributes? Fortunately there is a hook that gets called when the object gets encoded, and we can use it like so:

class BabyMonster : public Monster
{
    //...

public:
    ArchiveAttributes( age );

    EncodeAttributes()
    {
        EncodeSuper( Monster );
    }

    //...
}

Astute C++ hackers will immediately wonder how that hook gets called under the hood since, after all, with no inheritance there is no virtual method. How does Archivist call a method that may or may not be declared? Well, my friend, you'll have to look at the code. It's a particularly nasty piece of hackery involving templates, but it does work.

Along with EncodeAttributes() and EncodeSuper() there is a DecodeAttributes() hook for decoding and a DecodeSuper() call for decoding the super class. You can have multiple calls to EncodeSuper() and DecodeSuper() to deal with multiple super classes.

Okay, so BabyMonster is serializable. Obviously there's a little macro magic happening here. Let me pull back the curtain and show you the expanded, preprocessed code produced. I think it will be informative.

The ArchiveAttributes() macro turns into:

virtual void Encode( Archivist::Object & object ) const 
{ 
    const char * names[] = { "age", __null }; 
    Archivist::GetProxy( age ).Encode( object, names ); 
    EncodeNotification( *this, object ); 
} 

virtual Archivist::Object Encode( void ) const 
{ 
    Archivist::Object object; 
    Encode( object ); 
    return object; 
}

virtual void Decode( const Archivist::Object & object ) 
{ 
    const char * names[] = { "age", __null }; 
    Archivist::GetProxy( age ).Decode( object, names ); 
    DecodeNotification( *this, object ); 
}

It is still surprisingly readable I think. Some wonderfully twisted macro hacking happens to turn the attributes you pass in (age, in this case) into a list of parameters as well as a list of string keys. These get passed to the classic Object type which, as I mentioned in the last article, is simply a map of string keys to variant types. The Decode() method is very similar.

Notice the EncodeNotification() and DecodeNotification() calls though. That is where the call to our hooks happen. It passes a reference to both the object being encoded or decoded and the Archivist::Object container.

So what do the hooks look like? Those macros expand to:

void OnEncode( Archivist::Object & object ) const
{
    Monster::Encode( object );
}

void OnDecode( const Archivist::Object & object )
{
    Monster::Decode( object );
}

They're really simple. EncodeAttributes() turns into the OnEncode() method and the EncodeSuper() call turns into the Monster::Encode() call.

Right now the only namespace pollution Archivist creates is these four methods on your serializable classes: Encode(), Decode(), OnEncode() and OnDecode(). And even then, method signatures should take care of any clashes. A small price to pay in my book.

That's it for this week. You can get Archivist from GitHub here: https://github.com/pbhogan/Archivist

A final note: Archivist strives to be very simple to use, but to do so it has to pull out all the stops under the hood and I learned several really neat, powerful and ugly hacks in the process — stuff I had no idea was possible in C++. If you're interested in that sort of thing, read through the source. Otherwise, stay tuned! No doubt I'll introduce some of those tricks in future posts. :)

This post is part of iDevBlogADay, a group of blogs by indie iPhone developers featuring two posts per day. You can subscribe to iDevBlogADay through RSS or follow the #iDevBlogADay hash tag or @idevblogaday on Twitter.