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
struct stringifier_select,
is_outputtable_tag>
{
using S = std::basic_string;
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
struct stringifier_select
{
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
std::ostream& output_iterable(std::ostream& s, const T& t)
{
s << iterable_opener()(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)
<< prettyprint(e); });
return s << iterable_closer()(t);
}
template
struct stringifier_select
{
using S = T[N];
explicit stringifier_select(const S& t) : m_t(t) {}
std::ostream& output(std::ostream& s) const
{
return output_iterable(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
struct stringifier_select
{
explicit stringifier_select(T t) : m_t(t) {}
std::ostream& output(std::ostream& s) const
{
return s << static_cast>(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, pair
s and tuple
s, 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 tuple
s, and the amazing void_t
.
You can find all the resulting code from this exercise on github.