Part 1 Part 2 Part 3 Part 4 Part 5 Postscript
I thought I’d have a go at writing some code that could print things. A pretty-printer, if you like. What I want to be able to do is this:
// Print x correctly, where x is ANY type. cout << prettyprint(x) << endl; |
Obviously this is going to involve templates since the type of x
may vary.
There are a few things that immediately spring to mind:
- Maybe
x
is already printable, that is,operator<<
might work “out of the box”. - If
x
is a container, we want to do something with that and print the elements. - We probably want to print elements of
pair
andtuple
also. - The value of some things is not printable, or at least not sensible to print. For example, functions.
The basic plan that came to mind is this: prettyprint
is a function template, which returns an object that wraps the x
and has operator<<
defined. This class will itself be a template, and the template instantiation we choose will be determined by what sort of thing x
is.
This suggest that the very first thing we can write looks something like this:
template <typename T> struct stringifier { explicit stringifier(const T&) {} std::ostream& output(std::ostream& s) const { return s << "<unknown>"; } }; template <typename T> inline std::ostream& operator<<(std::ostream& s, const stringifier<T>& t) { return t.output(s); } template <typename T> inline stringifier<T> prettyprint(T&& t) { return stringifier<T>(std::forward<T>(t)); } |
So far so good. Now we can print “<unknown>” for any type. We have a place to start specializing things.
A quick poke around C++ type support and some experiments soon fleshed out the plan:
- There are at least 6 types of callable things: functions, member functions,
std::function
s, bind expressions, lambdas, and objects that supportoperator()
(function objects). All of these are “unprintable”. Lambdas are a kind of function object; I don’t know whether they can be meaningfully distinguished or whether that is desirable. nullptr
is its own type and unprintable.enum class
values are not printable out-of-the-box; they will require a cast tostd::underlying_type
.pair
s should be easy to deal with, buttuple
s will require some effort. They are designed for static access, not iteration.
So now I have 4 broad categories I want to represent for printing: already-outputtable, unprintable, callable, and containers/pair
s/tuple
s. I decided to make tag types for each specializable thing so as to be able to select on them.
struct is_iterable_tag {}; struct is_pair_tag {}; struct is_tuple_tag {}; struct is_callable_tag {}; struct is_outputtable_tag {}; struct is_enum_tag {}; struct is_unprintable_tag {}; |
As luck would have it, several of these already have built-in support for detecting:
template<typename T> using is_unprintable = typename std::conditional< std::is_union<T>::value || std::is_class<T>::value || std::is_null_pointer<T>::value, std::true_type, std::false_type>::type; // similarly we can use: // std::is_enum<T>::value to detect enum or enum class |
Detecting pair
s and tuple
s is also easy, using a standard specialization pattern:
template <typename T> struct is_pair : public std::false_type {}; template <typename T, typename U> struct is_pair<std::pair<T, U>> : public std::true_type {}; template <typename T> struct is_tuple : public std::false_type {}; template <typename... Ts> struct is_tuple<std::tuple<Ts...>> : public std::true_type {}; |
A good setup so far. We can detect concrete types. Next, how to detect whether something already supports operator<<
.