<TRL>

Design

Our goal is to achieve transparent serialization of user defined types. A common way to implement serialization is to add a base class that defines virtual serialize and deserialize functions.

class Serializable
{
public:
   virtual void serialize(ostream& stream) = 0;
   virtual void deserialize(const istream& stream) = 0;
};


class UserClass : public Serializable
{
public:
   virtual void serialize(ostream& stream);
   virtual void deserialize(const istream& stream);

private:
   int m_count;
   std::string m_name;
};

We can now implement the serialize and deserialize function for the user class.

void UserClass::serialize(ostream& stream)
{
   stream << m_count << m_name;
}

void UserClass::deserialize(const istream& stream)
{
   stream >> m_count >> m_name;
}

This kind of serialization isn't transparent, because transparent serialization would require a generic serialize function (one that doesn't have to be rewritten for every user class). To achieve this we need a way to access the members of an object without knowing the number of members and the names of these members. Accessing a member without using its name can be done using a pointer to member. For each member we could add a 'getMemberPtr' function to the user class.

class UserClass
{
public:
   typedef int UserClass::* MemberPtr0;
   static MemberPtr0 getMemberPtr0(void) { return &UserClass::m_count; }
  
   typedef std::string UserClass::* MemberPtr1;
   static MemberPtr1 getMemberPtr1(void) { return &UserClass::m_name; }

private:
   int m_count;
   std::string m_name;
};

Now we could use a templated serialize function:

template <class UserClass_>
void serialize(ostream& stream, UserClass_* ptr)
{
   ostream << ptr->* UserClass_::getMemberPtr0()
           << ptr->* UserClass_::getMemberPtr1();
}

The definition of the user class is quite involving, this could be simplified using preprocessor macros. But there is a major problem, this technique can only be used for a fixed number of data members. To solve this problem we need to have a 'collection' of member pointers. Ofcourse this can't be a classic collection (eg. std::vector), since we would loose the type information. So lets explore the possibilities of a typelist. If we rewrite the 'getMemberPtr' functions so they take an Int2Type as argument we don't have to enumerate the names. We could then add all Int2Types to a typelist.

class UserClass
{
// first member
private:
   int m_count;
   typedef int UserClass::* MemberPtr0;
  
public:
   static MemberPtr0 getMemberPtr(Loki::Int2Type<0>) { return &UserClass::m_count; }

// second member
private:
   std::string m_name;
   typedef std::string UserClass::* MemberPtr1;
  
public:
   static MemberPtr1 getMemberPtr(Loki::Int2Type<1>) { return &UserClass::m_name; }

// type list
public:
   typedef TYPELIST_2(Loki::Int2Type<0>, Loki::Int2Type<1>) MemberIndices;
};

The serialize function will have to use a recursive call to serialize all members.

template <class TList_>
class RecursiveSerialization : public RecursiveSerialization<typename TList_::Tail>
{
public:
   template <class UserClass_>
   static void serialize(ostream& stream, UserClass_* ptr)
   {
      stream << ptr->* UserClass_::getMemberPtr(typename TList_::Head());
      RecursiveSerialization<typename TList_::Tail>::serialize(stream, ptr);
   }
}

template <>
class RecursiveSerialization<Loki::NullType>
{
public:
   template <class UserClass_>
   static void serialize(ostream& stream, UserClass_* ptr)
   {

   }
}

template <class UserClass_>
void serialize(ostream& stream, UserClass_* ptr)
{
   RecursiveSerialization<typename UserClass_::MemberIndices>::serialize(stream, ptr);
}

In the previous code, when serialize is called for the RecursiveSerialization instantiation, it calls serialize for each element in the typelist. This seams to be what we are looking for, lets try to write this using macros. We can make some observations. We need a macro to add a member. Since this macro will need the name of the user class, it would be usefull to pass this name in a seperate macro. Finally we also need a macro to add the type list. Lets name these macros BEGIN_MEMBERS, ADD_MEMBER and END_MEMBERS.

#define BEGIN_MEMBERS(ConcreteClass_)        \
private:                                     \
   typedef ConcreteClass_ ConcreteClass;
  

#define ADD_MEMBER(DataType, name, nb)                      \
private:                                                    \
   DataType name;                                           \
   typedef DataType ConcreteClass::* MemberPtr##nb;         \
public:                                                     \
   static MemberPtr##nb getMemberPtr(Loki::Int2Type<nb>)    \
   {                                                        \
      return &ConcreteClass::name;                          \
   }

The END_MEMBERS macro will need to create the type list typedef. To do this it will need to know the number of each member. If we enforce that the user has to enumerate his members starting with zero (eg. 0, 1, 2, ...) the number of members would suffice. We could then use a recursive template algorithm to create the type list.

#define END_MEMBERS(nb)                                              \
public:                                                              \
   typedef CreateMemberIndices<nb>::Result MemberIndices;


template <class TList_, int nb_>
struct CreateMemberIndicesImpl
{
   typedef Loki::Typelist<Loki::Int2Type<nb_>, TList_> NewTList;
  
   typedef typename CreateMemberIndicesImpl<NewTList, nb_ - 1>::Result Result;
};

template <class TList_>
struct CreateMemberIndicesImpl<TList_, 0>
{
   typedef Loki::Typelist<Int2Type<0>, TList_> Result;
};


