Wednesday 27 November 2019

templates - Pretty-print C++ STL containers



Please take note of the updates at the end of this post.



Update: I have created a public project on GitHub for this library!






I would like to have a single template that once and for all takes care of pretty-printing all STL containers via operator<<. In pseudo code, I'm looking for something like this:



template
std::ostream & operator<<(std::ostream & o, const C & x)
{
o << open;
// for (typename C::const_iterator i = x.begin(); i != x.end(); i++) /* Old-school */
for (auto i = x.begin(); i != x.end(); i++)
{
if (i != x.begin()) o << delim;
o << *i;
}
o << close;
return o;
}


Now I've seen plenty of template magic here on SO that I never thought possible, so I'm wondering if anyone can suggest something that would match all containers C. Maybe something trait-ish that can figure out if something has the necessary iterator?



Many thanks!






Update (and solution)



After raising this problem again on Channel 9, I got a fantastic answer from Sven Groot, which, combined with a bit of SFINAE type traiting, appears to solve the problem in a completely general and nestable fashion. The delimiters may be individually specialised, an example specialization for std::set is included, as well as an example of using custom delimiters.



The helper "wrap_array()" can be used to print raw C arrays. Update: Pairs and tuples are available for printing; default delimiters are round brackets.



The enable-if type trait requires C++0x, but with some modifications it should be possible to make a C++98 version of this. Tuples require variadic templates, hence C++0x.



I have asked Sven to post the solution here so that I can accept it, but in the meantime I'd like to post the code myself for reference. (Update: Sven has now posted his code below, which I made the accepted answer. My own code uses container type traits, which work for me but may cause unexpected behaviour with non-container classes that provide iterators.)



Header (prettyprint.h):



#ifndef H_PRETTY_PRINT
#define H_PRETTY_PRINT


#include
#include
#include
#include


namespace std
{
// Pre-declarations of container types so we don't actually have to include the relevant headers if not needed, speeding up compilation time.
template class set;
}

namespace pretty_print
{

// SFINAE type trait to detect a container based on whether T::const_iterator exists.
// (Improvement idea: check also if begin()/end() exist.)

template
struct is_container_helper
{
private:
template static char test(typename C::const_iterator*);
template static int test(...);
public:
static const bool value = sizeof(test(0)) == sizeof(char);
};


// Basic is_container template; specialize to derive from std::true_type for all desired container types

template struct is_container : public ::std::integral_constant::value> { };


// Holds the delimiter values for a specific character type

template
struct delimiters_values
{
typedef TChar char_type;
const TChar * prefix;
const TChar * delimiter;
const TChar * postfix;
};


// Defines the delimiter values for a specific container and character type

template
struct delimiters
{
typedef delimiters_values type;
static const type values;
};


// Default delimiters

template struct delimiters { static const delimiters_values values; };
template const delimiters_values delimiters::values = { "[", ", ", "]" };
template struct delimiters { static const delimiters_values values; };
template const delimiters_values delimiters::values = { L"[", L", ", L"]" };


// Delimiters for set

template struct delimiters< ::std::set, char> { static const delimiters_values values; };
template const delimiters_values delimiters< ::std::set, char>::values = { "{", ", ", "}" };
template struct delimiters< ::std::set, wchar_t> { static const delimiters_values values; };
template const delimiters_values delimiters< ::std::set, wchar_t>::values = { L"{", L", ", L"}" };


// Delimiters for pair (reused for tuple, see below)

template struct delimiters< ::std::pair, char> { static const delimiters_values values; };
template const delimiters_values delimiters< ::std::pair, char>::values = { "(", ", ", ")" };
template struct delimiters< ::std::pair, wchar_t> { static const delimiters_values values; };
template const delimiters_values delimiters< ::std::pair, wchar_t>::values = { L"(", L", ", L")" };


// Functor to print containers. You can use this directly if you want to specificy a non-default delimiters type.

template, typename TDelimiters = delimiters>
struct print_container_helper
{
typedef TChar char_type;
typedef TDelimiters delimiters_type;
typedef std::basic_ostream & ostream_type;

print_container_helper(const T & container)
: _container(container)
{
}

inline void operator()(ostream_type & stream) const
{
if (delimiters_type::values.prefix != NULL)
stream << delimiters_type::values.prefix;

for (typename T::const_iterator beg = _container.begin(), end = _container.end(), it = beg; it != end; ++it)
{
if (it != beg && delimiters_type::values.delimiter != NULL)
stream << delimiters_type::values.delimiter;

stream << *it;
}

if (delimiters_type::values.postfix != NULL)
stream << delimiters_type::values.postfix;
}

private:
const T & _container;
};


// Type-erasing helper class for easy use of custom delimiters.
// Requires TCharTraits = std::char_traits and TChar = char or wchar_t, and MyDelims needs to be defined for TChar.
// Usage: "cout << pretty_print::custom_delims(x)".

struct custom_delims_base
{
virtual ~custom_delims_base() { }
virtual ::std::ostream & stream(::std::ostream &) = 0;
virtual ::std::wostream & stream(::std::wostream &) = 0;
};

template
struct custom_delims_wrapper : public custom_delims_base
{
custom_delims_wrapper(const T & t) : t(t) { }

::std::ostream & stream(::std::ostream & stream)
{
return stream << ::pretty_print::print_container_helper, Delims>(t);
}
::std::wostream & stream(::std::wostream & stream)
{
return stream << ::pretty_print::print_container_helper, Delims>(t);
}

private:
const T & t;
};

template
struct custom_delims
{
template custom_delims(const Container & c) : base(new custom_delims_wrapper(c)) { }
~custom_delims() { delete base; }
custom_delims_base * base;
};

} // namespace pretty_print


