xmllmx
xmllmx

Reputation: 42379

Why does ostream_iterator need to explicitly declare the type of objects to output?

In current C++, the class ostream_iterator was designed like the following:

// excerpted from the standard C++

template<class T, ...>
class ostream_iterator
{
public:
    ostream_iterator(ostream_type&);
    ...

    ostream_iterator<T,...>& operator =(const T&);
    ...
};

To me, this design is suboptimal. Because the user must specify the type T when declaring an ostream_iterator like this: ostream_iterator<int> oi(cout); In fact, cout can take any type of object as its argument, rather than only one type. This is an obvious restriction.

// Below is my own version

// doesn't need any template parameter here
class ostream_iterator
{
public:
    ostream_iterator(ostream_type&);
    ...

    // define a template member function which can take any type of argument and output it
    template<class T> 
    ostream_iterator<T,...>& operator =(const T&);
    ...
};

Now, we can use it as follows:

ostream_iterator oi(cout);

I think it is more generic and more elegant than

ostream_iterator<int> oi(cout);

Am I right?

Upvotes: 12

Views: 2090

Answers (4)

CashCow
CashCow

Reputation: 31445

It appears you could be right.

Let's see if we can construct an ostream_iterator that does not need a template argument.

The iterator works by copying values into it, so *iter = x; ++iter; The iterator cheats by making operator* return itself and ++iter also returning itself without changing any state. The "magic" is in the operator= which performs the output.

The "cout" must be a class member of type ostream*. It needs to be a pointer as iterators must be assignable, thus we assign the member (call it os) to the address of the stream passed in.

So we would overload operator= this way:

template< typename T >
our_ostream_iterator& operator=( const T& t )
{
   (*os) << t;
   if( delim )
      (*os) << delim;
   return *this;
}

Note that the templatised operator= should not oveload operator=(our_ostream_iterator const&) which is more specialised than the template.

You would still want a template on the element type so we will call that our_basic_ostream_iterator

ostream_iterator would still remain a template class on its element type. Thus:

template< typename E, typename TR=char_traits<E> >
class our_basic_ostream_iterator : public std::iterator< /*traits here*/ >
{
public:
   typedef E element_type;
   typedef TR traits_type;
   typedef basic_ostream< E, TR > stream_type;
private:
   stream_type * os;
   const E* delim;
public:
   our_basic_ostream_iterator( stream_type s, const E* d = nullptr ) :
      os( &s ), delim( d )
   {
   }

   our_basic_ostream_iterator& operator++() { return *this; }
   our_basic_ostream_iterator operator++(int) { return *this; }
   our_basic_ostream_iterator& operator*() { return *this; }

   template< typename T >
   our_basic_ostream_iterator& operator=( const T& t ); // as above
};

and then of course

typedef our_basic_ostream_iterator<char> our_ostream_iterator;
typedef our_basic_ostream_iterator<wchar_t> our_wostream_iterator;

The drawback of all of this though is that the above does not conform to all the properties of iterators such that it could be passed to any algorithm / class that requires a forward iterator. Why? Because such an algorithm should be able to invoke iterator_traits to extract the element type and the class above does not contain an element type.

It would lead to compile-time errors in the algorithm that is using your iterator and would potentially be hard to track down the reason why.

Upvotes: 1

alfC
alfC

Reputation: 16270

