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