I have this code that fails (Explorer):
#include <range/v3/view/any_view.hpp>
#include <vector>
int main() {
std::vector<int> a{1};
const ranges::any_view<int, ranges::category::forward> av(a);
ranges::begin(av);
}
And was in the impression that begin returns a new iterator into the range, that allows me to iterate through it several times. Why would it need a modifiable range for that?
As reference (but I'm more interested in the reason of the above than (just) a solution to my problem below), in my real code, this appears as
void merge(ranges::any_view<int, ranges::category::forward> av) {
for(auto x : av) { ... }
}
And unfortunately clang-tidy warns that av should be made const& because it's passed by value but not moved inside of the function body.
My suspicion is that it has to do with the "amortized constant time complexity" requirement of begin / end calls. The following fails because I think the begin iterator can't be cached in the const view returned by drop:
std::list<int> b{1, 2, 3, 4};
const auto latter = ranges::views::all(b) | ranges::views::drop(2);
ranges::begin(latter);
So just in case the type-erased variable contains such a view, it has to cache the first result of begin for all wrapped types. However, even if I change av to random_access, it won't let me call begin on a const any_view:
const ranges::any_view<int, ranges::category::random_access> av(a);
And if this is indeed the reason why it fails for input, I wonder whether there's a way around this? Like the std::move_only_function allows a moving std::function (kind of) in C++23.
First, I'd call this very much a clang-tidy defect. It shouldn't complain about your passing
any_view<T>by value; norstring_view; norspan<T>; norfunction_ref<Sig>; nor any other type-erased view-semantic type. The entire point of these types is that you pass them by value.(Likewise, I would hope that clang-tidy doesn't complain about passing
unique_ptr<T>orshared_ptr<T>by value. In those cases, the reason to pass the type at all is to participate in lifetime, and if you take byconst&you're failing to participate in lifetime, which can cause awful bugs in a multithreaded program. Vice versa, if you don't need to participate in lifetime you should takeconst T&orconst T*, notconst unique_ptr<T>&.)Okay, so, @Barry says "PRs welcome [on
any_view, I'm going to assume]." I'm not 100% sure, but I think that would be a bad idea. Iterating a view in C++20-Ranges-world is a mutating operation by definition.any_viewtype-erases over that operation. Thus iterating anany_viewis a mutating operation by definition. Sure, one could physically slap aconstqualifier onany_view::begin, but that would be a semantic mistake, just like it was when C++11 did it forstd::function::operator().Here's what
std::functiondoes wrong:Both
t1andt2will try to modifyf's controlled lambda's captureiat the same time. This is UB.Now here's how
any_viewwould go wrong if it were const-iterable:Both
t1andt2will try to modifyv's controlledfilter_view's cache at the same time. This is UB.std::move_only_functionandstd::function_ref(and soonstd::copyable_functionIIUC) deal with this problem by providing different signatures for callability versus const-callability.There's no natural place to hang the
constonany_view's template parameter; but I could imagine a library providing bothlib::any_view<T>andlib::const_any_view<T>, if there were really any demand for the latter.As I said first of all, though, I don't think there should be any demand for the latter. The only reason you were trying to do
const any_view&at all is because clang-tidy told you to; and that's clearly just a bug in clang-tidy.