The simple answer is that iterator have associated types and ostream_iterator conceptually violates the concept of an iterator by requiring a value_type even when it is not necessary. (This is basically @pts's answer)

What you are proposing is related to the idea behind the new "transparent operators", such as the new std::plus<void>. Which consist in having a special instantiation whose member function has a delayed type deduction.

It is also backward compatible because void is not a useful instantiation to begin with. Moreover the void parameter is also the default. For example template<T = void> struct std::plus{...} is the new declaration.


A possible implementation of a transparent ostream_iterator

Going back of std::ostream_iterator, an important test is whether we want to make it work with std::copy as std::ostream_iterator is usually used:

std::vector<int> v = {...};
std::copy(v.begin(), v.end(), std::ostream_iterator<int>(std::cout, " "));

The technology for a transparent std::ostream_iterator is not there yet, because this fails:

std::copy(v.begin(), v.end(), std::ostream_iterator<void>(std::cout, " "));

To make this work, one can explicitly define the void instance. (This completes @CashCow 's answer)

#include<iterator>
namespace std{
    template<>
    struct ostream_iterator<void> : 
        std::iterator<std::output_iterator_tag, void, void, void, void>
    {
        ostream_iterator(std::ostream& os, std::string delim) : 
            os_(os), delim_(delim)
        {}
        std::ostream& os_;
        std::string delim_;
        template<class T> ostream_iterator& operator=(T const& t){
            os_ << t << delim_;
            return *this;
        }
        ostream_iterator& operator*(){return *this;}
        ostream_iterator& operator++(){return *this;}
        ostream_iterator& operator++(int){return *this;}
    };

}

Now this works:

std::copy(v.begin(), v.end(), std::ostream_iterator<void>(std::cout, " "));

Moreover, if we convince the standard committee to have a default void parameter (as they did with with std::plus): template<class T = void, ...> struct ostream_iterator{...}, we could go a step further and omit the parameter altogether:

std::copy(v.begin(), v.end(), std::ostream_iterator<>(std::cout, " "));

The root of the problem and a possible way out

Finally, in my opinion the problem might also be conceptual, in STL one expects an iterator to have a definite value_type associated even if it is not necessary like here. In some sense ostream_iterator violates some concepts of what is an iterator.

So there are two things that are conceptually wrong in this usage: 1) when one copies one expects to know the type of the source (container value_type) and target types 2) one is not copying anything in the first place!. In my opinion there is a double design mistake in this typical usage. There should be a std::send that works with a template shift << operators directly, instead of making = redirect to << as ostream_iterator does.

std::send(v.begin(), v.end(), std::cout); // hypothetical syntax
std::send(v.begin(), v.end(), std::ostream_receiver(std::cout, " ")); // hypothetical syntax
std::send(v.begin(), v.end(), 'some ostream_filter'); // hypothetical syntax

(The last argument should fulfill some kind of Sink concept).


** Using std::accumulate instead and a possible implementation of std::send **

From a conceptual point of view, sending objects to a stream is more of an "accumulate" operation than a copy operator, so in principle std::accumulate should be a more suitable candidate, besides we don't need "target" iterators for it. The problem is that std::accumulate wants to make copies of every object that is being accumulated, so this doesn't work:

    std::accumulate(e.begin(), e.end(), std::cout, 
        [](auto& sink, auto const& e){return sink << e;}
    ); // error std::cout is not copiable

To make it work we need to do some reference_wrapper magic:

    std::accumulate(e.begin(), e.end(), std::ref(std::cout), 
        [](auto& sink, auto const& e){return std::ref(sink.get() << e);}
    );

Finally, the code can be simplified by having the equivalent of std::plus for the shift operator, in modern C++ this should look like this IM:

namespace std{

    template<class Sink = void, class T = void>
    struct put_to{
        std::string delim_;
        using sink_type = Sink;
        using input_type = T;
        Sink& operator()(Sink& s, T const& t) const{
            return s << t << delim_;
        }
    };

    template<>
    struct put_to<void, void>{
        std::string delim_;
        template<class Sink, class T>
        Sink& operator()(Sink& s, T const& t){
            return s << t;
        }
        template<class Sink, class T>
        std::reference_wrapper<Sink> operator()(std::reference_wrapper<Sink> s, T const& t){
            return s.get() << t << delim_;
        }
    };

}

Which can be used as:

std::accumulate(e.begin(), e.end(), std::ref(std::cout), std::put_to<>{", "});

Finally we can define:

namespace std{
    template<class InputIterator, class Sink>
    Sink& send(InputIterator it1, InputIterator it2, Sink& s, std::string delim = ""){
        return std::accumulate(it1, it2, std::ref(s), std::put_to<>{delim});
    }
}

Which can be used as

std::send(e.begin(), e.end(), std::cout, ", ");

Finally, there is no dilemma about the type of any output_iterator here.

Upvotes: 3

Chubsdad
Chubsdad

Reputation: 25537

I think the reason is that it has other members also. Obviously the entire set of member functions need to be consistent in their behavior for a given set of T and other template arguments.

There's danger in operator < being instantiated for a set of template arguments which is different from what is used to instantiate operator * or operator++

Hence, the individual methods are not template themselves and rather the entire class is a template so ensure uniform T and other template arguments.

Upvotes: 1

pts
pts

Reputation: 87361

Yes, you are right. It would be more flexible as you suggest. However, the way it's designed fits more closely to how STL uses iterators: one iterator type for data type (T).

Upvotes: 0

Related Questions