Templatized Visitor Pattern

Michael Smit


Table of Contents

License
What's the problem?
Serialization
Handling Structures
Handling 3rd Party Data Types
Generalizing the pattern

License

This document is licensed under a Creative Commons license.

What's the problem?

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:

  1. Binary serialization of an arbitrary data type to a particular byte-order scheme.

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

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.

Handling Structures

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 ISerializables 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);
	};

Handling 3rd Party Data Types

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);
		....
	};

Generalizing the pattern

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);
	...
};