template <int nb_>
struct CreateMemberIndices
{
   typedef typename CreateMemberIndicesImpl<Loki::NullType, nb_ - 1>::Result Result;
};

We can now use these macros as follows.

class UserClass
{
   BEGIN_MEMBERS(UserClass)
   ADD_MEMBER(int, m_count, 0)
   ADD_MEMBER(std::string, m_name, 1)
   END_MEMBERS(2)
};

This already is quite simple, but it isn't simple enough. In particular, the user has to pass a number to each ADD_MEMBER call. Also, each time a new member is added to a user class the call to END_MEMBERS needs to be changed. Lets see if we can't simplify this further.

Instead of passing the number to ADD_MEMBER, the macro could use the line number as the unique integer. This means that each ADD_MEMBER call must have its own line, but that isn't a problem. Looking at the previous version of this macro we can see that the nb parameter is also used to create a unique name for the member pointer type. Although we could also use the line number for this, it is easier to use the name (since it also has to be unique).

#define ADD_MEMBER(DataType, name)                                \
private:                                                          \
   DataType name;                                                 \
   typedef DataType ConcreteClass::* MemberPtr##name;             \
public:                                                           \
   static MemberPtr##name getMemberPtr(Loki::Int2Type<__LINE__>)  \
   {                                                              \
      return &ConcreteClass::name;                                \
   }

If we want to use this macro we will need another mechanism to create the type list. Since all ADD_MEMBER macros are enclosed within the BEGIN_MEMBERS and END_MEMBERS calls, we could let them create two enums, startLine and endLine respectively, holding the line number. These could then be passed to the template algorithm instanciated by END_MEMBERS. The new template algorithm looks something like this.

template <class TList_, int startLine_, int endLine_>
struct CreateMemberIndicesImpl
{
   typedef Loki::Typelist<Loki::Int2Type<startLine_>, TList_> NewTList;
  
   typedef typename CreateMemberIndicesImpl< NewTList
                                           , startLine_ + 1
                                           , endLine_ >::Result Result;
};

template <class TList_, int endLine_>
struct CreateMemberIndicesImpl<TList_, endLine_, endLine_>
{
   typedef TList_ Result;
};


template <int startLine_, int endLine_>
struct CreateMemberIndices
{
   typedef typename CreateMemberIndicesImpl< Loki::NullType
                                           , startLine_ + 1
                                           , endLine_ >::Result Result;
};

Using this algorithm, every line between the begin and end members macros must contain an ADD_MEMBER call. This means that no single ADD_MEMBER call may occupy two lines. This isn't flexible enough, so we need a mechanism to check line numbers before adding them to the type list.

Template specialization could do the trick here. If the BEGIN_MEMBERS macro would create a template class IsMemberPresent, taking an integer and defining an enum as false; then ADD_MEMBER could specialize it, defining the enum as true. But since explicit template specialization isn't allowed inside class bodies, we will need to use partial template specialization. Furthermore, if the CreateMemberIndices algorithm wants to access this class it will also need the user class type. These are the new macros:

#define BEGIN_MEMBERS(ConcreteClass_)                                      \
private:                                                                   \
   typedef ConcreteClass_ ConcreteClass;                                   \
   enum { startLine = __LINE__ };                                          \
public:                                                                    \
   template <int lineNb_, class Dummy_ = Loki::NullType>                   \
   struct IsMemberPresent { enum value = false };


#define ADD_MEMBER(DataType, name)                                         \
private:                                                                   \
   DataType name;                                                          \
   typedef DataType ConcreteClass::* MemberPtr##name;                      \
public:                                                                    \
   template <class Dummy_>                                                 \
   struct IsMemberPresent<__LINE__, Dummy_> { enum value = true };         \
   static MemberPtr##name getMemberPtr(Loki::Int2Type<__LINE__>)           \
   {                                                                       \
      return &ConcreteClass::name;                                         \
   }


#define END_MEMBERS()                                                      \
private:                                                                   \
   enum { endLine = __LINE__ };                                            \
public:                                                                    \
   typedef CreateMemberIndices<startLine, endLine, ConcreteClass>::Result  \
           MemberIndices;

The following code shows the altered CreateMemberIndices.

template <class TList_, int startLine_, int endLine_, class ConcreteClass_>
struct CreateMemberIndicesImpl
{
   enum { isMemberPresent = ConcreteClass_::IsMemberPresent<startLine_>::value };
  
   typedef typename Loki::Select< isMemberPresent
                                , Loki::Typelist<Loki::Int2Type<startLine_>, TList_>
                                , TList_ >::Result NewTList;
  
   typedef typename CreateMemberIndicesImpl< NewTList
                                           , startLine_ + 1
                                           , endLine_
                                           , ConcreteClass_ >::Result Result;
};

template <class TList_, int endLine_, class ConcreteClass_>
struct CreateMemberIndicesImpl<TList_, endLine_, endLine_, ConcreteClass_>
{
   typedef TList_ Result;
};


template <int startLine_, int endLine_, class ConcreteClass_>
struct CreateMemberIndices
{
   typedef typename CreateMemberIndicesImpl< Loki::NullType
                                           , startLine_ + 1
                                           , endLine_
                                           , ConcreteClass_ >::Result Result;
};

And here is the new user code:

class UserClass
{
   BEGIN_MEMBERS(UserClass)
   ADD_MEMBER(int, m_count)
   ADD_MEMBER(std::string, m_name)
   END_MEMBERS()
};