template
inline std::basic_ostream & operator<<(std::basic_ostream & stream, const pretty_print::custom_delims & p)
{
return p.base->stream(stream);
}


// Template aliases for char and wchar_t delimiters
// Enable these if you have compiler support
//
// Implement as "template const sdelims::type sdelims>::values = { ... }."

//template using pp_sdelims = pretty_print::delimiters;
//template using pp_wsdelims = pretty_print::delimiters;


namespace std
{
// Prints a print_container_helper to the specified stream.

template
inline basic_ostream & operator<<(basic_ostream & stream,
const ::pretty_print::print_container_helper & helper)
{
helper(stream);
return stream;
}

// Prints a container to the stream using default delimiters

template
inline typename enable_if< ::pretty_print::is_container::value, basic_ostream&>::type
operator<<(basic_ostream & stream, const T & container)
{
return stream << ::pretty_print::print_container_helper(container);
}

// Prints a pair to the stream using delimiters from delimiters>.
template
inline basic_ostream & operator<<(basic_ostream & stream, const pair & value)
{
if (::pretty_print::delimiters, TChar>::values.prefix != NULL)
stream << ::pretty_print::delimiters, TChar>::values.prefix;

stream << value.first;

if (::pretty_print::delimiters, TChar>::values.delimiter != NULL)
stream << ::pretty_print::delimiters, TChar>::values.delimiter;

stream << value.second;

if (::pretty_print::delimiters, TChar>::values.postfix != NULL)
stream << ::pretty_print::delimiters, TChar>::values.postfix;

return stream;
}
} // namespace std

// Prints a tuple to the stream using delimiters from delimiters>.

namespace pretty_print
{
struct tuple_dummy_t { }; // Just if you want special delimiters for tuples.

typedef std::pair tuple_dummy_pair;

template
struct pretty_tuple_helper
{
static inline void print(::std::basic_ostream & stream, const Tuple & value)
{
pretty_tuple_helper::print(stream, value);

if (delimiters::values.delimiter != NULL)
stream << delimiters::values.delimiter;

stream << std::get(value);
}
};

template
struct pretty_tuple_helper
{
static inline void print(::std::basic_ostream & stream, const Tuple & value) { stream << ::std::get<0>(value); }
};
} // namespace pretty_print


namespace std
{
template
inline basic_ostream & operator<<(basic_ostream & stream, const tuple & value)
{
if (::pretty_print::delimiters< ::pretty_print::tuple_dummy_pair, TChar>::values.prefix != NULL)
stream << ::pretty_print::delimiters< ::pretty_print::tuple_dummy_pair, TChar>::values.prefix;

::pretty_print::pretty_tuple_helper &, sizeof...(Args), TChar, TCharTraits>::print(stream, value);

if (::pretty_print::delimiters< ::pretty_print::tuple_dummy_pair, TChar>::values.postfix != NULL)
stream << ::pretty_print::delimiters< ::pretty_print::tuple_dummy_pair, TChar>::values.postfix;

return stream;
}
} // namespace std


