Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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.)

like image 795
Drew Dormann Avatar asked Dec 05 '12 13:12

Drew Dormann


People also ask

What is the use of streaming operator?

For extra flexibility, streaming operators can be used in the cases where the bit ordering is important or a simple bit-stream cast is not sufficient.. There are two streaming operators, {>> {}} and {<< {}}, which operate on data blocks (or slices).

How to pack data using the SystemVerilog streaming operators?

How to Pack Data Using the SystemVerilog Streaming Operators (>>, <<) 1 1. Pack bytes into an int. 2 2. Reverse the elements of a byte array and pack them into an int. 3 3. Reverse the bits in a byte. 4 4. Reverse the nibbles in a byte. 5 5. Reverse the bits of an array and pack them into a shortint. More items

Does it matter which side the streaming operator appears on?

Back to your first question, in your case it does not matter which side the streaming operator appears because both sides are a single variables of the same size. There is one slight difference when the variables are not the same size as mentioned in section 11.4.14.3.

How to pack an array of bytes into a single variable?

Packing an array of bytes into a single variable is just as easy: 2. Reverse the elements of a byte array and pack them into an int We can reverse the order of an array’s elements and then pack them into a single value in the following way:


Video Answer


3 Answers

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.

like image 75
Yakk - Adam Nevraumont Avatar answered Oct 12 '22 02:10

Yakk - Adam Nevraumont


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.

like image 30
Armen Tsirunyan Avatar answered Oct 12 '22 03:10

Armen Tsirunyan


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;
}
like image 30
Zero Avatar answered Oct 12 '22 01:10

Zero