How to write a streaming 'operator<<' that can take arbitary containers (of type 'X')?

吃可爱长大的小学妹 提交于 2019-12-04 10:49:02

问题


I have a C++ class "X" which would have special meaning if a container of them were to be sent to a std::ostream.

I originally implemented it specifically for std::vector<X>:

std::ostream& operator << ( std::ostream &os, const std::vector<X> &c )
{
   // The specialized logic here expects c to be a "container" in simple
   // terms - only that c.begin() and c.end() return input iterators to X
}

If I wanted to support std::ostream << std::deque<X> or std::ostream << std::set<X> or any similar container type, the only solution I know of is to copy-paste the entire function and change only the function signature!

Is there a way to generically code operator << ( std::ostream &, const Container & )?

("Container" here would be any type that satisfies the commented description above.)


回答1:


If you have read this answer before, you might want to scroll down to the ADL version below. It is much improved.

First, a short and sweet version that pretty much works:

#include <iostream>
#include <type_traits>
template<typename T, typename Iterator, typename=void>
struct is_iterator_of_type: std::false_type {};

template<typename T, typename Iterator>
struct is_iterator_of_type<
  T,
  Iterator,
  typename std::enable_if<
    std::is_same<
      T,
      typename std::iterator_traits< Iterator >::value_type
    >::value
  >::type
>: std::true_type {};

template<typename Container>
auto operator<<( std::ostream& stream, Container const& c ) ->
  typename std::enable_if< is_iterator_of_type<int, typename Container::iterator>::value, std::ostream& >::type
{
  return stream << "int container\n";
}
template<typename Container>
auto operator<<( std::ostream& stream, Container const& c ) ->
  typename std::enable_if< is_iterator_of_type<double, typename Container::iterator>::value, std::ostream& >::type
{
  return stream << "double container\n";
}

which merely detects things that look sort of like int and double containers with distinct overloads. I would advise changing the implementation of operator<<. ;)