// A wrapper for raw C-style arrays. Usage: int arr[] = { 1, 2, 4, 8, 16 }; std::cout << wrap_array(arr) << ...

namespace pretty_print
{
template
struct array_wrapper
{
typedef const T * const_iterator;
typedef T value_type;

array_wrapper(const T (& a)[N]) : _array(a) { }
inline const_iterator begin() const { return _array; }
inline const_iterator end() const { return _array + N; }

private:
const T * const _array;
};
} // namespace pretty_print

template
inline pretty_print::array_wrapper pretty_print_array(const T (& a)[N])
{
return pretty_print::array_wrapper(a);
}


#endif


Usage example:



#include 
#include
#include
#include
#include
#include
#include
#include
#include

#include "prettyprint.h"

// Specialization for a particular container
template<> const pretty_print::delimiters_values pretty_print::delimiters, char>::values = { "|| ", " : ", " ||" };

// Custom delimiters for one-off use
struct MyDel { static const delimiters_values values; };
const delimiters_values MyDel::values = { "<", "; ", ">" };

int main(int argc, char * argv[])
{
std::string cs;
std::unordered_map um;
std::map om;
std::set ss;
std::vector v;
std::vector> vv;
std::vector> vp;
std::vector vd;
v.reserve(argc - 1);
vv.reserve(argc - 1);
vp.reserve(argc - 1);
vd.reserve(argc - 1);

std::cout << "Printing pairs." << std::endl;

while (--argc)
{
std::string s(argv[argc]);
std::pair p(argc, s);

um[argc] = s;
om[argc] = s;
v.push_back(s);
vv.push_back(v);
vp.push_back(p);
vd.push_back(1./double(i));
ss.insert(s);
cs += s;

std::cout << " " << p << std::endl;
}

std::array a{{ 'h', 'e', 'l', 'l', 'o' }};

std::cout << "Vector: " << v << std::endl
<< "Incremental vector: " << vv << std::endl
<< "Another vector: " << vd << std::endl
<< "Pairs: " << vp << std::endl
<< "Set: " << ss << std::endl
<< "OMap: " << om << std::endl
<< "UMap: " << um << std::endl
<< "String: " << cs << std::endl
<< "Array: " << a << std::endl
;

// Using custom delimiters manually:
std::cout << pretty_print::print_container_helper, char, std::char_traits, MyDel>(v) << std::endl;

// Using custom delimiters with the type-erasing helper class
std::cout << pretty_print::custom_delims(v) << std::endl;

// Pairs and tuples and arrays:
auto a1 = std::make_pair(std::string("Jello"), 9);
auto a2 = std::make_tuple(1729);
auto a3 = std::make_tuple("Qrgh", a1, 11);
auto a4 = std::make_tuple(1729, 2875, std::pair(1.5, "meow"));
int arr[] = { 1, 4, 9, 16 };

std::cout << "C array: " << wrap_array(arr) << std::endl
<< "Pair: " << a1 << std::endl
<< "1-tuple: " << a2 << std::endl
<< "n-tuple: " << a3 << std::endl
<< "n-tuple: " << a4 << std::endl
;
}


Further ideas for improvements:




  • Implement output for std::tuple<...> in the same way is we have it for std::pair. Update: This is now a separate question on SO! Upupdate: This has now been implemented, thanks to Xeo!

  • Add namespaces so that the helper classes don't bleed into the global namespace. Done

  • Add template aliases (or something similar) to facilitate making custom delimiter classes, or maybe preprocessor macros?



Recent updates:




  • I removed the custom output iterator in favour of a simple for loop in the print function.

  • All implementation details are now in the pretty_print namespace. Only the global stream operators and the pretty_print_array wrapper are in the global namespace.

  • Fixed the namespacing so that operator<< is now correctly in std.



