Hi,
I decided to use some of the techniques explained in 'Modern C++ Design' by
Andrei Alexandrescu to create a very simple heterogeneous container. Before
reading this book I never considered how this type of container might be
implemented. Some input from the group would be appreciated and I hope this
posting is a learning experience for anyone not familiar with C++
meta-programming. I learned a lot from actually using the techniques described
by Andrei.
To implement the container I added data to the Typelist template that allowed
it to be useful when instantiated:
//This is a typelist as explained in 'Modern C++ Design' with some extra data
template <class T, class U> struct Typelist {
typedef T Head;
typedef U Tail;
T data;
U NextTypeData;
};
NextTypeData is the type of the next typelist. Each typelist has its own type
and data it holds. The data contained in each Typelist object is of type T and
the instance of that type is T data declared in the Typelist template.
As explained in by Andrei in 'Modern C++ Design' the typelist are created by
nesting the declarations inside each other:
typedef
Typelist<int,Typelist<string,Typelist<vector<string>,Typelist<vector<double>,NullType>
> > > HeteroCont;
The NullType is used to break the recursive unfolding of the typelist. The list
is unfolded until the NullType specialization for the Typelist is reached.
class NullType { };
In order for Typelist to be a container it must be instantiated. The datatypes
declared in the template class will be created and they are of the types
specified in the declaration:
typedef
Typelist<int,Typelist<string,Typelist<vector<string>,Typelist<vector<double>,NullType>
> > > HeteroCont;
HeteroCont MyTypes;
Each Typelist object created will contain the data object of the type it's
associated with in the declaration. This is the variable called 'data' of
parameter type T.
I guess the most confusing aspect of this implementation is understanding how
to manipulate the data in the container. It's not possible to access the data
of a specific type without expanding the container until you reach the type
you're targeting. After you reach the type, the object of that type is
available in the Typelist object. I unfolded the type using recursive calls to
a template function.
The template function ValueAt(int index,R& r, T& arg) is called recursively
until the parameter index reaches 0 or the Typelist object unfolds into the
NullType type parameter. The NullType template parameter causes the
specialization for NullType to be called and the recursion ends with a return
of 0.
In addition to the convertibility checking template classes Andrei described
and implemented, I created a template class called DoAssign to help with the
assignment operations. The NextDataType object inside of the Typelist template
is of parameter type T of the Typelist template, which is another Typelist
containing the next type in the Typelist (original list of types). Each
recursive call to ValueAt expands to the type of next type in the list because
NextDataType is of the next type in the typelist chain.
template<typename T, typename R> int ValueAt(int index,R& r, T& arg) {
const int cv=Conversion<typename T::Head,R>::exist;
if(!index) {
DoAssign<R,typename T::Head,cv> assign(r,arg.data);
return cv ;
}
return ValueAt((index-1),r,arg.NextTypeData);
}
This is the specialization for the NullType type that ends the recursion and
returns 0.
template<typename R>int ValueAt(int , R& , NullType&) {
cerr << " NullType version called for ValueAt template function " <<
endl ;
return 0;
}
I implemented a template class to help with assignment operations. To set a
value of an object, get a reference to an object, or fetch a value from an
object in the container an assignment operation is necessary. This is not
possible if the objects are not convertible and even if they are convertible
the compiler iterates through all types until the target is reached so, the
assignment statement used to perform the needed operations more than likely
will not always be valid to the compiler. So, the template class DoAssign takes
care of this problem by having a specialization that does not perform the
assignment if the types are not convertible. DoAssign performs the assignment
based on the result of the conversion checking template class described in
'Modern C++ Design'. See ValueAt for an example of how I used DoAssign to
perform the assignment. The template class DoAssign keeps the invalid
assignment attempts out of the code.
template <class D, class T, unsigned int flag> class DoAssign {
public:
DoAssign(D& dest, T& target) { dest=target; }
};
This is the specialization of DoAssign that's used for types that are not
convertible.
template <class D, class T> class DoAssign<D,T,0> {
public:
DoAssign(D& , T& ) { cerr << " Error: conversion not possible" << endl
; }
};
I also implemented SetValueAt and GetValueAt template functions that use the
same techniques as ValueAt in their definitions. GetValueAt uses another
version of template class DoAssign called PtrDoAssign. PtrDoAssign does what
DoAssign does, but works with pointers in order to get a reference to objects
in the container. Here are the definitions:
template <class D, class T, unsigned int flag> class PtrDoAssign {
public:
PtrDoAssign(D** dest, T* target) { *dest=target; }
};
This is the specialization for types that are not convertible.
template <class D, class T> class PtrDoAssign<D,T,0> {
public:
PtrDoAssign(D** , T* ) {
cerr << " Error: pointer conversion not possible" << endl ;
}
};
template<typename V> int GetItemAt(const int , V** , NullType ) {
cerr << " GetItemAt NullType called " << endl ;
return 0;
}
template<typename V, typename T> int GetItemAt(const int index, V** ref, T&
arg) {
const int cv=Conversion<typename T::Head*,V*>::exist;
if(!index) {
PtrDoAssign<V,typename T::Head,cv> assign(ref,&arg.data);
return cv;
}
return GetItemAt((index-1),ref, arg.NextTypeData);
}
template<typename V, typename T> int SetValueAt(const int index, V& value, T&
arg) {
const int cv=Conversion<typename T::Head,V>::exist;
if(!index) {
DoAssign<typename T::Head,V,cv> assign(arg.data,value);
return cv;
}
return SetValueAt((index-1),value, arg.NextTypeData);
}
Best Regards,
James Smith