Skip to content

A header-only C++ library for pretty-printing collections (std::vector, std::map, etc.) and tuple-like types (std::tuple, std::pair, etc.), uncluding nested ones, with customizable formatting, via operator<<

License

Notifications You must be signed in to change notification settings

r0masaN/pretty_view

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

58 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

pretty_view

A header-only C++ library for pretty-printing collections (std::vector, std::map, etc.) and tuple-like types (std::tuple, std::pair, etc.), uncluding nested ones, with customizable formatting, via operator<<.

Standards: C++11-C++23+. Compilers: GCC/G++, MSVC, Clang.

TL;DR

#include "pretty_view/pretty_view.hpp"

using namespace rmsn;

std::vector<int> vec{1, 2, 3, 4, 5};
std::cout << vec;

Output:

[1, 2, 3, 4, 5]

Why pretty_view?

Unfortunately, C++ does'n provide a standard way to print containers and tuple-like types, expecially nested ones. This library solves that problem without modifying std namespace, while supporting nested structures, custom formatting, user-defined data types and containers.

Usage of the pretty_view

0. Ways to use

For now, there are 2 ways – 1st is using wrapper class pretty_view and 2nd without anything additional.

About differences – down through paragraphs.

1. Including and files

Include pretty_view's header:

#include "pretty_view/pretty_view.hpp"

Or import module:

import pretty_view;

Module's file pretty_view.ixx is located at the same directory "pretty_view" as pretty_view.hpp and pretty_view.tcc.

2. Namespaces

If you don't want to write rmsn:: every time:

using namespace rmsn;

There are 3 namespaces:

  • rmsn: core pretty_view class and overloaded operator<< functions;
  • rmsn::fmt: formatter class (with prefixes, delimiters and postfixes for formatting output) and global formatter format object (with default values);
  • rmsn::dtl: hidden technical helping tools like concepts and type traits.

3. Formatting output

If you want to customize prefixes, delimiters and postfixes, use:

rmsn::fmt::format.collection_prefix = "[";
rmsn::fmt::format.collection_delimiter = ", ";
rmsn::fmt::format.collection_postfix = "]";
rmsn::fmt::format.tuple_prefix = "{";
rmsn::fmt::format.tuple_delimiter = ", ";
rmsn::fmt::format.tuple_postfix = "}";

Default values are demonstrated above. You can set these variables to whatever you want.

Of course, you can create formatter object and use it in pretty_view wrapper (pass it as second parameter – pretty_view will contain const reference to it); by default all pretty_view objects use rmsn::fmt::format.

rmsn::fmt::formatter custom_format{};
// ...
rmsn::pretty_view pv{/* container */, custom_format};

4. Wrapping structures

4.1. 1st way

Wrap array/collection/tuple/etc. you want to print in pretty_view class:

std::vector<int> v{1, 2, 3, 4, 5}; // for example
rmsn::pretty_view pv{v, /* [optional] `formatter` object */};

You can also wrap structures of varying complex degrees of nesting:

std::map<int, std::tuple<int, std::vector<int>, std::string>> complex_map{
    {18, {10, {1, 2, 3}, "cat"}},
    {24, {48, {4, 51}, "dog"}}
};
rmsn::pretty_view pv1{complex_map};
// sometimes it's hard for compiler to deduce actual type of structure
// so you need to explicitly define it by yourself
rmsn::pretty_view<decltype<mega_map>> pv2{complex_map}; // the easiest way
rmsn::pretty_view<std::map<int, std::tuple<int, std::vector<int>, std::string>>> pv3{complex_map}; // the gigachad way

Wrapping is needed due to ADL mechanism in C++ which helps compiler to find correct operator<< overloading (mine) without explicit specifying a specific namespace.

In simple words: adding new operator<< overloading in std namespace is unsafe and unreliable so you can have our own overloadings in yours namespaces. But you don't want to write rmsn::operator<<(std::cout, {1, 2, 3}), do you? That's when ADL appears: compiler sees pretty_view object from rmsn namespace and at first it looks at the current translation unit, after – at namespace pretty_view came from (which is rmsn where my operator<< overloading is placed).

Important! Do not use temporary objects (both collection and formatter) in wrapper pretty_view, unless you instantly printing them. Lifetime of temporary objects ends at the end of the line they were declared!

Undefined behaviour:

rmsn::pretty_view pv{
    std::vector<int>{1, 2, 3, 4, 5},
    rmsn::fmt::formatter{
        .collection_prefix = "<",
        .collection_postfix = ">"
    }
};
std::cout << pv;

