tl;dr
It's not clear to me how ranges::any_view<T, C> works, especially in relation to what T is, i.e. a &, a const&, a value type, or what else.¹
Research
There isn't really much about it here, yet, nor I have found much around, beside this very useful explanation of why ranges::begin can't be called on const ranges::any_view.
A tricky (?) case
Here's one such example which puzzles me a lot, and below are the relavants bit of it. I think that thoroughly understanding why the examples linked below work (or don't) the way they do, should help me understand what any_view actually is.
This function accepts any range of std::strings and prints each of them (the filter(always(true)) is letting everything pass):
void test(any_view<std::string> parts) {
for_each(parts | filter(always(true)), print);
};
Now, if I pass an lvalue of type any_view<std::string const&>
any_view<std::string const&> view{foos};
into it,
test(view);
all is fine, but if I first pipe that into cache1²,
test(view | cache1);
then the all the std::strings result empty.
By trial and error I realized that changing
test(any_view<std::string> parts) {
to
test(any_view<std::string const&> parts) {
solves the problem, but I don't really understand why.
But if I then was to also change
any_view<std::string const&> view{foos};
to
any_view<std::string> view{foos};
Furthermore, manually inlining the body of test at the call site, also seems to solve the issue.
And to make the thing trickier, when I try to change std::string for a Foo that holds a std::string (and sets it to "" in the destructor, to hopefully make possible UB a bit more "observable"), the issue seems to go away.
Side question
I know there's no any_view in c++20, and I guess there won't be one in c++23 either, but is there a reason (beside performance) why it's not been included yet or won't be included in the foreseeable future? I mean, to me it looks like it does serve a ligitimate purpose, so what else in the standard library is or will fill that hole?
(¹) It's a bit clearer what the role of C is, especially because I had an experience of downgrading an any_view<SomeType, sized> to any_view<SomeType, forward> for the purpose of allowing storing a filter_view, which is not sized, into it.
(²) Yes, the cache1 is useless here, but in my real usecase I had view | transform(f) | filter(g), so I inserted cache1 in between to prevent f from being called twice for each entry x in view which passes the test g(f(x)).
This is a combination of a few things:
any_view<T>, thereferencetype of the range isT.any_view<string>is a range of prvaluestring, whileany_view<string const&>is a range of lvalues ofstring const.any_view<T>accepts any range who reference is convertible toT, not same asT. So you can construct anany_view<string>from a range whose reference type isn't juststringor evenchar const*but also, relevantly here,string&&.cache1is implemented in range-v3 is probably not the best way to do this, and looks like:filterrequires dereferencing iterators more than once (once to see if it passes the filter, and then the second time for when you actually use the value).So when you do this:
foos | cache1is a range ofstring&&(which is an xvalue intocache1's internal cache,foosis unaffected - it could've beenconsttoo). We're converting that toany_view<string>, which is now a range ofstring, which means that somewhere in all these types we have an iterator whoseoperator*()returns astringfrom the result of dereferencing(foos | cache1)'s iterator. That will move fromcache1's cache. So if you dereference twice, you will end up with an empty cache.Then when you do
parts | views::filter(pred), because you dereference every iterator twice (once to check if it satisfies the predicate, the second to print it), you will end up moving from the cachedstringright before you print it, so you only get empty strings.The problem here is probably that
cache1is a range ofrange_value_t<V>&&, which can lend itself to this double-dereference issue. If it were a range ofrange_value_t<V>&instead, then this would just work (at the cost of every dereference of yourany_view<string>leading to astringcopy). On the other hand, if you won't dereference the iterator more than once, you do want to just move, since if you were doing something like:You don't want to have to remember to write:
Note that a similar problem exists for
views::concat(r1, r2)ifr1is a range ofstringandr2is a range ofstring&&, you will end up with a range whose reference type isstring, such that dereferencing that iterator will end up moving from the elements ofr2. Which is what you want to happen if you're doingviews::concat(r1, r2) | to<vector>(), but is definitely want you don't want to happen if you're doingviews::concat(r1, r2) | filter(p) | ...It's hard to know what the right answer is.