Pretty-print C++ STL containers

前端 未结 10 1756
谎友^
谎友^ 2020-11-21 07:39

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

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


相关标签:
10条回答
  • 2020-11-21 07:44

    I am going to add another answer here, because I have come up with a different approach to my previous one, and that is to use locale facets.

    The basics are here

    Essentially what you do is:

    1. Create a class that derives from std::locale::facet. The slight downside is that you will need a compilation unit somewhere to hold its id. Let's call it MyPrettyVectorPrinter. You'd probably give it a better name, and also create ones for pair and map.
    2. In your stream function, you check std::has_facet< MyPrettyVectorPrinter >
    3. If that returns true, extract it with std::use_facet< MyPrettyVectorPrinter >( os.getloc() )
    4. Your facet objects will have values for the delimiters and you can read them. If the facet isn't found, your print function (operator<<) provides default ones. Note you can do the same thing for reading a vector.

    I like this method because you can use a default print whilst still being able to use a custom override.

    The downsides are needing a library for your facet if used in multiple projects (so can't just be headers-only) and also the fact that you need to beware about the expense of creating a new locale object.

    I have written this as a new solution rather than modify my other one because I believe both approaches can be correct and you take your pick.

    0 讨论(0)
  • 2020-11-21 07:45

    My solution is simple.h, which is part of scc package. All std containers, maps, sets, c-arrays are printable.

    0 讨论(0)
  • 2020-11-21 07:47

    You can format containers as well as ranges and tuples using the {fmt} library. For example:

    #include <vector>
    #include <fmt/ranges.h>
    
    int main() {
      auto v = std::vector<int>{1, 2, 3};
      fmt::print("{}", v);
    }
    

    prints

    {1, 2, 3}
    

    to stdout.

    Disclaimer: I'm the author of {fmt}.

    0 讨论(0)
  • 2020-11-21 07:52

    The code proved to be handy on several occasions now and I feel the expense to get into customization as usage is quite low. Thus, I decided to release it under MIT license and provide a GitHub repository where the header and a small example file can be downloaded.

    http://djmuw.github.io/prettycc

    0. Preface and wording

    A 'decoration' in terms of this answer is a set of prefix-string, delimiter-string, and a postfix-string. Where the prefix string is inserted into a stream before and the postfix string after the values of a container (see 2. Target containers). The delimiter string is inserted between the values of the respective container.

    Note: Actually, this answer does not address the question to 100% since the decoration is not strictly compiled time constant because runtime checks are required to check whether a custom decoration has been applied to the current stream. Nevertheless, I think it has some decent features.

    Note2: May have minor bugs since it is not yet well tested.

    1. General idea/usage

    Zero additional code required for usage

    It is to be kept as easy as

    #include <vector>
    #include "pretty.h"
    
    int main()
    {
      std::cout << std::vector<int>{1,2,3,4,5}; // prints 1, 2, 3, 4, 5
      return 0;
    }
    

    Easy customization ...

    ... with respect to a specific stream object

    #include <vector>
    #include "pretty.h"
    
    int main()
    {
      // set decoration for std::vector<int> for cout object
      std::cout << pretty::decoration<std::vector<int>>("(", ",", ")");
      std::cout << std::vector<int>{1,2,3,4,5}; // prints (1,2,3,4,5)
      return 0;
    }
    

    or with respect to all streams:

    #include <vector>
    #include "pretty.h"
    
    // set decoration for std::vector<int> for all ostream objects
    PRETTY_DEFAULT_DECORATION(std::vector<int>, "{", ", ", "}")
    
    int main()
    {
      std::cout << std::vector<int>{1,2,3,4,5}; // prints {1, 2, 3, 4, 5}
      std::cout << pretty::decoration<std::vector<int>>("(", ",", ")");
      std::cout << std::vector<int>{1,2,3,4,5}; // prints (1,2,3,4,5)
      return 0;
    }
    

    Rough description

    • The code includes a class template providing a default decoration for any type
    • which can be specialized to change the default decoration for (a) certain type(s) and it is
    • using the private storage provided by ios_base using xalloc/pword in order to save a pointer to a pretty::decor object specifically decorating a certain type on a certain stream.

    If no pretty::decor<T> object for this stream has been set up explicitly pretty::defaulted<T, charT, chartraitT>::decoration() is called to obtain the default decoration for the given type. The class pretty::defaulted is to be specialized to customize default decorations.

    2. Target objects / containers

    Target objects obj for the 'pretty decoration' of this code are objects having either

    • overloads std::begin and std::end defined (includes C-Style arrays),
    • having begin(obj) and end(obj) available via ADL,
    • are of type std::tuple
    • or of type std::pair.

    The code includes a trait for identification of classes with range features (begin/end). (There's no check included, whether begin(obj) == end(obj) is a valid expression, though.)

    The code provides operator<<s in the global namespace that only apply to classes not having a more specialized version of operator<< available. Therefore, for example std::string is not printed using the operator in this code although having a valid begin/end pair.

    3. Utilization and customization

    Decorations can be imposed separately for every type (except different tuples) and stream (not stream type!). (I.e. a std::vector<int> can have different decorations for different stream objects.)

    A) Default decoration

    The default prefix is "" (nothing) as is the default postfix, while the default delimiter is ", " (comma+space).

    B) Customized default decoration of a type by specializing the pretty::defaulted class template

    The struct defaulted has a static member function decoration() returning a decor object which includes the default values for the given type.

    Example using an array:

    Customize default array printing:

    namespace pretty
    {
      template<class T, std::size_t N>
      struct defaulted<T[N]>
      {
        static decor<T[N]> decoration()
        {
          return{ { "(" }, { ":" }, { ")" } };
        }
      };
    }
    

    Print an arry array:

    float e[5] = { 3.4f, 4.3f, 5.2f, 1.1f, 22.2f };
    std::cout << e << '\n'; // prints (3.4:4.3:5.2:1.1:22.2)
    

    Using the PRETTY_DEFAULT_DECORATION(TYPE, PREFIX, DELIM, POSTFIX, ...) macro for char streams

    The macro expands to

    namespace pretty { 
      template< __VA_ARGS__ >
      struct defaulted< TYPE > {
        static decor< TYPE > decoration() {
          return { PREFIX, DELIM, POSTFIX };
        } 
      }; 
    } 
    

    enabling the above partial specialization to be rewritten to

    PRETTY_DEFAULT_DECORATION(T[N], "", ";", "", class T, std::size_t N)
    

    or inserting a full specialization like

    PRETTY_DEFAULT_DECORATION(std::vector<int>, "(", ", ", ")")
    

    Another macro for wchar_t streams is included: PRETTY_DEFAULT_WDECORATION.

    C) Impose decoration on streams

    The function pretty::decoration is used to impose a decoration on a certain stream. There are overloads taking either - one string argument being the delimiter (adopting prefix and postfix from the defaulted class) - or three string arguments assembling the complete decoration

    Complete decoration for given type and stream

    float e[3] = { 3.4f, 4.3f, 5.2f };
    std::stringstream u;
    // add { ; } decoration to u
    u << pretty::decoration<float[3]>("{", "; ", "}");
    
    // use { ; } decoration
    u << e << '\n'; // prints {3.4; 4.3; 5.2}
    
    // uses decoration returned by defaulted<float[3]>::decoration()
    std::cout << e; // prints 3.4, 4.3, 5.2
    

    Customization of delimiter for given stream

    PRETTY_DEFAULT_DECORATION(float[3], "{{{", ",", "}}}")
    
    std::stringstream v;
    v << e; // prints {{{3.4,4.3,5.2}}}
    
    v << pretty::decoration<float[3]>(":");
    v << e; // prints {{{3.4:4.3:5.2}}}
    
    v << pretty::decoration<float[3]>("((", "=", "))");
    v << e; // prints ((3.4=4.3=5.2))
    

    4. Special handling of std::tuple

    Instead of allowing a specialization for every possible tuple type, this code applies any decoration available for std::tuple<void*> to all kind of std::tuple<...>s.

    5. Remove custom decoration from the stream

    To go back to the defaulted decoration for a given type use pretty::clear function template on the stream s.

    s << pretty::clear<std::vector<int>>();
    

    5. Further examples

    Printing "matrix-like" with newline delimiter

    std::vector<std::vector<int>> m{ {1,2,3}, {4,5,6}, {7,8,9} };
    std::cout << pretty::decoration<std::vector<std::vector<int>>>("\n");
    std::cout << m;
    

    Prints

    1, 2, 3
    4, 5, 6
    7, 8, 9
    

    See it on ideone/KKUebZ

    6. Code

    #ifndef pretty_print_0x57547_sa4884X_0_1_h_guard_
    #define pretty_print_0x57547_sa4884X_0_1_h_guard_
    
    #include <string>
    #include <iostream>
    #include <type_traits>
    #include <iterator>
    #include <utility>
    
    #define PRETTY_DEFAULT_DECORATION(TYPE, PREFIX, DELIM, POSTFIX, ...) \
        namespace pretty { template< __VA_ARGS__ >\
        struct defaulted< TYPE > {\
        static decor< TYPE > decoration(){\
          return { PREFIX, DELIM, POSTFIX };\
        } /*decoration*/ }; /*defaulted*/} /*pretty*/
    
    #define PRETTY_DEFAULT_WDECORATION(TYPE, PREFIX, DELIM, POSTFIX, ...) \
        namespace pretty { template< __VA_ARGS__ >\
        struct defaulted< TYPE, wchar_t, std::char_traits<wchar_t> > {\
        static decor< TYPE, wchar_t, std::char_traits<wchar_t> > decoration(){\
          return { PREFIX, DELIM, POSTFIX };\
        } /*decoration*/ }; /*defaulted*/} /*pretty*/
    
    namespace pretty
    {
    
      namespace detail
      {
        // drag in begin and end overloads
        using std::begin;
        using std::end;
        // helper template
        template <int I> using _ol = std::integral_constant<int, I>*;
        // SFINAE check whether T is a range with begin/end
        template<class T>
        class is_range
        {
          // helper function declarations using expression sfinae
          template <class U, _ol<0> = nullptr>
          static std::false_type b(...);
          template <class U, _ol<1> = nullptr>
          static auto b(U &v) -> decltype(begin(v), std::true_type());
          template <class U, _ol<0> = nullptr>
          static std::false_type e(...);
          template <class U, _ol<1> = nullptr>
          static auto e(U &v) -> decltype(end(v), std::true_type());
          // return types
          using b_return = decltype(b<T>(std::declval<T&>()));
          using e_return = decltype(e<T>(std::declval<T&>()));
        public:
          static const bool value = b_return::value && e_return::value;
        };
      }
    
      // holder class for data
      template<class T, class CharT = char, class TraitT = std::char_traits<CharT>>
      struct decor
      {
        static const int xindex;
        std::basic_string<CharT, TraitT> prefix, delimiter, postfix;
        decor(std::basic_string<CharT, TraitT> const & pre = "",
          std::basic_string<CharT, TraitT> const & delim = "",
          std::basic_string<CharT, TraitT> const & post = "")
          : prefix(pre), delimiter(delim), postfix(post) {}
      };
    
      template<class T, class charT, class traits>
      int const decor<T, charT, traits>::xindex = std::ios_base::xalloc();
    
      namespace detail
      {
    
        template<class T, class CharT, class TraitT>
        void manage_decor(std::ios_base::event evt, std::ios_base &s, int const idx)
        {
          using deco_type = decor<T, CharT, TraitT>;
          if (evt == std::ios_base::erase_event)
          { // erase deco
            void const * const p = s.pword(idx);
            if (p)
            {
              delete static_cast<deco_type const * const>(p);
              s.pword(idx) = nullptr;
            }
          }
          else if (evt == std::ios_base::copyfmt_event)
          { // copy deco
            void const * const p = s.pword(idx);
            if (p)
            {
              auto np = new deco_type{ *static_cast<deco_type const * const>(p) };
              s.pword(idx) = static_cast<void*>(np);
            }
          }
        }
    
        template<class T> struct clearer {};
    
        template<class T, class CharT, class TraitT>
        std::basic_ostream<CharT, TraitT>& operator<< (
          std::basic_ostream<CharT, TraitT> &s, clearer<T> const &)
        {
          using deco_type = decor<T, CharT, TraitT>;
          void const * const p = s.pword(deco_type::xindex);
          if (p)
          { // delete if set
            delete static_cast<deco_type const *>(p);
            s.pword(deco_type::xindex) = nullptr;
          }
          return s;
        }
    
        template <class CharT> 
        struct default_data { static const CharT * decor[3]; };
        template <> 
        const char * default_data<char>::decor[3] = { "", ", ", "" };
        template <> 
        const wchar_t * default_data<wchar_t>::decor[3] = { L"", L", ", L"" };
    
      }
    
      // Clear decoration for T
      template<class T>
      detail::clearer<T> clear() { return{}; }
      template<class T, class CharT, class TraitT>
      void clear(std::basic_ostream<CharT, TraitT> &s) { s << detail::clearer<T>{}; }
    
      // impose decoration on ostream
      template<class T, class CharT, class TraitT>
      std::basic_ostream<CharT, TraitT>& operator<<(
        std::basic_ostream<CharT, TraitT> &s, decor<T, CharT, TraitT> && h)
      {
        using deco_type = decor<T, CharT, TraitT>;
        void const * const p = s.pword(deco_type::xindex);
        // delete if already set
        if (p) delete static_cast<deco_type const *>(p);
        s.pword(deco_type::xindex) = static_cast<void *>(new deco_type{ std::move(h) });
        // check whether we alread have a callback registered
        if (s.iword(deco_type::xindex) == 0)
        { // if this is not the case register callback and set iword
          s.register_callback(detail::manage_decor<T, CharT, TraitT>, deco_type::xindex);
          s.iword(deco_type::xindex) = 1;
        }
        return s;
      }
    
      template<class T, class CharT = char, class TraitT = std::char_traits<CharT>>
      struct defaulted
      {
        static inline decor<T, CharT, TraitT> decoration()
        {
          return{ detail::default_data<CharT>::decor[0],
            detail::default_data<CharT>::decor[1],
            detail::default_data<CharT>::decor[2] };
        }
      };
    
      template<class T, class CharT = char, class TraitT = std::char_traits<CharT>>
      decor<T, CharT, TraitT> decoration(
        std::basic_string<CharT, TraitT> const & prefix,
        std::basic_string<CharT, TraitT> const & delimiter,
        std::basic_string<CharT, TraitT> const & postfix)
      {
        return{ prefix, delimiter, postfix };
      }
    
      template<class T, class CharT = char,
      class TraitT = std::char_traits < CharT >>
        decor<T, CharT, TraitT> decoration(
          std::basic_string<CharT, TraitT> const & delimiter)
      {
        using str_type = std::basic_string<CharT, TraitT>;
        return{ defaulted<T, CharT, TraitT>::decoration().prefix,
          delimiter, defaulted<T, CharT, TraitT>::decoration().postfix };
      }
    
      template<class T, class CharT = char,
      class TraitT = std::char_traits < CharT >>
        decor<T, CharT, TraitT> decoration(CharT const * const prefix,
          CharT const * const delimiter, CharT const * const postfix)
      {
        using str_type = std::basic_string<CharT, TraitT>;
        return{ str_type{ prefix }, str_type{ delimiter }, str_type{ postfix } };
      }
    
      template<class T, class CharT = char,
      class TraitT = std::char_traits < CharT >>
        decor<T, CharT, TraitT> decoration(CharT const * const delimiter)
      {
        using str_type = std::basic_string<CharT, TraitT>;
        return{ defaulted<T, CharT, TraitT>::decoration().prefix,
          str_type{ delimiter }, defaulted<T, CharT, TraitT>::decoration().postfix };
      }
    
      template<typename T, std::size_t N, std::size_t L>
      struct tuple
      {
        template<class CharT, class TraitT>
        static void print(std::basic_ostream<CharT, TraitT>& s, T const & value,
          std::basic_string<CharT, TraitT> const &delimiter)
        {
          s << std::get<N>(value) << delimiter;
          tuple<T, N + 1, L>::print(s, value, delimiter);
        }
      };
    
      template<typename T, std::size_t N>
      struct tuple<T, N, N>
      {
        template<class CharT, class TraitT>
        static void print(std::basic_ostream<CharT, TraitT>& s, T const & value,
          std::basic_string<CharT, TraitT> const &) {
          s << std::get<N>(value);
        }
      };
    
    }
    
    template<class CharT, class TraitT>
    std::basic_ostream<CharT, TraitT> & operator<< (
      std::basic_ostream<CharT, TraitT> &s, std::tuple<> const & v)
    {
      using deco_type = pretty::decor<std::tuple<void*>, CharT, TraitT>;
      using defaulted_type = pretty::defaulted<std::tuple<void*>, CharT, TraitT>;
      void const * const p = s.pword(deco_type::xindex);
      auto const d = static_cast<deco_type const * const>(p);
      s << (d ? d->prefix : defaulted_type::decoration().prefix);
      s << (d ? d->postfix : defaulted_type::decoration().postfix);
      return s;
    }
    
    template<class CharT, class TraitT, class ... T>
    std::basic_ostream<CharT, TraitT> & operator<< (
      std::basic_ostream<CharT, TraitT> &s, std::tuple<T...> const & v)
    {
      using deco_type = pretty::decor<std::tuple<void*>, CharT, TraitT>;
      using defaulted_type = pretty::defaulted<std::tuple<void*>, CharT, TraitT>;
      using pretty_tuple = pretty::tuple<std::tuple<T...>, 0U, sizeof...(T)-1U>;
      void const * const p = s.pword(deco_type::xindex);
      auto const d = static_cast<deco_type const * const>(p);
      s << (d ? d->prefix : defaulted_type::decoration().prefix);
      pretty_tuple::print(s, v, d ? d->delimiter : 
        defaulted_type::decoration().delimiter);
      s << (d ? d->postfix : defaulted_type::decoration().postfix);
      return s;
    }
    
    template<class T, class U, class CharT, class TraitT>
    std::basic_ostream<CharT, TraitT> & operator<< (
      std::basic_ostream<CharT, TraitT> &s, std::pair<T, U> const & v)
    {
      using deco_type = pretty::decor<std::pair<T, U>, CharT, TraitT>;
      using defaulted_type = pretty::defaulted<std::pair<T, U>, CharT, TraitT>;
      void const * const p = s.pword(deco_type::xindex);
      auto const d = static_cast<deco_type const * const>(p);
      s << (d ? d->prefix : defaulted_type::decoration().prefix);
      s << v.first;
      s << (d ? d->delimiter : defaulted_type::decoration().delimiter);
      s << v.second;
      s << (d ? d->postfix : defaulted_type::decoration().postfix);
      return s;
    }
    
    
    template<class T, class CharT = char,
    class TraitT = std::char_traits < CharT >>
      typename std::enable_if < pretty::detail::is_range<T>::value,
      std::basic_ostream < CharT, TraitT >> ::type & operator<< (
        std::basic_ostream<CharT, TraitT> &s, T const & v)
    {
      bool first(true);
      using deco_type = pretty::decor<T, CharT, TraitT>;
      using default_type = pretty::defaulted<T, CharT, TraitT>;
      void const * const p = s.pword(deco_type::xindex);
      auto d = static_cast<pretty::decor<T, CharT, TraitT> const * const>(p);
      s << (d ? d->prefix : default_type::decoration().prefix);
      for (auto const & e : v)
      { // v is range thus range based for works
        if (!first) s << (d ? d->delimiter : default_type::decoration().delimiter);
        s << e;
        first = false;
      }
      s << (d ? d->postfix : default_type::decoration().postfix);
      return s;
    }
    
    #endif // pretty_print_0x57547_sa4884X_0_1_h_guard_
    
    0 讨论(0)
  • 2020-11-21 07:56

    Coming out of one of the first BoostCon (now called CppCon), I and two others worked on a library to do just this. The main sticking point was needing to extend namespace std. That turned out to be a no-go for a boost library.

    Unfortunately the links to the code no longer work, but you might find some interesting tidbits in the discussions (at least those that aren't talking about what to name it!)

    http://boost.2283326.n4.nabble.com/explore-Library-Proposal-Container-Streaming-td2619544.html

    0 讨论(0)
  • 2020-11-21 07:57

    This has been edited a few times, and we have decided to call the main class that wraps a collection RangePrinter

    This should work automatically with any collection once you have written the one-time operator<< overload, except that you will need a special one for maps to print the pair, and may want to customise the delimiter there.

    You could also have a special "print" function to use on the item instead of just outputting it direct. A bit like STL algorithms allow you to pass in custom predicates. With map you would use it this way, with a custom printer for the std::pair.

    Your "default" printer would just output it to the stream.

    Ok, let's work on a custom printer. I will change my outer class to RangePrinter. So we have 2 iterators and some delimiters but have not customised how to print the actual items.

    struct DefaultPrinter
    {
       template< typename T >
       std::ostream & operator()( std::ostream& os, const T& t ) const
       {
         return os << t;
       }
    
       // overload for std::pair
       template< typename K, typename V >
       std::ostream & operator()( std::ostream & os, std::pair<K,V> const& p)
       {
          return os << p.first << '=' << p.second;
       }
    };
    
    // some prototypes
    template< typename FwdIter, typename Printer > class RangePrinter;
    
    template< typename FwdIter, typename Printer > 
      std::ostream & operator<<( std::ostream &, 
            RangePrinter<FwdIter, Printer> const& );
    
    template< typename FwdIter, typename Printer=DefaultPrinter >
    class RangePrinter
    {
        FwdIter begin;
        FwdIter end;
        std::string delim;
        std::string open;
        std::string close;
        Printer printer;
    
        friend std::ostream& operator<< <>( std::ostream&, 
             RangePrinter<FwdIter,Printer> const& );
    
    public:
        RangePrinter( FwdIter b, FwdIter e, Printer p,
             std::string const& d, std::string const & o, std::string const& c )
          : begin( b ), end( e ), printer( p ), open( o ), close( c )
        {
        } 
    
         // with no "printer" variable
        RangePrinter( FwdIter b, FwdIter e,
             std::string const& d, std::string const & o, std::string const& c )
          : begin( b ), end( e ), open( o ), close( c )
        {
        } 
    
    };
    
    
    template<typename FwdIter, typename Printer>
    std::ostream& operator<<( std::ostream& os, 
              RangePrinter<FwdIter, Printer> const& range )
    {
        const Printer & printer = range.printer;
    
        os << range.open;
        FwdIter begin = range.begin, end = range.end;
    
        // print the first item
        if (begin == end) 
        { 
          return os << range.close; 
        }
    
        printer( os, *begin );
    
        // print the rest with delim as a prefix
        for( ++begin; begin != end; ++begin )
        {
           os << range.delim;
           printer( os, *begin );
        }
        return os << range.close;
    }
    

    Now by default it will work for maps as long as the key and value types are both printable and you can put in your own special item printer for when they are not (as you can with any other type), or if you do not want = as the delimiter.

    I am moving the free-function to create these to the end now:

    A free-function (iterator version) would look like something this and you could even have defaults:

    template<typename Collection>
    RangePrinter<typename Collection::const_iterator> rangePrinter
        ( const Collection& coll, const char * delim=",", 
           const char * open="[", const char * close="]")
    {
       return RangePrinter< typename Collection::const_iterator >
         ( coll.begin(), coll.end(), delim, open, close );
    }
    

    You could then use it for std::set by

     std::cout << outputFormatter( mySet );
    

    You can also write free-function version that take a custom printer and ones that take two iterators. In any case they will resolve the template parameters for you, and you will be able to pass them through as temporaries.

    0 讨论(0)
提交回复
热议问题