Still safe but very close to death:

std::cout << rmsn::pretty_view{
    std::vector<int>{1, 2, 3, 4, 5},
    rmsn::fmt::formatter{
        .collection_prefix = "<",
        .collection_postfix = ">"
    }
};
// because lifetime of temporary vector ends after operator<< performing
// it's still safe to print it

For you, it's better to use pretty_view wrapper with lvalue objects (objects that have name identifier and place in memory), not rvalue (temporary).

4.2. 2nd way

Luckily, this way fixes that annoying linkage to wrapper structure pretty_view. I just figured out that ADL not that really necessary in this situation, you can just write:

#include "pretty_view/pretty_view.hpp"

using namespace rmsn;

and with no troubles use my custom operator<< overloading because it was written in such way that compiler will find it and 100% add to Candidate Function Set without any ambiguous calls to standard operator<< overloadings from std namespace.

5. Stream output

5.1. 1st way

As usually with std::cout, you can use operator<< with any std::ostream you want, just pass into it pretty_view object:

std::vector<int> v{1, 2, 3, 4, 5};
rmsn::pretty_view pv{v};
std::ostream& os = std::cout; // just for example, you're free to use any possible std::ostream inheritor
os << pv;
[1, 2, 3, 4, 5]

Enjoy the results :)

std::map<int, std::tuple<int, std::vector<int>, std::string>> complex_map{
    {18, {10, {1, 2, 3}, "cat"}},
    {24, {48, {4, 51}, "dog"}}
};
std::cout << rmsn::pretty_view{complex_map};
[{18, {10, [1, 2, 3], cat}}, {24, {48, [4, 51], dog}}]

5.2. 2nd way

As long as I got rid of proxy class, no needed to wrap data structure into something. Just pass it into operator<<:

using namespace rmsn;

std::cout << std::vector<int>{1, 2, 3, 4, 5} << std::endl;

std::map<int, std::tuple<int, std::vector<int>, std::string>> complex_map{
    {18, {10, {1, 2, 3}, "cat"}},
    {24, {48, {4, 51}, "dog"}}
};
std::cout << complex_map;
[1, 2, 3, 4, 5]
[{18, {10, [1, 2, 3], cat}}, {24, {48, [4, 51], dog}}]

Support for non-STL data structures

The best thing is you can use your own collections/tuple-likes or data containers from other libraries, they just must satisfy the concept is_collection_or_tuple_and_not_string_like from rmsn::dtl.

Good news is you don't need to care about scary things below unless you're going to write your own good-coded collection or tuple-like dta container.

There are simplified representations:

template<typename T>
concept is_collection_or_tuple_and_not_string_like =
        (is_collection<T> // is given type a collection or array
        || is_tuple_like<T>) // or is it a tuple or tuple-like (e.g. pair)
        && !is_string_like<T>; // and is it not a string-like (string, string_view, raw char array etc.)

If any data structure is satisfying this concept – congratulations, you can use it with pretty_view (or wrap-less) overloading. Otherwise, you will get compilation errors.

String-like objects consists of any char type (std::string, std::string_view, std::basic_string<T>, char *, etc.) will be printed like strings (using standard operator<< overloadings from std namespace), not like a collection of chars.

Also, of course, you can have any data structure with your custom types and pretty_view (or wrap-less) overloading will correctly use your custom operator<< overloading for it. Quick example:

struct MyStruct {
    int i;
    std::string s;
    
    friend std::ostream& operator<<(std::ostream& os, const MyStruct& ms) {
        os << "MyStruct{" << ms.i << ", \"" << ms.s << "\"}";
        return os;
    }
};

std::vector<MyStruct> my_structs{
    {5, "Jeff"},
    {1, "Brent"}
};
std::cout << my_structs;
[MyStruct{5, "Jeff"}, MyStruct{1, "Brent"}]

Ongoing updates

  • 3rd way (ostream wrapper);
  • 4th way (function that takes ostream, data structure and formatter);
  • 5th way (std::formatter for std::format/std::print).

Requirements

  • Standards: C++11, C++14, C++17, C++20, C++23, C++23+;
  • Compilers: G++/GCC (confirmed), MSVC & Clang/LLVM (potentially).

About

A header-only C++ library for pretty-printing collections (std::vector, std::map, etc.) and tuple-like types (std::tuple, std::pair, etc.), uncluding nested ones, with customizable formatting, via operator<<

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages