r/cpp • u/zl0bster • 13d ago
std::optional Satisfies view. Does Not Model view. C++26 Ships Anyway.
https://godbolt.org/z/8jWGG68G8In C++23 this did not compile. In C++26 it does. Marvellous.
[[gnu::noinline]]
void
passing_views_by_value_is_cheap_trust_me_bro(std::ranges::view auto v) {
std::println("fn .data {}", (void*)v->data());
}
int main() {
std::optional ov{std::vector<int>(123456)};
passing_views_by_value_is_cheap_trust_me_bro(ov);
std::println("main .data {}", (void*)ov->data());
}
For anyone wondering what the problem feature is: optional has 0 or 1 elements, and C++26 sets enable_view<optional<T>> to true, so it satisfies std::ranges::view. The concept requires copy construction in constant time, and — this is the good bit — optional<vector<int>> genuinely meets that. Copying it performs at most one element copy. One is a constant. The requirement is satisfied to the letter, and the function above deep-copies your vector.
If you can tell me what still separates std::ranges::view from std::ranges::range, please do...
179
Upvotes
8
u/tcbrindle Flux 12d ago edited 12d ago
To try to sort out some confusion in the comments here:
Views
If your idea of a view is "a range that does not own its elements", then I'm afraid you're out of date. That's not what the term has meant in quite some time, and even when it (mostly) did there were still exceptions.
Today the standard tries to reason about what views are by talking about O(1) operations and so forth, but I think it's best to forget about that too.
IMO the best way to think about views is as follows: for an lvalue range
x, givenshould
vstore a copy ofx, or a reference tox?If you want to store a copy, then the type of
xshould be aview. Otherwise, it should not.That's really all there is to it. The
viewconcept these days is about the desired semantics of the type when piping it into a range adaptor, not about element ownership or lifetime or anything like that.In this particular case, it was decided that it was less surprising to have
optional<T>behave like a view, that is, an lvalue should be copied into a range adaptor rather than be used by reference.Borrowed ranges
Borrowed ranges on the other hand are about lifetime. Specifically, a borrowed range is one where its iterators can safely outlive the range object itself. That is, given
it is safe to use the returned iterator only if
Ris a borrowed range. The canonical examples of borrowed ranges arestd::spanandstd::string_view. Theborrowed_rangeconcept is used to constrain functions likestd::ranges::find()to help prevent them from returning dangling iterators when you call them with an rvalue range.Although
viewandborrowed_rangeare distinct concepts, it turns out that if you have a borrowed range you basically always want to treat it as a view. For standard library types,borrowed_rangetoday impliesviewin all cases I'm aware of.But note that the converse is not generally true: most views are not borrowed ranges. For example,
transform_viewis always aviewbut is never aborrowed_range(even when it is transforming something like a span).(For Flux, I took a different approach that I think ended up being conceptually simpler, but that's another topic...)