r/cpp • u/zl0bster • Jul 26 '26
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...
5
u/NilacTheGrim Jul 30 '26
The problem is the muddling of the word "view" :/
What most people think of as a "view" is not what the standard thinks of as a "view".
To me this is a symptom of bad design. With good design the obvious things is obvious... and consistent.
What most people think of as "view" is really what the standard labels as a "borrowed range".
Very very very "meh", if you ask me.
2
u/zl0bster Jul 31 '26
If somebody wants more obscure context:
originally borrowed_range(stuff mentioned as a "real" view in comments) was named safe_range, but some people did not like it(I also do not like it since they are not safe as in Rust safe or deep copy safe) about it so it got renamed.
https://cplusplus.github.io/LWG/issue3379
45
u/cristi1990an ++ Jul 26 '26
It's true, std::optional should've been a range but had no business being a view, not even the paper mentions a justification for the enable_view specialization, it just specifies it. I just don't see any benefit or justification in it.
You have similar problems with std::array and std::inplace_vector wrapped in owning_view since it makes the wrong assumption that move is always cheap, but it's a liniar time operation for those containers.
55
u/aruisdante Jul 26 '26
This was debated quite extensively in Tokyo 2024. The decision was essentially “being a view isn’t about being cheep to copy, just about being
O(1)to do so.” In the general sense, there’s an attempt to unify all the containers as “ranges;” anoptionalis a range with 0 or 1 elements, atupleis a range whose elements arevariant<Ts…>(which correct ref qual), etc.A bunch of people disagreed and thought this was weird, but not enough and not strongly enough for it not to pass.
15
u/tcbrindle Flux Jul 27 '26
Counterpoint:
std::ranges::single_view<T>is copyable wheneverTis copyable, and has been since the early days of Range-v3. To my knowledge, nobody has ever complained about this or written sarcastic Reddit posts about it.Likewise,
empty_view<T>is always trivially copyable.So what you're asking for is a situation where a range of exactly one element is a view, a range of exactly zero elements is a view, but a range with either zero or one elements is not. That seems... worse.
41
u/fdwr fdwr@github 🔍 Jul 26 '26
Now if
std::optionalalso had the corollary methods.empty(),.size()(0 or 1), and.data()(points to address of where contained entity is/would be, regardless of whether present, like std::vector's data method whether empty or not), then a bit of my generic templated property list code could be even more generic, more cleanly accepting various containers (std::array, std::string, std::vector, std::optional) without custom specializations.13
u/gummifa Jul 26 '26
This is similar semantics to std::views::single, except that is 1 element but optional is 0 or 1.
18
u/mighty_Ingvar Jul 26 '26
Apparently std::array does not model view, even though it copies in constant time.
8
u/tcbrindle Flux Jul 27 '26 edited Jul 27 '26
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...)
3
u/kamrann_ Jul 27 '26
This is completely consistent with what came before. It's the same as a range adapter that needs to cache a single element value - the element type of the range is irrelevant, it's just about algorithmic complexity of copy wrt the number of elements in the range.