4

I'm writing approval tests using the excellent ApprovalTests.cpp library. This library automates generation of "snapshots" of results from a function. Snapshots are generated serializing results of type T to a file using the ostream& operator<< (T val).

This operator has always been the C++ convention for formatting some value to a textual representation. While primitive types support this operator and you can write your own implementation for custom types, there isn't a standard implementation for STL containers like std::vector.

You can implement your own, even using other libraries like fmt or pprint. Here are some example with similar outputs. I use the generic type STREAM as parameter instead of the concrete type ostream as recommended by ApprovalTests.cpp but the idea doesn't change.

for loop

template <typename STREAM, typename T> STREAM& operator<<(STREAM& os, const std::vector<T>& vec) {
  os << "[";
  for (const auto& x : vec) {
    os << x << ", ";
  }
  os << "]";
  return os;
}

ostream_iterator

template <typename T> std::ostream& operator<<(std::ostream& os, const std::vector<T>& v) {
  using namespace std;
  os << "[";
  copy(v.begin(), v.end(), ostream_iterator<T>(os, ", "));
  os << "]";
  return os;
}

fmt

https://github.com/fmtlib/fmt

template <typename STREAM, typename T> STREAM& operator<<(STREAM& os, const std::vector<T>& vec) {
  fmt::print(os, "[{}]", fmt::join(vec, ", "));
  return os;
}

With <fmt/ranges.h header:

template <typename STREAM, typename T> STREAM& operator<<(STREAM& os, const std::vector<T>& vec) {
  fmt::print(os, "{}", vec);
  return os;
}

Pretty print

https://github.com/p-ranav/pprint

template <typename STREAM, typename T> STREAM& operator<<(STREAM& os, const std::vector<T>& vec) {
  pprint::PrettyPrinter printer{os};
  printer.print(vec);
  return os;
}

cxx-prettyprint

Just include prettyprint.hpp and it works for STL containers.

This seems to be the simplest solution, but has the same problem as other solutions, it may break other code.

A convention?

After some experience with Rust, I find tedious to do this for every C++ STL container. Doing this may break other code where, for example, the same operator has been overloaded for vector.

In Rust you can just add #[Debug] over the struct you want to format to text and it can be automatically converted to a textual representation, or implement the trait yourself if you need some non-canonical representation. It is responsibility of a struct author to define its Debug implementation. This is why every container in the Rust standard library has its own Debug implementation.

I'm asking if some convention exists in C++ or there is some similar proposal for the standard. It could be useful for approval tests, like in my case, but also for logging or debugging (the debugger could use this formatter to show a variable value to the user).

Alessandro Pezzato
  • 8,603
  • 5
  • 45
  • 63
  • All containers provide iterators; a display function that takes a pair of iterators is far better than hand-wiring a different function for each container type. Once you've written that function to show things the way you want them, you can use it for every container. – Pete Becker Apr 21 '20 at 14:30
  • To be safe, as Rust, you should not write functions/traits, if you don't own neither the class nor the traits. So `operator<<(STREAM&, const std::vector&)` is risky. It would be fine if instead of template for STREAM, you would have your own class. – Jarod42 Apr 21 '20 at 14:41
  • Hi, on behalf of myself and Llewellyn Falco - the two maintainers of ApprovalTests.cpp - thanks very much for this question, and the really useful links in it... we weren't aware of most of these libraries - we'll experiment with them when we can, and then reply here if we come up with any useful comments... – Clare Macrae Apr 21 '20 at 18:25
  • @PeteBecker you're right, it works where iterators yield a simple item (like `vector` or `list`) but what about `std::map`? – Alessandro Pezzato Apr 21 '20 at 22:17
  • @Jarod42 that's the point, the operator is not implemented in the standard library and it is too risky to craft it yourself. This is why I'm asking if there is at least a convention or a quasi-standard for this case. In this particular case, having my own class (or ApprovalTests class) seems to be a valid solution. – Alessandro Pezzato Apr 21 '20 at 22:24
  • @ClareMacrae thank you for the excellent tool! I'm going to experiment more and let you know if I find something useful. – Alessandro Pezzato Apr 21 '20 at 22:24
  • 1
    @ClareMacrae I've forked your library to make some experiments. Here I'm using fmt to automatically format supported containers: https://github.com/alepez/ApprovalTests.cpp/blob/fmt-ranges/ApprovalTests/utilities/StringUtils.h#L59-L72 This is a test project: https://github.com/alepez/approvaltests-experiments/blob/master/tests/experiments.cpp I've specialized StringUtils::toString instead of implementing `operator<<`, so it doesn't collide with possible other implementations of `operator<<` for std containers. – Alessandro Pezzato Apr 23 '20 at 16:53
  • @AlessandroPezzato I had been meaning to put in a comment referring to https://github.com/approvals/ApprovalTests.cpp/issues/6 - which I think is advocating for exactly what you have done. So instead I've added a comment on that ticket - https://github.com/approvals/ApprovalTests.cpp/issues/6#issuecomment-618686248... Thank you!!!! I would love to work with you to add your ideas to ApprovalTests.cpp...! – Clare Macrae Apr 23 '20 at 21:43
  • @AlessandroPezzato I put the above in a comment rather than an answer, as I really think your demos should **become** the answer - and you should got the rep for them!! – Clare Macrae Apr 23 '20 at 21:45

1 Answers1

3

I'm not aware of any convention or standard proposal for printing containers. However the {fmt} library can print anything range- and tuple-like: https://fmt.dev/latest/api.html#ranges-and-tuple-formatting so you could probably integrate it with ApprovalTests and avoid defining ostream insertion operators yourself.

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

vitaut
  • 49,672
  • 25
  • 199
  • 336
  • 2
    Thank you for the response. I've been using fmt for a long time and I'm very grateful it has come to std. I'm trying to integrate it in ApprovalTests.cpp: https://github.com/alepez/ApprovalTests.cpp/blob/fmt-ranges/ApprovalTests/utilities/StringUtils.h#L59-L72 It seems to work as expected, here an example project tested with it: https://github.com/alepez/approvaltests-experiments/blob/master/tests/experiments.cpp Do you have any advice? – Alessandro Pezzato Apr 23 '20 at 16:49
  • 1
    Very cool. The only advice that I have is try avoiding `fmt/ostream.h` unless absolutely necessary. – vitaut Apr 23 '20 at 18:05
  • 1
    Yes, you're right. In that case, where `std::string` is the return type, I can just use `format` instead of `print` – Alessandro Pezzato Apr 23 '20 at 20:21