C++ container output stream header file

This header file provides extensions to the iostream << operator so you can pretty print C++ containers. It has a few interesting examples of template metaprogramming and uses some of the more modern C++ (2017) features. The repo code is evolving: https://github.com/adrianfreed/containerostream
//
// 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.

C++20 version (cpp20 branch)

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 version (cpp23 branch)

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

Here is a simple test by AdrianFreed