Let's delve further into source code to understand how it works. If we look back at the point where TypeList is created (cmdCreator::createCmd) we actually have all information that we need: R, T1, T2, etc. we just need some way to convert TypeList<R, TypeList<T1, TypeList<T2, ....> > > to R (*fptr)(T1, T2, ...);
The helper classes I use to do this conversion I call TypeConv (TypeConv1, TypeConv2, etc. for the number of supported types).
I'll start from the simplest specialization for terminal classes, because it shows very clearly the idea. The cass has only one typedef, that's all:
template <typename T>
struct TypeConv< TypeList<T, NullType> > {
typedef T (*fp)(void);
};
Functions with one parameter looks similar:
template <typename T, typename Tp>
struct TypeConv1< Tp, TypeList<T, NullType> > {
typedef Tp (*fp)(T);
};
Now let's examine generic version of TypeConv
template <class T, class U>
struct TypeConv<TypeList<T, U> > {
typedef typename TypeConv1< T,
TypeList<typename U::Head, typename U::Tail>
> ::fp fp;
};
This code basically creates a typedef in a recursive way. The first parameter of TypeConv1 is a Head type of TypeList and the second is Tail of TypeConv's template parameter U, which was reformatted as TypeList<typename U::Head, typename U::Tail>. Here is TypeConv1 class:
template <class T, class U, typename Tp>
struct TypeConv1<Tp, TypeList<T, U> > {
typedef typename TypeConv2<
Tp,
T,
TypeList<typename U::Head, typename U::Tail>
> ::fp fp;
};
This code calls TypeConv2 (which looks similar). If, for example, U were the NullType , then terminal version specialization would be used. Otherwise, the generic approach is used and recursion continues until U would be NullType.
Having implemented those classes, I can add them with just two lines into the TypeList class:
template <class T, class U>
struct TypeList: public CmdBase {
typedef T Head;
typedef U Tail;
typedef TypeList<T, U> MyType;
// 2 lines :-)
typedef typename TypeConv< MyType >::fp myfp;
myfp m_fptr_;
TypeList(std::string name, myfp fptr):CmdBase(name), m_fptr_(fptr) {}
//...
};
That's it!
I certainly could add another template parameter for the type of the function, but I wanted to show how we can use template metaprogramming to achieve the same result using information that we already have. This "type conversion" can be used for other different purposes.
In the same way, I can add functionality to automatically print usage information for the function. One possible implementation could look like this:
const std::string getUsage()
{
std::string str;
std::string rv = Type2String<Head>::name();
str.append("RV: ").append(rv); str.append(" ").append(getName()).append(" ");
str.append( MyType::get_usage() ); str.append(" ");
return str;
}
static std::string get_usage()
{
std::string p = Type2String<Tail::Head>::name();
p.append(" ");
p.append( Tail::get_usage() );
return p;
}
I used the same approach here: recursive calls for Tail type. Type2String is just a simple class that returns a string name for a type. I did not show it here because its implementation is trivial, but you can see everything in the accompanying code.
To sum up, I'll provide here is full code for the TypeList class:
template <class T, class U>
struct TypeList: public CmdBase
{
typedef T Head;
typedef U Tail;
typedef TypeList<T, U> MyType;
enum { myIndex = NumEl< TypeList<T, U> >::value };
typedef typename TypeConv< MyType >::fp myfp;
myfp m_fptr_;
TypeList(std::string name, myfp fptr):CmdBase(name), m_fptr_(fptr) {}
bool check(const std::vector< Variant >& params)
{
if(params.size() < myIndex)
return false;
return checkParams(params, 0);
}
bool static checkParams(const std::vector< Variant >& params, int offset = 0)
{
if(!params[offset].is<Tail::Head>())
return false;
return Tail::checkParams(params, offset+1);
}
static std::string get_usage()
{
std::string p = dconsole::BaseCmd::Type2String<Tail::Head>::name();
p.append(" ");
p.append( Tail::get_usage() );
return p;
}
const std::string getUsage()
{
std::string str;
std::string rv = dconsole::BaseCmd::Type2String<Head>::name();
str.append("RV: ").append(rv); str.append(" ").append(getName()).append(" ");
str.append( MyType::get_usage() ); str.append(" ");
return str;
}
virtual void run(const std::vector<Variant>& params)
{
if( check(params) )
executor<myfp, myIndex>::run(m_fptr_, params);
}
};
Further improvements
What should be done with the return value? It's up to you. I just print it into my debug console output, so the user will see the result of an executed command. If you try to use the output value, recall that it might be void.
Another thing you can add is a compile-time check for function parameters to be of a fundamental class, if you would like to constrain them.
Conclusion
By providing type extraction for functions, I now have a flexible mechanism to inspect function type parameters for different needs.
The code provided in this article was applied to create a debug console (like many games have) and can be found here: http://dconsole.googlecode.com.
Sergii Biloshytski started as an embedded programmer, but changed course and became a game programmer. At Ubisoft, he's been working on such titles as Blazing Angels, From DUST, Assassins Creed, and others.


