Hacker News new | past | comments | ask | show | jobs | submit login
Pprint: Pretty Printer for Modern C++ (github.com/p-ranav)
100 points by starbugs on April 29, 2019 | hide | past | favorite | 35 comments



The library {fmt} (http://fmtlib.net/latest/index.html) is on track to be included in the standard.


Wow, this is so much better. I hope they deprecate cout and its inane operator overloads. Call me old-fashioned but I like an operator to do something at least logically consistent with its intended use. Shifting shouldn't print or read from a stream, what on earth made them think that's a good idea?

One thing that drives me crazy about C++ is that if you incorporate a few different third-party libraries you often end up writing a few different DSLs. Everyone has their own subset of operator overloads that do different things in different contexts and you just have to remember which one does what based on your receiver type -- which is often 'auto' these days anyways. It's maddening. Worse still, the STL plays this game. Time to move on.


Why? I think there’s a reason behind it. I always imagined it to to be shifting bits along the stream.


It's about violating the principle of least surprise. If you create a culture of redefining any and all operators all the time to effectively create DSLs, you can't infer what anything does. You can even override operator, for some reason. Just looking at a piece of code you have no idea what it's actually intending to do.


I agree with you that there are some DSLs which are very difficult to understand at first look. However, to me shifting, and pushing/pulling over streams are not so dissimilar.

There are lots of cases where overriding operators makes sense and is intuitive to the reader, for instance when you are dealing with monoids, groups or rings.


The issue is not that each one in isolation is particularly problematic, the issue IMO is when they get assembled by an end-user into a pile of mixed metaphors.

  fmt::memory_buffer out1;
  std::ostream out2 = std::cout;
  auto out3 = printer(stream);
  auto out4 = my_printer();

  format_to(out1, ...)
  out2 << "Hi..."
  out3.print(5);
  out3 += 6;
These all do the same but it's a total mess.


Anyone who writes like that needs to have a chat with Uncle Bob.


Haha, that's fair :) I'm trying to convey that sometimes as an end developer, you pick a number of third party libraries based on what they do -- not what their API definition looks like. Then you end up in an awkward mix-n-match of DSLs that are hard to reason about without a deep knowledge of each because the operators you normally key off of are redefined. Maybe thats why I'm not a C++ developer haha.


Oh that’s great! Where have you seen that it might be included in the standard?


Has been presented to the committee, first steps in any case. The proposal: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p064...


Interesting, thank you for sharing. Nothing new since 2017 if I understand correctly? I understand that the C++ proposal system is very slow, still it would be nice to know if that’s still something people are working on. I would personally love to see something like this in the standard!


A lot of work has been done since 2017: the proposal is at its 9th revision (http://fmtlib.net/D0645R9.html) and is prioritized (but not accepted yet) for C++20.


Awesome, thank you for sharing


FWIW {fmt} already supports formatting of containers, tuples and ranges as of version 5.0 (https://github.com/fmtlib/fmt/releases/tag/5.0.0), for example:

  #include <fmt/ranges.h>

  std::vector<int> v = {1, 2, 3};
  fmt::print("{}", v); // prints {1, 2, 3}


This reminds me very strongly of Google's string::Substitute(). Super convenient, and IIRC it was faster than sprintf as well.

Looks like it's now public as a part of Abseil: https://github.com/abseil/abseil-cpp/blob/bf29470384a101b307...


I also recently created a pretty printer library (https://github.com/calebzulawski/hippo).

In some ways they are similar, though I felt it was valuable to avoid using ostreams at all, and store indentation levels separately from the printed string. Additionally, I think reflection macros are especially helpful for creating printers for user-defined types.


I like your idea to support separately tracked indentation levels.


Thanks! It really makes writing printers very easy.


I find it interesting that this library requires the creation of a "printer" object instead of the (IMO more standard) definition of operator<<. Is there any specific reason why the author chose to go this way?


A huge problem with cout and << is that setting flags (such as to print numbers in hex) is global and a destructive update. If you don't save the stream state beforehand and restore it you change the flags for printing in unrelated code.

So making an object for printing probably helps with scope issues like that.


Yeah, I always wondered was << just an ill advised attempt to show off operator overloading (I’m sure things had it before C++, but it definitely wasn’t commonplace). It never made much sense to me.


They were adding features to the language (e.g., "Koenig lookup") in the CFront era to accommodate iostreams.

I was sad that they were spending a bunch of resources on that relatively useless stuff, rather than a sorely needed set of standard collection classes, not to mention a string class. And by "standard" I don't mean actual standards -- it was far too early in the game for that -- but even just enough to get by.


ADL is important for customization points in modern C++.


Does anyone know why C++ uses the shift operator for streams? It’s always rubbed me the wrong way


Originally C++ lacked templates, and even once it got templates it was 20 (?) years before it got variadic templates. Operator overloads were added early, however, so using an operator for this was a "hack" to make a type safe print function. (It probably also served as a demo application for operator overloading.)

If you squint enough, it looks like a fluent interface, just fluent interfaces wouldn't become mainstream until later. That is, "cout << a << b << c;" is pretty close to "cout.print(a).print(b).print(c);"

Another reason to use an operator is that if you use free functions (without variadic parameters) then your parens and nesting stacks up: "print(print(print(cout, a), b), c);"

An infix operator doesn't have that problem.

In modern C++, there is at least the possibility to replace "<<" with something better for printing.


I mean, it's also looks intuitive. if you imagine the arrows as the data fllowing.


Among its many flaws, you can't really use streams for output where internationalization will ever be a consideration, which immediately eliminates many potential use cases.


phaedrus explained why use an infix operator, but why specially the shift operator?

My guess: because it has very low precedence, allowing stuff like:

> cout << a + b * c << " is the result\n";

Instead of:

> cout << (a + b * c) << " is the result\n";


Yes! They look cute, sure. But aren't they the wrong precedence? I think its because its a little-used operator that can be overloaded without interfering with normal expressions within the stream I/o invocation.


My guess is it's mimicking a Python library of similar name and purpose.

edit: the similarities are pretty obvious, but for those too lazy to Google, see https://docs.python.org/3/library/pprint.html


Efficiency, likely. Using standard function call syntax lets you know all arguments up front. Using the stream operator doesn't. There's likely a way to do it with templates, but this is easier.

Edit: this also lets them assert on argument types.


This is a bit like '%r' or repr() in Python, the Debug trait in Rust, or the Show class in Haskell, but it doesn't really address what most pretty printers need: highly customized formatting, as used in Wadler/Leijen pretty printers.


Indeed. I wrote a Python pretty printer that uses a Wadler/Leijen algorithm, and it's a great fit. It doesn't take too long to learn how to use the layout primitives, and it allows for easy creation of higher level abstractions while still maintaining plenty of control.

https://github.com/tommikaikkonen/prettyprinter


There’s also https://github.com/tfc/pprintpp which is quite nice.


    pprint::PrettyPrinter printer;
    printer.print("x'x\"x");
And the output is

    "x'x"x"
So it doesn't actually escape any characters in the string. Same with newlines inside the string.




Join us for AI Startup School this June 16-17 in San Francisco!

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: