Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

Tags:

c++

stream

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?

like image 324
xmllmx Avatar asked Dec 02 '10 08:12

xmllmx


1 Answers

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.

like image 108
alfC Avatar answered Oct 02 '22 14:10

alfC