A more proper route (thanks @Xeo) would be this adl-hack. We create an auxiliary namespace where we import begin and end from std, then some template functions that do argument dependent lookup on begin and end (seeing the std version if we don't have a more tightly bound one), and then use these aux::adl_begin functions to determine if what we are passed in can be treated as a container over X:

#include <iostream>
#include <vector>
#include <type_traits>
#include <iterator>
#include <set>

template<typename T, typename Iterator, typename=void>
struct is_iterator_of_type: std::false_type {};

template<typename T, typename Iterator>
struct is_iterator_of_type<
  T,
  Iterator,
  typename std::enable_if<
    std::is_same<
      T,
      typename std::iterator_traits< Iterator >::value_type
    >::value
  >::type
>: std::true_type {};

namespace aux {
  using std::begin;
  using std::end;
  template<class T>
  auto adl_begin(T&& v) -> decltype(begin(std::forward<T>(v))); // no implementation
  template<class T>
  auto adl_end(T&& v) -> decltype(end(std::forward<T>(v))); // no implementation
}

template<typename T, typename Container, typename=void>
struct is_container_of_type: std::false_type {};

template<typename T, typename Container>
struct is_container_of_type<
  T,
  Container,
  typename std::enable_if<
    // we only want this to be used if we iterable over doubles:
    is_iterator_of_type<
      T,
      decltype(void(aux::adl_begin(*(Container*)nullptr)), aux::adl_end(*(Container*)nullptr)) // ensure being and end work as bonus
    >::value
  >::type
>: std::true_type
{};

template<class Ch, class Tr, class Container>
auto operator<<( std::basic_ostream<Ch,Tr>& stream, Container const& c ) ->
  typename std::enable_if<
    is_container_of_type<double, Container>::value,
    decltype(stream)
  >::type
{
  stream << "'double' container: [ ";
  for(auto&& e:c)
    stream << e << " ";
  return stream << "]";
}

int main() {
  std::cout << std::vector<double>{1,2,3} << "\n";
  std::cout << std::set<double>{3.14,2.7,-10} << "\n";
  double array[] = {2.5, 3.14, 5.0};
  std::cout << array << "\n";
}

With this, not only do arrays of doubles count as containers over double, so does anything where in its namespace you define a begin and end function that returns iterators over double that takes the container as an argument also works. This matches how for(auto&& i:container) lookup works (perfectly? reasonably well?), so is a good working definition of "container".

Note, however, that as we add more of these embellishments, fewer current compilers have all of the C++11 features we are using. The above compiles in gcc 4.6 I believe, but not gcc 4.5.*.

...

And here is the original short code with some testing framework around it: (useful if your compiler throws it up, you can see where it goes wrong below)

#include <iostream>
#include <type_traits>
#include <vector>
#include <iostream>
#include <set>

template<typename T, typename Iterator, typename=void>
struct is_iterator_of_type: std::false_type {};

template<typename T, typename Iterator>
struct is_iterator_of_type<
  T,
  Iterator,
  typename std::enable_if<
    std::is_same<
      T,
      typename std::iterator_traits< Iterator >::value_type
    >::value
  >::type
>: std::true_type {};

void test1() {
  std::cout << is_iterator_of_type<int, std::vector<int>::iterator>::value << "\n";
}
template<typename T, typename Container>
auto foo(Container const&) -> typename std::enable_if< is_iterator_of_type<T, typename Container::iterator>::value >::type
{
  std::cout << "Container of int\n";
}
template<typename T>
void foo(...)
{
  std::cout << "No match\n";
}
void test2() {
  std::vector<int> test;
  foo<int>(test);
  foo<int>(test.begin());
  foo<int>(std::set<int>());
}
template<typename Container>
auto operator<<( std::ostream& stream, Container const& c ) ->
  typename std::enable_if< is_iterator_of_type<int, typename Container::iterator>::value, std::ostream& >::type
{
  return stream << "int container\n";
}
void test3() {
  std::vector<int> test;
  std::cout << test;
  std::set<int> bar;
  std::cout << bar;
}
template<typename Container>
auto operator<<( std::ostream& stream, Container const& c ) ->
  typename std::enable_if< is_iterator_of_type<double, typename Container::iterator>::value, std::ostream& >::type
{
  return stream << "double container\n";
}
void test4() {
  std::vector<int> test;
  std::cout << test;
  std::set<int> bar;
  std::cout << bar;
  std::vector<double> dtest;
  std::cout << dtest;
}
void test5() {
  std::vector<bool> test;
  // does not compile (naturally):
  // std::cout << test;
}
template<typename Container>
auto operator<<( std::ostream& stream, Container const& c ) ->
  typename std::enable_if< is_iterator_of_type<bool, typename Container::iterator>::value, std::ostream& >::type
{
  return stream << "bool container\n";
}
void test6() {
  std::vector<bool> test;
  // now compiles:
  std::cout << test;
}
int main() {
  test1();
  test2();
  test3();
  test4();
  test5();
  test6();
}

about half of the above is testing boilerplate. The is_iterator_of_type template, and the operator<< overloads are what you want.

I am presuming that a container of type T is any class with a typedef iterator which whose value_type is a T. This will cover every std container, and most custom ones.

Link to execution run: http://ideone.com/lMUF4i -- note that some compilers don't support full C++11 SFINAE, and may require tomfoolery to get it to work.

Test cases left in to help someone check what level of support their compiler has for these techniques.




回答2:


template<template<class T, class A> class container>
std::ostream& opertaor << ( std::ostream&, const container<X, std::allocator<X> > &)
{
}

This won't work if on your implementation vector, list, etc. have more than 2 template parameters.




回答3:


Simple if not elegant - and the next person to maintain your code might appreciate a lack of fancy templates! In practice I would hide the 'Print' method in a cpp, or at least a Detail namespace.

#include <iostream>
#include <vector>
#include <deque>
#include <list>
#include <set>
#include <multiset>

class X {};

template <typename T>
std::ostream& Print(std::ostream& os, const T& container)
{
    for(auto ii = container.cbegin(); ii != container.cend(); ++ii);
        //etc
        //
    return os;
}

std::ostream& operator<<(std::ostream& os, const std::vector<X>& v) { return Print(os, v); }
std::ostream& operator<<(std::ostream& os, const std::deque<X>& v) { return Print(os, v); }
std::ostream& operator<<(std::ostream& os, const std::list<X>& v) { return Print(os, v); }
std::ostream& operator<<(std::ostream& os, const std::set<X>& v) { return Print(os, v); }
std::ostream& operator<<(std::ostream& os, const std::multiset<X>& v) { return Print(os, v); }

int main()
{
            // Example
    std::vector<X> v;
    std::cout << v;
}



回答4:


If you slightly redefine the question to providing special streaming behavior for any class that provides range based access to Widget, instead of special behavior for all Widget containers, one solution is:

  template <class Container>
  std::ostream& operator << (std::ostream &out, const Container &container) 
  {
    for(const Widget& c : container) {
      out << c;
      out.put(' ');
    }
    return out;
  }

This works for std::vector, std::list, std::deque, and std::set. If you attempt to stream something that does not provide range access to Widget, say std::list<int>, you'll get a compilation error, because the const Widget reference cannot bind to the ints in std::list<int>. If you provide an overload for operator << for std::list<int> the code will compile.




回答5:


While @razeh has a nice solution, if you need to get fancy and have specialized printing for a container of X versus a container of Y, you can do the following:

    // Types for which you want specialized streaming of containers
    // We need some identifiable typedef in these types
    struct  X { typedef void X_type; };
    struct  Y { typedef void Y_type; };


    // Wrappers for implementing streaming logic for each type        

template <typename C>
struct WrapX
{
    WrapX(const C& c) : c(c) { }
    const C& c;

    std::ostream& stream(std::ostream& os)
    {
         // Special container of X printing
         return os;
    }
};

template <typename C>
struct WrapY
{
    WrapY(const C& c) : c(c) { }
    const C& c;

    std::ostream& stream(std::ostream& os)
    {
        // Special container of Y printing
        return os;
    }
};

    // Wrap functions, by using a 'dummy' parameter
    // we can get the compiler to select the function based
    // on the incoming type

template <typename C >
WrapX<C> Wrap(const C& c,  typename C::value_type::X_type* = 0) { return WrapX<C>(c); }

template <typename C>
WrapY<C> Wrap(const C& c, typename C::value_type::Y_type* = 0) { return WrapY<C>(c); }



    // Overload - same problem as @razeh solution, this is a VERY generic
    // function and may clash with other declarations. Keep it closely confined to
    // where you need it.
template <typename C>
std::ostream& operator<<(std::ostream& os, const C& c) { return Wrap(c).stream(os);  }




int main()
{
    std::vector<X> vx;
    std::cout << vx;

        std::vector<Y> vy;
        std::cout << vy;
}


来源:https://stackoverflow.com/questions/13724766/how-to-write-a-streaming-operator-that-can-take-arbitary-containers-of-type

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!