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

Part 1 Part 2 Part 3 Part 4 Part 5 Postscript

So far, we can print containers, but what about arrays? And what about “pretty-printing” strings – perhaps we need to wrap them with quotes. Well, we know that with the existing code, both arrays and strings count as outputtable. Both std::string and char* (and its variously const friends) can be passed to operator<<, and so can arrays, because they decay to pointers.

So, we should be able to deal with this using common-or-garden specialization on stringifier_select. Specializing for std::string is easy:

template <typename C, typename T, typename A>
struct stringifier_select<std::basic_string<C,T,A>, 
                          is_outputtable_tag>
{
  using S = std::basic_string<C,T,A>;
  explicit stringifier_select(const S& t) : m_t(t) {}
 
  std::ostream& output(std::ostream& s) const
  {
    return s << C('\"') << m_t << C('\"');
  }
 
  const S& m_t;
};

And specializing for the other char* related types is also as easy, albeit verbose because of dealing with char*, const char*, char* const and const char* const.

Specializing for arrays of char is just as easy. This time we have just the array size as a template argument. And once again, there is a specialization for arrays of const char that is identical.

template <size_t N>
struct stringifier_select<char[N], is_outputtable_tag>
{
  using S = char[N];
  explicit stringifier_select(const S& t) : m_t(t) {}
 
  std::ostream& output(std::ostream& s) const
  {
    return s << '\"' << m_t << '\"';
  }
 
  const S& m_t;
};

Specializing for arrays (other than of char) is also easy, and gives us a chance to abstract out the code that we used for the iterable printing. By happy circumstance (i.e. by design!), arrays support std::begin() and std::end(), so we can write the following:

template <typename T>
std::ostream& output_iterable(std::ostream& s, const T& t)
{
  s << iterable_opener<T>()(t);
  auto b = std::begin(t);
  auto e = std::end(t);
  if (b != e)
    s << prettyprint(*b);
  std::for_each(++b, e,
                [&s, &t] (auto& e)
                { s << iterable_separator<T>()(t)
                    << prettyprint(e); });
  return s << iterable_closer<T>()(t);
}
 
template <typename T, size_t N>
struct stringifier_select<T[N], is_outputtable_tag>
{
  using S = T[N];
  explicit stringifier_select(const S& t) : m_t(t) {}
 
  std::ostream& output(std::ostream& s) const
  {
    return output_iterable<T[N]>(s, m_t);
  }
 
  const S& m_t;
};

And the code for printing iterable things changes likewise. Unlike the situation with char*, we don’t need to deal with const and non-const separately because here, T itself is inferred to be const or not.

And that’s pretty much it – just a couple more things to add. I mentioned enum classes back in part 1, and here’s how we print out their values:

template <typename T>
struct stringifier_select<T, is_enum_tag>
{
  explicit stringifier_select(T t) : m_t(t) {}
 
  std::ostream& output(std::ostream& s) const
  {
    return s << static_cast<std::underlying_type_t<T>>(m_t);
  }
 
  T m_t;
};

Simple. Two final things to add: first, specialize for pretty-printing bool equivalently to using std::boolalpha; second, distinguish a few “unprintable” things and output something for them – classes without operator<<, unions, nullptr. The code that does this is very similar to what we’ve already seen.

So now, we can pretty-print all “normally” printable things, containers, pairs and tuples, callable things, certain unprintables that we can meaningfully label, and really unprintable things with a fallback. I think that’ll do for the time being. It’s been a fun journey, exploring TMP techniques, C++ type support, mapping over tuples, and the amazing void_t.

You can find all the resulting code from this exercise on github.

Leave a Reply