Reputation: 1130
Edit: see also a connected question.
I have tested the following program on godbolt:
#include <algorithm>
#include <iostream>
#include <ranges>
#include <sstream>
int main() {
std::istringstream iss{"\x{01}\x{02}\x{03}\x{04}"};
auto view = std::views::istream<char>(iss) | std::views::chunk(2) |
std::views::transform([](auto t) {
return std::ranges::fold_left(t, 0, std::plus{});
});
for (const auto& p :
std::views::cartesian_product(view, std::views::iota(0, 3))) {
const auto [s, i] = p;
std::cout << s << ", " << i << '\n';
}
return 0;
}
The result with both GCC and clang is:
3, 0
0, 1
0, 2
7, 0
0, 1
0, 2
Since the first std::views::cartesian_product
argument is supposed to be only passed through a single time, shouldn't it rather be the following?
3, 0
3, 1
3, 2
7, 0
7, 1
7, 2
I saw a relevant question in a comment that then was removed: dereferencing the iterator returned by view.begin()
three times gives 3, 0, 0, which should be the root cause.
Upvotes: 3
Views: 338
Reputation: 3402
This is not related to std::views::cartesian_product
, which works correctly. This is related to your std::istringstream
iss
or (as stated in comments) to its usage in the std::views::istream<char>(iss)
.
Replace it with vector and everything would work as expected:
#include <algorithm>
#include <iostream>
#include <ranges>
#include <sstream>
#include <vector>
int main()
{
using namespace std::string_literals;
std::vector<int> iss{ 1, 2, 3, 4};
auto view = iss | std::views::chunk(2) |
std::views::transform([](auto t) {
return std::ranges::fold_left(t, 0, std::plus{});
});
for (const auto& p :
std::views::cartesian_product(view, std::views::iota(0, 3))) {
const auto [s, i] = p;
std::cout << s << ", " << i << '\n';
}
return 0;
}
with the results:
3, 0
3, 1
3, 2
7, 0
7, 1
7, 2
Demo.
So, this answers to your question on how std::views::cartesian_product
is supposed to work and narrows your way to look for issue with std::istringstream
and, possibly, its view.
Solution and conclusions
While everything is covered in detail in the answer above from Barry, I’d like to provide my takeaways here, since the problem is funny and worth lessons learned. I wouldn’t repeat what is said in the Barry’s answer, but rather would provide my view for the problem (although these are just different faces of the same thing). Just is case it would help someone in similar research cases and as a keepsake knot for me.
When I work with views, I assume that they should provide some “static” representation of underlying data and this assumption is correct. With the same input data, the view should return the same result.
The difference comes when we consider the “staticness”. When we wrap up the data into a stream, data still being static is now accessed dynamically though the stream view which is not static from the reading perspective and fold_left
’s usage effect highlights this fact. Now this is not a view actually, but a flow and now we must be very careful with views’ use cases, since for most of them we assume and anticipate “staticness” from all perspectives. We expect to have views, not flows.
With all the mentioned above, I would personally think twice before using streams with views, since although this is still valid and helpful, this is very risky for the common views’ mindset.
Lessons learned
I was a step away from the solution, found by Barry, but I suspected the std::views::istream
and focus on it allowed me to stuck on the "The iterator type of basic_istream_view is move-only: it does not meet the LegacyIterator requirements" and decide that this explanation covers the issue.
I saw with simple debugging code that the view
is consumed, but I stopped with my hypothesis. "Divide et impera”. I had to divide the view to parts and study each of them to finally pinpoint the root case. This wasn’t done in this case because of “good enough” result balance which failed me this time.
Upvotes: 2
Reputation: 303997
The problem here is the transform
actually.
There are two facts that we're running afoul of:
transform(f)
must be equality-preserving. That is, the function, f
, that you are providing must return the same answer given the same input.When you're doing fold_left
in the transform, the argument there (t
) is an input range (the originating istream<char>
is an input range, and then chunk
-ing it gives you an input range of input ranges). fold_left
has to traverse that range, which consumes it. But you can only do that one single time. The second time you do it, there's nothing left to traverse, so you have en empty range.
Put differently, you're violating the expectation of iterators, which is:
auto it = /* some iterator */;
auto v1 = *it;
auto v2 = *it;
assert(v1 == v2);
By providing a callable which consumes its input, it's preventing this iterator dereference from producing the same value every time. And that's the behavior you see, you get the expected answer the first time (3
and then 7
) but then garbage every other time (0
because that's what your fold
gives on an empty range).
There are two ways to fix this.
One is to make the range non-input, for instance by first consuming the whole views::istream<char>
into a vector<char>
and then doing the chunk | transform(fold)
on the resulting vector
. Now our chunks aren't input-only, so the fold
can give the same answer every time.
The other is to "fix" the result of the transform
in the literal sense by avoiding recalculating it. range-v3 has an adaptor for this (cache1
) and there is one proposed for standardization called cache_last
P3138:
auto view = views::istream<char>(iss)
| views::chunk(2)
| views::transform([](auto t) {
return ranges::fold_left(t, 0, plus{});
})
| views::cache_last // <== add me
;
cache_last
simply caches the last value, ensuring that *it
repeatedly always produces the same thing since we never go back to the underlying iterator. Since we're only calling fold
exactly one time, we don't run into the input issue.
I'm not sure if there's really a good way of diagnosing such mis-uses.
Upvotes: 10