Table of Contents
This document is licensed under a Creative Commons license.
C++ lacks introspection. There are a number of reasons this can be a problem and in my particular case I was trying to address two issues:
Binary serialization of an arbitrary data type to a particular byte-order scheme.
Automatic human-readable text description of an arbitrary data type.
There are a number of frameworks that already address this problem, but I happen to work in a real-time environment so we end up re-writing a lot of stuff anyway and there are some similarities between the two tasks that make for a nice solution. First lets describe the basic idea as it applies to serialization.
A basic solution to the serialization problem is essentially the
visitor pattern. When working purely with objects, this involves two
abstract classes:
ISerializable
and IArchive
.
IArchive
is the visitor and ISerializable
is the acceptor.
class IArchive { public: virtual ~IArchive(); //This may either put a value into the field //or take the value out virtual void op(int &value) = 0; virtual void op(unsigned int &value) = 0; //etc //Structured data type virtual void op(ISerializable &value) = 0; }; class ISerializable { public: virtual ~ISerializable(); virtual void accept(IArchive &archive); };
ISerializable
accepts IArchive
and the reports each sub-item via the op
method.
This is great when all you want to serialize are objects and all
those objects implement ISerializable
, but what about
structures?
Assuming the structures have an accept
method
then all we need to do is create a template.
class IArchive { public: virtual ~IArchive(); template<class Type> void op(Type &value); //This may either put a value into the field //or take the value out virtual void _op(int &value) = 0; virtual void _op(unsigned int &value) = 0; //etc //Structured data type virtual void _op(ISerializable &value) = 0; }; //specialize int, unsigned int, and ISerializable template<> void op(int &value); template<> void op(unsigned int &value); template<> void op(ISerializable &value);
We ints, unsigned ints, and ISerializable
s the
same way as before. Assuming anything else is a structure/object with an
accept
method then all we have to do it as an ISerializable
and pass it to the _op
method.
template<class Type> class SerializableWrapper:public SerializableWrapper { public: SerializableWrapper(Type *wrapValue); virtual void accept(IArchive &archive); private: Type *wrapValue; }; template<class Type> SerializableWrapper::SerializableWrapper(Type *wrapValue) { this->wrapValue = wrapValue; }; /*Assuming that Type has an accept method*/ template<class Type> SerializableWrapper::accept(IArchive &archive) { wrapValue->accept(archive); }; /*Use this class to handle the generic case for IArchive*/ template<class Type> void IArchive::op(Type &value) { SerializableWrapper<Type> wrapper(&value); _op(wrapper); };
This works pretty well except it still assumes the data type being
serialized has an accept
method. To handle this
we refactor SerializableWrapper::accept
to
call a template function which by default calls accept
,
but can be specialized to handle particular cases.
template<class Type> void serializeType(IArchive &archive, Type &value) { value.accept(archive); }; /*Assuming that Type has an accept method*/ template<class Type> SerializableWrapper::accept(IArchive &archive) { serializeType(archive, *wrapValue); };
Now to handle a generic structure you do the following:
template<> void serializeType(IArchive &archive, MyStructure &str) { archive.op(str.a); archive.op(atr.b); .... };
The problem is we are somewhat restricted in terms of what we can put into the serialization. We don't have names for the various types, so it would be difficult to do a nice XML serialization. We also don't have the ability to specify bit fields. If you want to create something like an IP header this might be a bit annoying. Finally, what if I want something else like long descriptions for each subtype?
I could add each of the attributes as arguments to the _op
methods, but that would lead to some other problems. First of all I would
have to provide this information for all data types that support the framework
even though only some would use them. Second, every archive class would
have to figure out what to ignore. As a result not only would everyone have
to do a lot more typing, it would now become unclear what objects support
which features
The solution, as always, is another template.
template<class Information> class AbstractVisitor { public: virtual ~AbstractVisitor(); template<class Type> void op(Type &value, const Information &info); ... }; template<class Information> class IVisitable { public: virtual ~IVisitable(); virtual void accept(AbstractVisitor &visitor) = 0; }; template<class Visitor, class Type> void visitorTypeBehavior(Visitor &visitor, Type &value) { value.accept(visitor); }
I leave the implementation details up to you, but let's see some examples of what I can do. First of all, defining a new visitor/visitable pair is easy:
typedef AbstractVisitor<DocumentationInfo> AbstractDocumentationVisitor; typedef IVisitable<DocumentationInfo> IDocumentable; typedef AbstractVisitor<SerializationInfo> AbstractSerializationVisitor; typedef IVisitable<SerializationInfo> ISerializable;
Second, I can now support only the information I want for a particular type.
/*only support documentation*/ template<> visitorTypeBehavior(AbstractDocumentationVisitor &visitor, MyDocumentable &doc) { ... }; /*support both types of visitor*/ template<> visitorTypeBehavior(AbstractDocumentationVisitor &visitor, MyVersatile &v) { ... }; template<> visitorTypeBehavior(AbstractSerializationVisitor &visitor, MyVersatile &v) { ... }; /*support both types right in the struct*/ struct MyStruct { void accept(AbstractDocumentationVisitor &visitor); void accept(AbstractSerializationVisitor &visitor); ... };