Why does ostream_iterator need to explicitly declare the type of objects to output?
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.