Recently I stumbled over an exercise in the very interesting book C++ Template Metaprogramming. The challenge of the exercise is to make something like the following compile and do what you expect:

cout << PrintType<int const* const**&>() << endl;

Note that we cannot use typeid, as typeid(T).name() is not required to print anything meaningful, and in practice doesn’t. As long as we don’t support function pointers, the problem from above can be solved rather easily using template specializations, although it requires quite some typing:

#ifndef HH_PRINT_TYPE #define HH_PRINT_TYPE #include <ostream> #include <boost/tr1/type_traits.hpp> #include <boost/lexical_cast.hpp> namespace mine { template<class T_> struct PrintType { // Never used directly. }; /** * For plain types we don't know. * * Could be enhanced by using type_traits. */ template<class T_> std::ostream& operator<<(std::ostream& os, PrintType<T_>) { os << "T"; return os; }; /** * For void. */ template<> std::ostream& operator<<(std::ostream& os, PrintType<void>) { os << "void"; return os; }; /** * For int. */ template<> std::ostream& operator<<(std::ostream& os, PrintType<int>) { os << "int"; return os; }; /** * For char. */ template<> std::ostream& operator<<(std::ostream& os, PrintType<char>) { os << "char"; return os; }; /** * For const qualified types. */ template<class T_> std::ostream& operator<<(std::ostream& os, PrintType<T_ const>) { os << PrintType<T_>() << " const"; return os; }; /** * For volatile qualified types. */ template<class T_> std::ostream& operator<<(std::ostream& os, PrintType<T_ volatile>) { os << PrintType<T_>() << " volatile"; return os; }; /** * For const volatile qualified types. */ template<class T_> std::ostream& operator<<(std::ostream& os, PrintType<T_ const volatile>) { os << PrintType<T_>() << " const volatile"; return os; }; /** * For reference types. */ template<class T_> std::ostream& operator<<(std::ostream& os, PrintType<T_&>) { os << PrintType<T_>() << "&"; return os; }; /** * For pointers. */ template<class T_> std::ostream& operator<<(std::ostream& os, PrintType<T_*>) { os << PrintType<T_>() << "*"; return os; } /** * For pointers to members. */ template<class ClassT_, class MemberT_> std::ostream& operator<<(std::ostream& os, PrintType<MemberT_ ClassT_::*>) { os << PrintType<MemberT_>() << " " << PrintType<ClassT_>() << "::*"; return os; } /** * Helper function for array types. */ template<std::size_t SizeP_, class T_> std::string printDimensions(PrintType<T_[SizeP_]>) { return "[" + boost::lexical_cast<std::string>(SizeP_) + "]" + printDimensions(PrintType<T_>()); } /** * Ends the recursion started by printDimensions(PrintType<T_[SizeP_]>). */ template<class T_> std::string printDimensions(PrintType<T_>) { return ""; } /** * For arrays. */ template<std::size_t SizeP_, class T_> std::ostream& operator<<(std::ostream& os, PrintType<T_[SizeP_]>) { os << PrintType<typename std::tr1::remove_all_extents<T_>::type>() << printDimensions(PrintType<T_[SizeP_]>()); return os; } /** * For const arrays. */ template<std::size_t SizeP_, class T_> std::ostream& operator<<(std::ostream& os, PrintType<const T_[SizeP_]>) { os << "const " << PrintType<T_[SizeP_]>(); return os; } /** * For volatile arrays. */ template<std::size_t SizeP_, class T_> std::ostream& operator<<(std::ostream& os, PrintType<volatile T_[SizeP_]>) { os << "volatile " << PrintType<T_[SizeP_]>(); return os; } /** * For const volatile arrays. */ template<std::size_t SizeP_, class T_> std::ostream& operator<<( std::ostream& os, PrintType<const volatile T_[SizeP_]>) { os << "const volatile " << PrintType<T_[SizeP_]>(); return os; } } #endif

