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";
}
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 << "]";
}
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...