This library, UP (Universal Print), helps you to std::cout
most C++ STL containers in addition to the primitive types, as long as you make the underlying object T
cout-able.
For those not overloading operator<<
, UP generates default outputs with the best effort, which works well in most cases.
The header is here.
Usage
Primitive Types
UP can print containers of primitive types with customized control.
std::vector<int> vec{1, 2, 3, 4, 5};
std::cout << util::pre(vec) << std::endl;
// [1, 2, 3, 4, 5]
std::cout << util::pre(vec, 2 /* limit */) << std::endl;
// [1, ..., 4] (sz: 5, ommitted 3)
std::cout << util::pre(vec, 0 /* limit */) << std::endl;
// [...]
UP works with adaptors, e.g., std::queue
, std::stack
, without data movement.
std::queue<int> queue; // assume it has [1, 2, 3, 4]
std::cout << "queue:" << util::pre(queue) << std::endl
// queue: [0, 1, 2, 3]
// ** YES, UP works with queue.
Custom Cout-able Types
UP can handle containers of custom types as long as you make it cout-able.
For example, Person
is a cout-able type that overloads operator<<
in a standard way.
struct Person
{
std::string name;
int age;
};
std::ostream &operator<<(std::ostream &os, const Person &p)
{
os << "{Person " << p.name << " age: " << p.age << "}";
return os;
};
In this case, UP handles the output of most STL containers for you automatically.
For vector (array, list, forward_list, etc)
std::vector<Person> persons{
Person{.name = "A", .age = 25},
Person{.name = "B", .age = 30},
Person{.name = "C", .age = 45},
};
std::cout << util::pre(persons) << std::endl;
// [{Person A age: 25}, {Person B age: 30}, {Person C age: 45}]
For map (unordered_map, unordered_multimap, set, unordered_set, unordered_multiset, etc)
std::map<std::string, Person> m;
m["boss"] = Person{.name = "A", .age = 70};
m["employee"] = Person{.name = "B", .age = 30};
m["employee2"] = Person{.name = "C", .age = 30};
m["employee4"] = Person{.name = "D", .age = 30};
std::cout << "map: " << util::pre(m) << std::endl;
// map: {("boss", {Person A age: 70}), ("employee", {Person B age: 30}), ("employee2", {Person C age: 30}), ("employee4", {Person D age: 30})}
For two-dimention vector (multi-dimentional array, C-style array, etc)
std::vector<std::vector<Person>> matrix;
matrix.push_back(
{Person{.name = "A", .age = 10}, Person{.name = "B", .age = 20}});
matrix.push_back(
{Person{.name = "C", .age = 30}, Person{.name = "D", .age = 40}});
std::cout << "matrix: " << util::pre(matrix) << std::endl;
// [[{Person A age: 10}, {Person B age: 20}], [{Person C age: 30}, {Person D age: 40}]]
Not Cout-able Types: Best Effort Output
For the not cout-able types, UP provides a best-effort output.
struct Obj
{
int x;
std::string s;
std::vector<int> v;
std::map<int, int> m;
std::atomic<int> a;
bool b;
};
// ** No operator<< overloaded.
Obj obj = {42, "42", {1, 2, 3}, {{5, 2}, {7, 11}}, 10, false};
std::cout << util::pre(obj) << std::endl;
// {Obj <42, "42", [1, 2, 3], {(5, 2), (7, 11)}, atomic(10), false>}
UP is able to retrieve the typename, i.e., Obj
.
Limitations:
The labels of the class are wiped out. UP is unable to preserve them since C++ does not support reflection.
This feature heavily relies on structured binding (C++17). Therefore, it does not recognize
- C-style array
- std::optional
- ... (maybe more)
Solve compile failure by explicitly overloading operator<<
in this case.
This feature borrowed codes from ezprint.
Other STL Wrappers and C-style Types
UP gives more friendly outputs to other STL utilities/wrappers and C-style types.
For pair and tuple:
std::pair<int, int> pair{100, 200};
std::cout << "pair: " << util::pre(pair) << std::endl;
// pair: (100, 200)
std::tuple<Person, int, int, int> t{
Person{.name = "tuple", .age = 4}, 2, 3, 4};
std::cout << "tuple: " << util::pre(t) << std::endl;
// tuple: <{Person tuple age: 4}, 2, 3, 4>
UP handles std::tuple<Types...>
well.
UP recognizes C-style array, but does not mess up with C-style string:
int c_array[] = {2, 4, 6, 8, 10};
std::cout << "c style array: " << util::pre(c_array) << std::endl;
// c style array: [2, 4, 6, 8, 10]
std::cout << "c style string: " << util::pre("hello world") << std::endl;
// c style string: "hello world"
// ** NOTE: will not show the ugly ['h', 'e', 'l', ...] version.
UP gives more friendly outputs to the primitive type.
std::cout << "c style string: " << util::pre("hello world") << std::endl;
// c style string: "hello world"
std::cout << "char: " << util::pre('a') << std::endl;
// char: 'a'
std::cout << "boolean: " << util::pre(true) << std::endl;
// boolean: true
With Variable Name
UP provides a convenient PRE
and PRES
to show variables with their names.
int hey_you = 5;
std::cout << PRE(hei_you) << std::endl;
// hei_you: 5
int a = 10;
float b = 5;
const char *name = "Bin";
std::vector<int> vec{1, 2, 3};
std::cout << PRES(a, b, name, vec) << std::endl;
// a: 10, b: 5, name: Bin, vec: [1, 2, 3]
Show Bits
UP provides pre_bin
, pre_oct
, pre_dec
, and pre_hex
to show a number in different forms.
Ensurance of No Data Movement
UP avoids any data movement, i.e., no copy and no move occurs.
Consider a non-copyable and non-movable type called Pin
.
class Pin
{
// All copy and move deleted
Pin() = delete;
Pin(const Pin &) = delete;
Pin(Pin &&) = delete;
Pin &operator=(const Pin &) = delete;
Pin &&operator=(Pin &&) = delete;
};
// with operator<< overloaded
UP is able to print it even if it is stored in some non-iterable adaptors, e.g., std::queue
.
std::queue<Pin> queue;
for (int i = 5; i < 10; ++i)
{
queue.emplace(i); // in-place construction
}
std::cout << util::pre(queue) << std::endl; // no copy occurred
// [{Pin 5}, {Pin 6}, {Pin 7}, {Pin 8}, {Pin 9}]
Applicability
UP supports most, if not all, STL wrappers. Including
- std::optional, std:: atomic
- std::shared_ptr, std::unique_ptr
- The adaptors, e.g., std::stack, std::queue, std::priority_queue, ... Yes, UP supports them without any copy.
Feature requests are welcomed.
For getting a std::string
, UP provides a similar util::pre_str
.
NOTE: the output of std::vector<bool>
is platform-specific, due to the specialization of bool vector in STL specification.
Use UP Universally
We recommend to use UP universally, i.e., use it under any interactions with std::ostream
.
Image you have nested classes, A
and B
.
struct A
{
int a;
};
struct B
{
A a; // nested here
int b;
};
Use UP to help you show any members recursively.
std::ostream &operator<<(std::ostream &os, const A &obj)
{
// use util::pre for int
os << "{A " << util::pre(obj.a) << "}";
return os;
};
std::ostream &operator<<(std::ostream &os, const B &obj)
{
// use util::pre for A and int
os << "{B a: " << util::pre(obj.a) << ", b: " << util::pre(obj.b) << "}";
return os;
};
Overhead
UP guarantees
However, UP is optimized for human-readability instead of performance. Use it out of critical path for any performance-critical programs.
Compile
mkdir build; cd build
cmake -DCMAKE_BUILD_TYPE=Debug ..
# or cmake -DCMAKE_BUILD_TYPE=Release ..
# for more options, see CMakeLists.txt
make -j
Run
$ ./bin/main