I found that c++20 ranges::basic_istream_view
is slightly different from the range-v3 version.
The most important difference is that the std::ranges::basic_istream_view
does not cache its begin()
, so that each begin()
s will return the next iterator with the value that has been read (godbolt):
auto words = std::istringstream{"today is yesterday's tomorrow"};
auto view = std::ranges::istream_view<std::string>(words);
std::cout << *view.begin() << "\n"; // today
std::cout << *view.begin() << "\n"; // is
std::cout << *view.begin() << "\n"; // yesterday's
std::cout << *view.begin() << "\n"; // tomorrow
Consider the following (godbolt), If I use the range-v3 version, all the three std::ranges::find()
s will found "is"
, but if I use the std version, "is"
will be found only in the first call.
auto words = std::istringstream{"today is yesterday's tomorrow"};
auto view = std::ranges::istream_view<std::string>(words);
std::cout << *std::ranges::find(view, "is") << "\n"; // is
std::cout << *std::ranges::find(view, "is") << "\n"; // tomorrow
std::cout << *std::ranges::find(view, "is") << "\n"; // tomorrow
Why did the standard choose a different design from range-v3? Is there any potential defect if begin()
is cached?
In the definition of the range
concept, in [range.range], we have:
template<class T>
concept range =
requires(T& t) {
ranges::begin(t); // sometimes equality-preserving (see below)
ranges::end(t);
};
where the "see below" parts are (emphasis mine):
Given an expression
t
such thatdecltype((t))
isT&
,T
models range only if
- ...
- if the type of
ranges::begin(t)
modelsforward_iterator
,ranges::begin(t)
is equality-preserving.[Note 1: Equality preservation of both
ranges::begin
andranges::end
enables passing a range whose iterator type modelsforward_iterator
to multiple algorithms and making multiple passes over the range by repeated calls toranges::begin
andranges::end
. Sinceranges::begin
is not required to be equality-preserving when the return type does not modelforward_iterator
, it is possible for repeated calls to not return equal values or to not be well-defined. — end note]
For forward ranges, you can call ranges::begin(r)
repeatedly and expect the same answer (it is equality-preserving). For some ranges, that requires caching.
But for input-only ranges (like istream_view
), you can call ranges::begin(r)
exactly one time, and there are no guarantees for what happens on the second invocation.
So the difference between the two implementations isn't observable by a valid program, since by calling begin
multiple times you're already violating the preconditions here.
For a more specific answer as to istream_view
, range-v3's implementation does cache not begin()
either. That's not the difference that's happening. And indeed, you cannot cache input iterators anyway, since they would immediately be invalidated.
The difference is rather when we read the first value from the stream:
istream_view
.begin()
.The latter is more consistent with the general Ranges model where constructing range adaptors does not actually do any work.
input iterators are such that as soon as you have dereferenced one, you need to instantly increment it. The same is with output iterators, such as back_insert_iterator. This is something that you are just not supposed to be doing. If you need the first value cached, cache it yourself.
The reason input and output iterators need to be incremented after dereferencing is that they are by design single pass.If you have read something from a stream, you cannot read it again. Operator * actually reads from the stream. What does ++ do? Nothing! Same goes for back_insert_iterator
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With