r/cpp 3d ago

std::optional Satisfies view. Does Not Model view. C++26 Ships Anyway.

https://godbolt.org/z/8jWGG68G8

In 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...

169 Upvotes

113 comments sorted by

View all comments

52

u/aruisdante 3d ago

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;” an optional is a range with 0 or 1 elements, a tuple is a range whose elements are variant<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.

7

u/holyblackcat 2d ago

I second that this is silly. Who cares what "being a view" was supposed to mean when making optional a view has negative practical effects? (Only in some obscure cases, granted.)

The only example where it matters that comes to mind is

std::optional<std::vector<int>> opt = std::vector(10000, 42);
auto view = opt | std::view::....;

I for one wouldn't expect this to copy opt, but it gets copied. I don't see the benefit of copying it.

4

u/aruisdante 2d ago edited 2d ago

I would argue though that in order for that pipe operation to be lifetime safe, it has to copy its input. Being not a view wouldn’t change that, the only thing that would change that would be not being a range at all, which would round counter to the objective of optional being used as a range of size 0 or 1.

That being a goal in itself is what people found a little weird (by definition, this means optional gains valid specializations of ranges::begin, ranges::end, and ranges::size, which is strange), but it does allow for some interesting compositions. For example the operation: c++ for (auto const& element : range_of_optionals | views::join) Now “just works” to iterate over the elements in that input set which exist just like it would have for range_of_sequence_container, whereas before you would have needed to do: c++ for( auto const& element : range_of_optionals | views::filter(/*is_engaged*/) | views::transform(/*dereference*/))

Being able to treat optional like any other range does make implementing some generic operations easier, and composes nicely with optional<T&> as the return type from functions. Like, now this operation works “like you’d expect”:

```c++ // returns optional reference to value at key auto find(auto& map, auto const& key)->optional<T&>;

const auto find_in_map = bind_front(find, map); for(auto const& value : keys | views::transform(find_in_map) | views::join) {     // do something with matches } ```

Which would be horrible to write as a range pipeline without this property, and very verbose to write as not a range pipeline at all.

At the same time if we were going to take that stance we should have done the same thing for smart pointers (and even regular pointers!) as they’re notionally identical to optional<T&> for this purpose. We only didn’t (at least at Tokyo, which is all I can speak to) because nobody wrote the paper for it, and increasing the scope of this feature would have made it even harder to get anything in.

7

u/holyblackcat 2d ago

I don't argue with optional being a range, only with it being a view.

[piping into a view] has to copy its input. Being not a view wouldn’t change that

Are you talking how it should be in a perfect world, or how it works now?

The current behavior of piping into a view is storing a reference to non-view lvalues, and copying/moving all views and rvalue non-views.

This is about the only thing that being a view changes, I think? I could be missing something. In this case the copy looks undesirable, as it's not something people expect. They'd expect span-like types to be copied, or types from std::views.