//
// Created by AdrianFreed on 9/24/20.
// << overloads for printing containers
// requires C++17
//
#ifndef CONTAINER_O_STREAM_H
#define CONTAINER_O_STREAM_H
#include <iostream>
#include <array>
#include <vector>
#include <set>
#include <unordered_set>
#include <map>
#include <unordered_map>
#include <tuple>
#include <deque>
#include <list>
#include <forward_list>
//Pairs
template<typename TA, typename TB>
auto &operator<<(std::ostream &os, const std::pair<TA, TB> &p) {
os << '(' << p.first << ',' << p.second << ')';
return os;
}
//Tuples
// variadic template metaprogram to iterate over the tuple elements
// requires C++17, replace with messier folds if using earlier version
template<size_t I = 0, typename... Tp>
void tuple_element_print(const std::tuple<Tp...> &t, std::ostream &os) {
os << std::get<I>(t);
if constexpr(I + 1 != sizeof...(Tp)) {
os << ", ";
tuple_element_print<I + 1>(t, os);
}
}
template<typename ...Types>
auto &operator<<(std::ostream &os, const std::tuple<Types ...> &t) {
os << "(T ";
tuple_element_print(t, os);
os << ")";
return os;
}
namespace CONTAINERSTREAM {
//Maps
template<typename C>
std::ostream &output(std::ostream &os, C &v, const char *first, const char *separator, const char *last) {
os << first;
for (auto it = v.cbegin(); it != v.cend(); ++it) {
os << it->first << ':' << it->second;
if (std::next(it) != v.cend())
os << separator;
}
os << last;
return os;
}
}
template<typename T, typename Tbis>
std::ostream &operator<<(std::ostream &os, const std::unordered_map<T, Tbis> &v) {
return CONTAINERSTREAM::output(os, v, "[Unorderedmap ", ", ", "]");
}
template<typename T, typename Tbis>
std::ostream &operator<<(std::ostream &os, const std::map<T, Tbis> &v) {
return CONTAINERSTREAM::output(os, v, "[Map ", ", ", "]");
}
template<typename T, typename Tbis>
std::ostream &operator<<(std::ostream &os, const std::multimap<T, Tbis> &v) {
return CONTAINERSTREAM::output(os, v, "[Multimap ", ", ", "]");
}
namespace CONTAINERSTREAM {
// The Rest
template<typename C>
std::ostream &
vectorarrayoutput(std::ostream &os, C &v, const char *first, const char *separator, const char *last) {
os << first;
for (auto it = v.cbegin(); it != v.cend(); ++it) {
os << *it;
if (std::next(it) != v.cend())
os << separator;
}
os << last;
return os;
}
}
// There may appear to be a lot of repetition in these functions
// The idea behind that is that you are invited to change the styling of
// each separator and bracket according to your own aesthetics or conventions
template<typename T, std::size_t N>
auto &operator<<(std::ostream &os, const std::array<T, N> &a) {
return CONTAINERSTREAM::vectorarrayoutput(os, a, "[Array ", ", ", "]");
}
template<typename T>
std::ostream &operator<<(std::ostream &os, const std::vector<T> &v) {
return CONTAINERSTREAM::vectorarrayoutput(os, v, "[Vector ", ", ", "]");
}
template<typename T>
std::ostream &operator<<(std::ostream &os, const std::set<T> &v) {
return CONTAINERSTREAM::vectorarrayoutput(os, v, "[Set ", ", ", "]");
}
template<typename T>
std::ostream &operator<<(std::ostream &os, const std::multiset<T> &v) {
return CONTAINERSTREAM::vectorarrayoutput(os, v, "[Multiset ", ", ", "]");
}
template<typename T>
std::ostream &operator<<(std::ostream &os, const std::list<T> &l) {
return CONTAINERSTREAM::vectorarrayoutput(os, l, "(List ", ", ", ")");
}
template<typename T>
std::ostream &operator<<(std::ostream &os, const std::forward_list<T> &l) {
return CONTAINERSTREAM::vectorarrayoutput(os, l, "(ForwardList ", ", ", ")");
}
template<typename T>
std::ostream &operator<<(std::ostream &os, const std::deque<T> &d) {
return CONTAINERSTREAM::vectorarrayoutput(os, d, "(Deque ", ", ", ")");
}
#endif //TEST_CONTAINER_O_STREAM_H
Branches now available on GitHub: master (the C++17 original above), cpp20, and cpp23. Each later branch shows how the same task collapses when you let the language do more of the work.
Concepts and ranges let the 8 nearly-identical operator<< overloads collapse into a single template constrained by std::ranges::input_range. Per-container labels become 1-line trait specializations. The std::apply + fold expression replaces the recursive tuple-printing helper.
///
// Created by AdrianFreed on 9/24/20.
// << overloads for printing containers
// C++20 modernization: concepts + ranges + std::apply fold
//
// Pedagogical notes (vs. the original C++17 version):
// * 8 near-identical operator<< overloads (vector/set/list/deque/...)
// collapse to ONE template constrained by std::ranges::input_range.
// * The recursive tupleElementPrint helper is replaced by std::apply
// plus a C++17 fold expression. (Valid in C++17 but more idiomatic
// once you have ranges to lean on for the rest.)
// * Per-container labels move into a tiny customization-point trait,
// so adding a new container is a 1-line specialization, not a 4-line
// operator<<.
// * The single overload is constrained to exclude std::string and
// anything convertible to std::string_view, so we don't hijack
// string output.
//
#ifndef CONTAINEROSTREAM_H
#define CONTAINEROSTREAM_H
#include <array>
#include <concepts>
#include <deque>
#include <forward_list>
#include <iostream>
#include <list>
#include <map>
#include <ranges>
#include <set>
#include <string>
#include <string_view>
#include <tuple>
#include <type_traits>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>
namespace containerstream {
// Per-container styling. Default is a square-bracketed list.
// Specialize this trait to give a container a custom label or brackets.
template <typename C>
struct style {
static constexpr const char* open = "[";
static constexpr const char* close = "]";
static constexpr const char* sep = ", ";
};
// Sequence-style
template <typename T, typename A> struct style<std::vector<T, A>> {
static constexpr const char* open = "[Vector "; static constexpr const char* close = "]"; static constexpr const char* sep = ", ";
};
template <typename T, std::size_t N> struct style<std::array<T, N>> {
static constexpr const char* open = "[Array "; static constexpr const char* close = "]"; static constexpr const char* sep = ", ";
};
template <typename T, typename A> struct style<std::deque<T, A>> {
static constexpr const char* open = "(Deque "; static constexpr const char* close = ")"; static constexpr const char* sep = ", ";
};
template <typename T, typename A> struct style<std::list<T, A>> {
static constexpr const char* open = "(List "; static constexpr const char* close = ")"; static constexpr const char* sep = ", ";
};
template <typename T, typename A> struct style<std::forward_list<T, A>> {
static constexpr const char* open = "(ForwardList "; static constexpr const char* close = ")"; static constexpr const char* sep = ", ";
};
// Set-style
template <typename T, typename C, typename A> struct style<std::set<T, C, A>> {
static constexpr const char* open = "[Set "; static constexpr const char* close = "]"; static constexpr const char* sep = ", ";
};
template <typename T, typename C, typename A> struct style<std::multiset<T, C, A>> {
static constexpr const char* open = "[Multiset "; static constexpr const char* close = "]"; static constexpr const char* sep = ", ";
};
template <typename T, typename H, typename E, typename A> struct style<std::unordered_set<T, H, E, A>> {
static constexpr const char* open = "[UnorderedSet "; static constexpr const char* close = "]"; static constexpr const char* sep = ", ";
};
// Map-style (key:value pairs)
template <typename K, typename V, typename C, typename A> struct style<std::map<K, V, C, A>> {
static constexpr const char* open = "[Map "; static constexpr const char* close = "]"; static constexpr const char* sep = ", ";
};
template <typename K, typename V, typename C, typename A> struct style<std::multimap<K, V, C, A>> {
static constexpr const char* open = "[Multimap "; static constexpr const char* close = "]"; static constexpr const char* sep = ", ";
};
template <typename K, typename V, typename H, typename E, typename A> struct style<std::unordered_map<K, V, H, E, A>> {
static constexpr const char* open = "[Unorderedmap "; static constexpr const char* close = "]"; static constexpr const char* sep = ", ";
};
// Concept: a printable container is any input_range we haven't already
// promised to stream as text. Excluding string-like types prevents this
// overload from stealing std::string / std::string_view output.
template <typename T>
concept printable_container =
std::ranges::input_range<T> &&
!std::convertible_to<std::remove_cvref_t<T>, std::string_view> &&
!std::is_array_v<std::remove_cvref_t<T>>;
// Map-like: an associative container with key->value mapping
// (std::map, std::multimap, std::unordered_map, std::unordered_multimap).
// Detected via the standard mapped_type/key_type member typedefs, which
// distinguishes a real map from a sequence-of-pairs like std::vector<pair>.
template <typename R>
concept map_like = requires {
typename std::remove_cvref_t<R>::key_type;
typename std::remove_cvref_t<R>::mapped_type;
};
} // namespace containerstream
// std::pair — printed as (first,second)
template <typename A, typename B>
std::ostream& operator<<(std::ostream& os, const std::pair<A, B>& p) {
return os << '(' << p.first << ',' << p.second << ')';
}
// std::tuple — printed as (T a, b, c) using std::apply + fold expression.
// Replaces the original recursive tupleElementPrint<> helper.
template <typename... Ts>
std::ostream& operator<<(std::ostream& os, const std::tuple<Ts...>& t) {
os << "(T ";
std::apply(
[&os](const auto&... xs) {
std::size_t n = 0;
(((n++ ? os << ", " : os) << xs), ...);
},
t);
return os << ")";
}
// One template for every range-shaped container.
// Replaces 8 separate operator<< overloads in the C++17 version.
template <containerstream::printable_container C>
std::ostream& operator<<(std::ostream& os, const C& c) {
using S = containerstream::style<std::remove_cvref_t<C>>;
os << S::open;
bool first = true;
for (const auto& x : c) {
if (!first) os << S::sep;
if constexpr (containerstream::map_like<C>)
os << x.first << ':' << x.second;
else
os << x;
first = false;
}
return os << S::close;
}
#endif // CONTAINEROSTREAM_H
C++23 ships default std::formatter support for ranges (P2286) and tuples/pairs (P2585), so most of the library disappears. What remains is just the custom labels ([Vector ...], (Deque ...), [Map ...]) expressed as 3-line std::formatter specializations inheriting from std::range_formatter<T>. Works with std::format, std::print, and std::println; composes with format-spec syntax; avoids global-namespace operator<< overloads on standard types.
///
// Created by AdrianFreed on 9/24/20.
// C++23 modernization: std::formatter + std::range_formatter
//
// Pedagogical notes (vs. master / cpp20 branches):
//
// 1. Most of the library is no longer needed. In C++23, <format> ships
// with default formatters for ranges (P2286) and tuples/pairs (P2585):
//
// std::vector v{1,2,3};
// std::println("{}", v); // prints: [1, 2, 3]
//
// std::map m{{'a',1},{'b',2}};
// std::println("{}", m); // prints: {'a': 1, 'b': 2}
//
// No library code is required for that.
//
// 2. The only remaining value of this library is the *custom labels*
// ("[Vector ...]", "(Deque ...)", "[Map ...]") that the original
// C++17 version was designed around.
//
// 3. The modern way to add a label is to specialize std::formatter
// and inherit from std::range_formatter<T>. Each container becomes
// a 3-line specialization that just calls set_brackets().
// Compare to the C++17 version's 4-line operator<< + helper-call
// per container, or the cpp20 version's 1-line style<C> trait
// specialization. C++23's approach also gains:
// - works with std::format / std::print / std::println
// - composes with format-spec syntax ("{:n}" for no-brackets,
// "{:e}" for escaped strings, width/fill, ...)
// - no global-namespace operator<< overloads on std types
// (which the standard technically forbids).
//
// 4. std::formatter specializations live in namespace std — the
// standard explicitly permits this for user code that wants to
// format library types in a custom way.
//
// 5. Maps don't use std::range_formatter because we want "key:value"
// (no space, matching the original output) rather than the
// "key: value" format range_format::map produces by default.
//
#ifndef CONTAINEROSTREAM_H
#define CONTAINEROSTREAM_H
#include <array>
#include <deque>
#include <forward_list>
#include <format>
#include <list>
#include <map>
#include <ranges>
#include <set>
#include <tuple>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>
// ---------- pair: print as (first,second) ----------
// Overrides the default C++23 std::pair formatter, which would yield
// "(1, 2)" with a space after the comma.
//
// Note: parse() and format() are templated on context types so they
// satisfy the std::formattable concept, which probes the formatter
// against multiple context instantiations.
template <typename A, typename B>
struct std::formatter<std::pair<A, B>> {
constexpr auto parse(auto& ctx) { return ctx.begin(); }
auto format(const std::pair<A, B>& p, auto& ctx) const {
return std::format_to(ctx.out(), "({},{})", p.first, p.second);
}
};
// ---------- tuple: print as (T a, b, c) ----------
// Overrides the default C++23 std::tuple formatter, which would yield
// "(a, b, c)" — we want the leading "T " marker the original library used.
template <typename... Ts>
struct std::formatter<std::tuple<Ts...>> {
constexpr auto parse(auto& ctx) { return ctx.begin(); }
auto format(const std::tuple<Ts...>& t, auto& ctx) const {
auto out = ctx.out();
out = std::format_to(out, "(T ");
std::apply(
[&out](const auto&... xs) {
std::size_t n = 0;
((out = std::format_to(out, "{}{}", n++ ? ", " : "", xs)), ...);
},
t);
return std::format_to(out, ")");
}
};
// ---------- sequence-style: vector, array, set, deque, list, ... ----------
// Each is a 3-line specialization that inherits from std::range_formatter
// and overrides the brackets. The standard library does all the heavy
// lifting (iteration, separator, nested-formatter dispatch).
template <typename T, typename A>
struct std::formatter<std::vector<T, A>> : std::range_formatter<T> {
constexpr formatter() { this->set_brackets("[Vector ", "]"); }
};
template <typename T, std::size_t N>
struct std::formatter<std::array<T, N>> : std::range_formatter<T> {
constexpr formatter() { this->set_brackets("[Array ", "]"); }
};
template <typename T, typename A>
struct std::formatter<std::deque<T, A>> : std::range_formatter<T> {
constexpr formatter() { this->set_brackets("(Deque ", ")"); }
};
template <typename T, typename A>
struct std::formatter<std::list<T, A>> : std::range_formatter<T> {
constexpr formatter() { this->set_brackets("(List ", ")"); }
};
template <typename T, typename A>
struct std::formatter<std::forward_list<T, A>> : std::range_formatter<T> {
constexpr formatter() { this->set_brackets("(ForwardList ", ")"); }
};
template <typename T, typename C, typename A>
struct std::formatter<std::set<T, C, A>> : std::range_formatter<T> {
constexpr formatter() { this->set_brackets("[Set ", "]"); }
};
template <typename T, typename C, typename A>
struct std::formatter<std::multiset<T, C, A>> : std::range_formatter<T> {
constexpr formatter() { this->set_brackets("[Multiset ", "]"); }
};
template <typename T, typename H, typename E, typename A>
struct std::formatter<std::unordered_set<T, H, E, A>> : std::range_formatter<T> {
constexpr formatter() { this->set_brackets("[UnorderedSet ", "]"); }
};
// ---------- maps: key:value (no space, matching original) ----------
namespace containerstream::detail {
template <typename Map, typename Ctx>
auto format_map(const Map& m, Ctx& ctx, std::string_view label) {
auto out = std::format_to(ctx.out(), "[{} ", label);
bool first = true;
for (const auto& [k, v] : m) {
out = std::format_to(out, "{}{}:{}", first ? "" : ", ", k, v);
first = false;
}
return std::format_to(out, "]");
}
} // namespace containerstream::detail
template <typename K, typename V, typename C, typename A>
struct std::formatter<std::map<K, V, C, A>> {
constexpr auto parse(auto& ctx) { return ctx.begin(); }
auto format(const std::map<K, V, C, A>& m, auto& ctx) const {
return containerstream::detail::format_map(m, ctx, "Map");
}
};
template <typename K, typename V, typename C, typename A>
struct std::formatter<std::multimap<K, V, C, A>> {
constexpr auto parse(auto& ctx) { return ctx.begin(); }
auto format(const std::multimap<K, V, C, A>& m, auto& ctx) const {
return containerstream::detail::format_map(m, ctx, "Multimap");
}
};
template <typename K, typename V, typename H, typename E, typename A>
struct std::formatter<std::unordered_map<K, V, H, E, A>> {
constexpr auto parse(auto& ctx) { return ctx.begin(); }
auto format(const std::unordered_map<K, V, H, E, A>& m, auto& ctx) const {
return containerstream::detail::format_map(m, ctx, "Unorderedmap");
}
};
#endif // CONTAINEROSTREAM_H
Comments