Printing a std::map using a std::range::copy and an adaptor

Viewed 156

Still trying to learn how to use templates, concepts, constraints. I wanted to use std::ranges::copy() to print out the contents of a std::map and I saw this answer. Impressed, I wonder if I could constrain the pair_adaptor to only work on std::pairs where the elements were printable.

So, I wrote this:

template <class T>
concept PrintablePair = requires(std::ostream & os, T a)
{
    os << a.first;
    os << a.second;
};

template <PrintablePair pair_type>
class pair_adaptor
{
public:
    const pair_type& m;
    pair_adaptor(const pair_type& a) : m(a) {}

    friend std::ostream& operator << (std::ostream& out,
        const pair_adaptor <pair_type>& d)
    {
        const pair_type& m = d.m;
        return out << m.first << " => " << m.second;
    }
};

This works well for maps that look like this:

std::map<int,int> m1;
std::ranges::copy(m1, 
    std::ostream_iterator<
    pair_adaptor<decltype(*m1.begin())> >(std::cout, "\n"));

Now, I would like to expand this further so that I can print maps that look like this:

std::map<int, std::pair<int, int>>

So, I think that I need to be able to recursively print pairs, but I am lost.

3 Answers

As far C++ concepts cannot be recursive you have to define upper level concept NestedPrintablePairand operator<< for it.

See also live at https://wandbox.org/permlink/pe7GzkFrEvxXoKmu:

#include <algorithm>
#include <iostream>
#include <map>
#include <ranges>

template <class T>
concept Printable = requires(std::ostream & os, T a)
{
    os << a;
};

template <class T>
concept PrintablePair = Printable<typename T::first_type> &&
    Printable<typename T::second_type>;
   
template <typename T, typename X1 = T::first_type, typename X2 = T::second_type> 
concept NestedPrintablePair = PrintablePair<T> ||
    ((Printable<X1> || PrintablePair<X1>) && (Printable<X2> || PrintablePair<X2>));

template <PrintablePair T>
std::ostream& operator << (std::ostream& out, const T& p)
{
    return out << p.first << ":" << p.second;
}

template <NestedPrintablePair pair_type>
class pair_adaptor
{
public:
    const pair_type& m;
    pair_adaptor(const pair_type& a) : m(a) {}      
   
    friend std::ostream& operator << (std::ostream& out, 
        const pair_adaptor<pair_type>& d)
    {
        const pair_type& m = d.m;
        return out << m.first << " => " << m.second;
    }
};

int main()
{
    std::map<int,int> m1 { {1,2}, {3,4} };
    
    std::ranges::copy(m1, 
        std::ostream_iterator<
        pair_adaptor<std::decay<decltype(*m1.begin())>::type > >(std::cout, "\n"));

    std::map<int, std::pair<int, int>> m2 { {1, {2, 3} }, {3, {4, 5} } };
    
    std::ranges::copy(m2, 
        std::ostream_iterator<
        pair_adaptor<std::decay<decltype(*m2.begin())>::type > >(std::cout, "\n"));

    std::map<std::pair<int, int>, int> m3 { { {2, 3}, 1}, { {4, 5}, 6} };
    
    std::ranges::copy(m3, 
        std::ostream_iterator<
        pair_adaptor<std::decay<decltype(*m3.begin())>::type > >(std::cout, "\n"));
    
    std::map<std::pair<int, int>, std::pair<int, int>> m4 
        { { {1, 2}, {3, 4}}, { {4, 5}, {6, 7} } };
    
    std::ranges::copy(m4, 
        std::ostream_iterator<
        pair_adaptor<std::decay<decltype(*m4.begin())>::type > >(std::cout, "\n"));
    
    return 0;
}

The problem is that your PrintablePair concept requires that type T's first_type and second_type should be printable and your pair_adaptor template parameter must satisfy the PrintablePair concept.

But when you take std::pair<int, std::pair<int, int>> as pair_adaptor's argument, the second_type which is std::pair<int, int> is not printable unless we transform it into pair_adaptor<std::pair<int, int>>.

The alternative solution is that we can define a std::pair concept first:

template <class P>
concept Pair = std::same_as<std::pair<
  typename P::first_type, 
  typename P::second_type>, 
P>;

Then when we encounter a std::pair, we just change it into pair_adaptor:

template <class T>
concept Printable = requires(std::ostream& os, T a) { os << a; };

template <Pair pair_type>
class pair_adaptor {
public:
  const pair_type& m;
  pair_adaptor(const pair_type& a) : m(a) {}
  friend std::ostream& operator<<(std::ostream& out, const pair_adaptor& d) {
    auto print = [&out]<typename T>(const T& x) {
      if constexpr (Pair<T>) out << pair_adaptor<T>{x};
      else {
        static_assert(Printable<T>);
        out << x;
      }
    };

    const pair_type& m = d.m;
    out << "(";
    print(m.first);
    out << " => ";
    print(m.second);
    out << ")";
    return out;
  }
};

Live demo (Examples steal from @Rost).

So, I think that I need to be able to recursively print pairs, but I am lost.

The problem is the concepts can't be recursive.

But you can construct a concept based on something that can be recursive, for example: template variables.

You can define a template variable that say if a type is printable

constexpr std::false_type print_test (...);

template <typename T>
constexpr auto print_test (T t) 
   -> decltype( std::declval<std::ostream>() << t, std::true_type{});

template <typename T>
constexpr bool isPrint = decltype(print_test(std::declval<T>()))::value;

then a recursive template variable that is true when the type is a std::pair<T1, T2> and both T1 and T2 are printable or a printable pair

template <typename>
constexpr bool isPrintPair { false };

template <typename T1, typename T2>
constexpr bool isPrintPair<std::pair<T1, T2>>
 { (isPrint<T1> || isPrintPair<T1>) && (isPrint<T2> || isPrintPair<T2>) }; 

Now your concept simply become

template <typename T>
concept PrintablePair = isPrintPair<std::decay_t<T>>;
Related