Notes:




  • Removing the output iterator means that there is no way to use std::copy() to get pretty-printing. I might reinstate the pretty iterator if this is a desired feature, but Sven's code below has the implementation.

  • It was a conscious design decision to make the delimiters compile-time constants rather than object constants. That means that you cannot supply delimiters dynamically at runtime, but it also means that there's no unneeded overhead. An object-based delimiter configuration has been proposed by Dennis Zickefoose in a comment to Sven's code below. If desired, this could be implemented as an alternative feature.

  • It is currently not obvious how to customize nested container delimiters.

  • Bear in mind that the purpose of this library is to allow quick container printing facilities that require zero coding on your part. It is not an all-purpose formatting library, but rather a developing tool to alleviate the need to write boiler-plate code for container inspection.



Thank you to everyone who contributed!






Note: If you are looking for a quick way to deploy custom delimiters, here is one way using type erasure. We assume that you have already constructed a delimiter class, say MyDel, like so:



struct MyDel { static const pretty_print::delimiters_values values; };
const pretty_print::delimiters_values MyDel::values = { "<", "; ", ">" };


Now we want to be able to write std::cout << MyPrinter(v) << std::endl; for some container v using those delimiters. MyPrinter will be a type-erasing class, like so:



struct wrapper_base
{
virtual ~wrapper_base() { }
virtual std::ostream & stream(std::ostream & o) = 0;
};

template
struct wrapper : public wrapper_base
{
wrapper(const T & t) : t(t) { }
std::ostream & stream(std::ostream & o)
{
return o << pretty_print::print_container_helper, Delims>(t);
}
private:
const T & t;
};

template
struct MyPrinter
{
template MyPrinter(const Container & c) : base(new wrapper(c)) { }
~MyPrinter() { delete base; }
wrapper_base * base;
};

template
std::ostream & operator<<(std::ostream & o, const MyPrinter & p) { return p.base->stream(o); }

Answer



This solution was inspired by Marcelo's solution, with a few changes:



#include 
#include
#include
#include
#include

// This works similar to ostream_iterator, but doesn't print a delimiter after the final item
template >
class pretty_ostream_iterator : public std::iterator
{
public:
typedef TChar char_type;
typedef TCharTraits traits_type;
typedef std::basic_ostream ostream_type;

pretty_ostream_iterator(ostream_type &stream, const char_type *delim = NULL)
: _stream(&stream), _delim(delim), _insertDelim(false)
{
}

pretty_ostream_iterator& operator=(const T &value)
{
if( _delim != NULL )
{
// Don't insert a delimiter if this is the first time the function is called
if( _insertDelim )
(*_stream) << _delim;
else
_insertDelim = true;
}
(*_stream) << value;
return *this;
}

pretty_ostream_iterator& operator*()
{
return *this;
}

pretty_ostream_iterator& operator++()
{
return *this;
}

pretty_ostream_iterator& operator++(int)
{
return *this;
}
private:
ostream_type *_stream;
const char_type *_delim;
bool _insertDelim;
};

#if _MSC_VER >= 1400

// Declare pretty_ostream_iterator as checked
template
struct std::_Is_checked_helper > : public std::tr1::true_type
{
};

#endif // _MSC_VER >= 1400

namespace std
{
// Pre-declarations of container types so we don't actually have to include the relevant headers if not needed, speeding up compilation time.
// These aren't necessary if you do actually include the headers.
template class vector;
template class list;
template class set;
template class map;
}

// Basic is_container template; specialize to derive from std::true_type for all desired container types
template struct is_container : public std::false_type { };

// Mark vector as a container
template struct is_container > : public std::true_type { };

// Mark list as a container
template struct is_container > : public std::true_type { };

// Mark set as a container
template struct is_container > : public std::true_type { };

// Mark map as a container
template struct is_container > : public std::true_type { };

// Holds the delimiter values for a specific character type
template
struct delimiters_values
{
typedef TChar char_type;
const TChar *prefix;
const TChar *delimiter;
const TChar *postfix;
};

// Defines the delimiter values for a specific container and character type
template
struct delimiters
{
static const delimiters_values values;
};

// Default delimiters
template struct delimiters { static const delimiters_values values; };
template const delimiters_values delimiters::values = { "{ ", ", ", " }" };
template struct delimiters { static const delimiters_values values; };
template const delimiters_values delimiters::values = { L"{ ", L", ", L" }" };

