Home > front end >  Is there a better way to generic print containers in C 20/C 23?
Is there a better way to generic print containers in C 20/C 23?

Time:06-22

I use the following code to print generic C containers and exclude strings (thanks to this SO answer).

#include <iostream>
#include <vector>

template <typename T,
          template <typename ELEM, typename ALLOC = std::allocator<ELEM>>
          class Container>
std::ostream &operator<<(std::ostream &out, const Container<T> &container) {
  out << "[";
  bool first = true;
  for (const auto &elem : container) {
    if (first)
      first = false;
    else
      out << ", ";
    out << elem;
  }
  return out << "]";
}

int main() {
  std::vector<std::vector<int>> v{{1, 2}, {3, 4}};
  std::cout << v << "\n";
  return 0;
}

Which prints the 2d vector correctly

[[1, 2], [3, 4]]

And does not mess with outputting strings. Is there a cleaner (concept-based?) way to write the template in C 20 or C 23?

CodePudding user response:

The better way is to use {fmt}:

#include <fmt/ranges.h>
#include <vector>

int main() {
  std::vector<std::vector<int>> v{{1, 2}, {3, 4}};
  fmt::print("{}\n", v); // prints [[1, 2], [3, 4]]
}

With in C 23 will be available as std::print.


There are actually two issues with strings that you'd have to deal with:

  • string literals (like "[") need to not be included
  • strings (like actual std::string) need to not be included

You can get rid of both by rejecting anything convertible to string_view rather than requiring a specific shape of the container type like C<T, A=?> (which as I mentioned also eliminates things like std::array, std::span, and std::map). That would let you print std::array and std::span just fine, but then for std::map you have to deal with the problem of... how do you print std::pair? And then how do you print a std::pair where one of the elements is a range?

And then where do you put this operator overload? You can't put it in std (that's not allowed). But you also can't really put it not in std, since otherwise real code probably won't find it.

It's a complicated problem, which is why it's better to defer to a good library (like {fmt}) which handles this very well.

CodePudding user response:

You should not overload operators on types you don't own; you don't own std containers. And you should only overload operators in associated namespaces; you are not allowed to inject overloads into the std namespace.

This means you shouldn't quite do what you are doing.

template<class T>
struct pretty_print {
  T&& t;
};
template<class T>
pretty_print(T&&)->pretty_print<T>;

template<typename T>
concept printable = requires(T t) {
  { std::declval<std::ostream&>() << t } -> std::same_as<std::ostream&>;
};
template<printable T>
std::ostream& operator<<( std::ostream& os, pretty_print<T> pp ) {
  return os << pp.t;
}

this lets you do

void test1() {
  int x=42;
  std::cout << pretty_print{x} << "\n";
}

for any printable type x. We are now, however, free to add overloads for non-printable types.

template<std::ranges::input_range T>
requires (not printable<T>)
std::ostream& operator<<( std::ostream& os, pretty_print<T> pp ) {
  using std::begin; using std::end;
  os << "[";
  for (auto&& e : pp.t | std::ranges::views::take(1))
  {
    os << pretty_print{e};
  }
  for (auto&& e : pp.t | std::ranges::views::drop(1))
  {
      os << ", ";
      os << pretty_print{e};
  }
  os << "]";
  return os;
}

void test2() {
  std::vector<int> v0 = {1,2,3};
  std::cout << pretty_print{v0} << "\n";
  std::vector<std::vector<int>> v1 = {{1},{1,2},{1,2,3}};
  std::cout << pretty_print{v1} << "\n";
}

Now if we want to support pairs:

template<typename T>
concept pairlike = requires(T t) {
  { t.first };
  { t.second };
};

template<pairlike P>
std::ostream& operator<<( std::ostream& os, pretty_print<P> pp ) {
  os << "{ ";
  os << pretty_print{pp.t.first} << ", ";
  os << pretty_print{pp.t.second} << " }";
  return os;
}

and now maps work.

void test3() {
  std::map<int, int> m = {{0,1}, {1,2}, {2,4}};
  std::cout << pretty_print{m}  << "\n";
}

Live example.

CodePudding user response:

With concepts, you might require only container/range types and discard string-like types, something like:

template <typename Container>
requires std::ranges::input_range<const Container>
     && (!std::convertible_to<const Container, std::string_view>)
std::ostream &operator<<(std::ostream &out, const Container& container)
{
  out << "[";
  const char* sep = "";
  for (const auto &elem : container) {
    out << sep << elem;
    sep = ", ";
  }
  return out << "]";
}

Demo

With the caveats that generic template should involve at least one user type. Else it might conflict with possible future operator<< for std (or other library) containers...

  • Related