Of course, we are still missing quite a few full specializations but adding them is trivial. In fact, the only part of the code above that is slightly tricky is the part between line 120 and 147, that supports pretty-printing of potentially multi dimensional arrays. Also note the explicit overloads for const volatile at lines 80 and 172. Without them we would get ambiguity errors if trying to pretty print a type that is qualified as such.

While this is all very nice and impressive, the code above still fails us miserably if we do something like

cout << mine::PrintType<int(int, char, void*)>() << endl;

or even

cout << mine::PrintType<int(SomeClass::*)(int, char, void*, ...)>() << endl;

as we didn’t yet add support for function pointers. We can of course fix this by adding more specializations, but as functions can take a nearly arbitrary number of arguments (the C++0x Final Committee Draft suggests implementations to support at least 256), doing this in C++03 would be a rather unpleasant experience. Using C++0x variadic templates on the other hand side, supporting function types is not a big deal (as soon as one understands how variadic templates work ;-)):

#ifdef __GXX_EXPERIMENTAL_CXX0X__ //<-- set by gcc for '-std=c++0x'. /** * Helper struct for printing parameter lists. * * @tparam StartsP_ * true iff we are at the start of the list (needed to omit * commas in the output). * @tparam ArgsT_ * the actual arguments. */ template<bool StartsP_, class... ArgsT_> struct PrintParameters { // Never used directly. }; /** * For parameter lists. */ template<bool StartsP_, class ArgT, class... ArgsT_> std::ostream& operator<<( std::ostream& os, PrintParameters<StartsP_, ArgT, ArgsT_...>) { os << (StartsP_ ? "" : ", ") << PrintType<ArgT>() << PrintParameters<false, ArgsT_...>(); return os; } /** * Terminates the recursion. */ template<bool StartsP_> std::ostream& operator<<(std::ostream& os, PrintParameters<StartsP_>) { return os; } /** * For function pointers. */ template<class RetT_, class... ArgsT_> std::ostream& operator<<(std::ostream& os, PrintType<RetT_(ArgsT_...)>) { os << PrintType<RetT_>() << "(" << PrintParameters<true, ArgsT_...>() << ")"; return os; } /** * For member functions. */ template<class ClassT_, class RetT_, class... ArgsT_> std::ostream& operator<<( std::ostream& os, PrintType<RetT_(ClassT_::*)(ArgsT_...)>) { os << PrintType<RetT_>() << "(" << PrintType<ClassT_>() << "::*)(" << PrintParameters<true, ArgsT_...>() << ")"; return os; } /** * For parameter lists with varargs. */ template<bool StartsP_, class... ArgsT_> struct PrintParamtersWithVarArgs { // Never used directly. }; /** * For function pointers with varargs. */ template<class RetT_, class... ArgsT_> std::ostream& operator<<( std::ostream& os, PrintType<RetT_(ArgsT_... ...)>) { os << PrintType<RetT_>() << "(" << PrintParameters<true, ArgsT_...>() << "...)"; return os; } /** * For member functions with varargs. */ template<class RetT_, class ClassT_, class... ArgsT_> std::ostream& operator<<( std::ostream& os, PrintType<RetT_(ClassT_::*)(ArgsT_... ...)>) { os << PrintType<RetT_>() << "(" << PrintType<ClassT_>() << "::*)(" << PrintParameters<true, ArgsT_...>() << "...)"; return os; } /** * For rvalue references (for completeness). */ template<class T_> std::ostream& operator<<(std::ostream& os, PrintType<T_&&>) { os << PrintType<T_>() << "&&"; return os; }; #endif

The main workhorse in the code above is the operator at line 19, that calls itself recursively in line 26 until it eventually degenerates into the operator defined at line 34. Also note the double ellipsis in lines 81 and 95: The first one unpacks the template parameter list, the second one is for old style varargs.

Last but not least: Feel free to come up with suggestions or comments.