// Delimiters for set
template struct delimiters, char> { static const delimiters_values values; };
template const delimiters_values delimiters, char>::values = { "[ ", ", ", " ]" };
template struct delimiters, wchar_t> { static const delimiters_values values; };
template const delimiters_values delimiters, wchar_t>::values = { L"[ ", L", ", L" ]" };

// Delimiters for pair
template struct delimiters, char> { static const delimiters_values values; };
template const delimiters_values delimiters, char>::values = { "(", ", ", ")" };
template struct delimiters, wchar_t> { static const delimiters_values values; };
template const delimiters_values delimiters, wchar_t>::values = { L"(", L", ", L")" };

// Functor to print containers. You can use this directly if you want to specificy a non-default delimiters type.
template, typename TDelimiters = delimiters >
struct print_container_helper
{
typedef TChar char_type;
typedef TDelimiters delimiters_type;
typedef std::basic_ostream& ostream_type;

print_container_helper(const T &container)
: _container(&container)
{
}

void operator()(ostream_type &stream) const
{
if( delimiters_type::values.prefix != NULL )
stream << delimiters_type::values.prefix;
std::copy(_container->begin(), _container->end(), pretty_ostream_iterator(stream, delimiters_type::values.delimiter));
if( delimiters_type::values.postfix != NULL )
stream << delimiters_type::values.postfix;
}
private:
const T *_container;
};

// Prints a print_container_helper to the specified stream.
template
std::basic_ostream& operator<<(std::basic_ostream &stream, const print_container_helper &helper)
{
helper(stream);
return stream;
}

// Prints a container to the stream using default delimiters
template
typename std::enable_if::value, std::basic_ostream&>::type
operator<<(std::basic_ostream &stream, const T &container)
{
stream << print_container_helper(container);
return stream;
}

// Prints a pair to the stream using delimiters from delimiters>.
template
std::basic_ostream& operator<<(std::basic_ostream &stream, const std::pair &value)
{
if( delimiters, TChar>::values.prefix != NULL )
stream << delimiters, TChar>::values.prefix;

stream << value.first;

if( delimiters, TChar>::values.delimiter != NULL )
stream << delimiters, TChar>::values.delimiter;

stream << value.second;

if( delimiters, TChar>::values.postfix != NULL )
stream << delimiters, TChar>::values.postfix;
return stream;
}

// Used by the sample below to generate some values
struct fibonacci
{
fibonacci() : f1(0), f2(1) { }
int operator()()
{
int r = f1 + f2;
f1 = f2;
f2 = r;
return f1;
}
private:
int f1;
int f2;
};

int main()
{
std::vector v;
std::generate_n(std::back_inserter(v), 10, fibonacci());

std::cout << v << std::endl;

// Example of using pretty_ostream_iterator directly
std::generate_n(pretty_ostream_iterator(std::cout, ";"), 20, fibonacci());
std::cout << std::endl;
}


Like Marcelo's version, it uses an is_container type trait that must be specialized for all containers that are to be supported. It may be possible to use a trait to check for value_type, const_iterator, begin()/end(), but I'm not sure I'd recommend that since it might match things that match those criteria but aren't actually containers, like std::basic_string. Also like Marcelo's version, it uses templates that can be specialized to specify the delimiters to use.



The major difference is that I've built my version around a pretty_ostream_iterator, which works similar to the std::ostream_iterator but doesn't print a delimiter after the last item. Formatting the containers is done by the print_container_helper, which can be used directly to print containers without an is_container trait, or to specify a different delimiters type.



I've also defined is_container and delimiters so it will work for containers with non-standard predicates or allocators, and for both char and wchar_t. The operator<< function itself is also defined to work with both char and wchar_t streams.



Finally, I've used std::enable_if, which is available as part of C++0x, and works in Visual C++ 2010 and g++ 4.3 (needs the -std=c++0x flag) and later. This way there is no dependency on Boost.


No comments:

Post a Comment

php - file_get_contents shows unexpected output while reading a file

I want to output an inline jpg image as a base64 encoded string, however when I do this : $contents = file_get_contents($filename); print &q...