How to print anything in C++ (part 1)

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 and tuple 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:

  1. There are at least 6 types of callable things: functions, member functions, std::functions, bind expressions, lambdas, and objects that support operator() (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.
  2. nullptr is its own type and unprintable.
  3. enum class values are not printable out-of-the-box; they will require a cast to std::underlying_type.
  4. pairs should be easy to deal with, but tuples 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/pairs/tuples. 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 pairs and tuples 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<<.

Leave a Reply