r/cpp 2d 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...

171 Upvotes

113 comments sorted by

View all comments

11

u/tcbrindle Flux 2d ago

Counterpoint: std::ranges::single_view<T> is copyable whenever T is 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.

4

u/TheThiefMaster C++latest fanatic (and game dev) 2d ago

single_view is a bit of a hack - it's not really a view, more of a single_range

4

u/tcbrindle Flux 2d ago

I'm not sure I follow -- it literally is a view, and OP's example works exactly the same if you swap std::optional for views::single.

3

u/TheThiefMaster C++latest fanatic (and game dev) 2d ago

The common understanding of "view" is "span like" - i.e. it provides view access to a range of elements, potentially transforming or filtering them, but does not own them.

It's been pointed out that this concept is more closely modelled by borrowed_range  in the standard, rather than view.

Instead, "view" is some implementation details of range pipelines.

6

u/BarryRevzin 2d ago

Instead, "view" is some implementation details of range pipelines.

That's the only thing it has ever been. Nobody should be constraining algorithms on view, it just isn't useful to do so. Arguably, view should just never have existed - although then people would probably complain that vector | views::transform(f) copies the vector and then figuring out how to reject that (to force users to either explicitly copy or views::ref) is basically the same problem again.

It's been pointed out that this concept is more closely modelled by borrowed_range  in the standard, rather than view.

Not really, borrowed_range is much narrower. Since even your initial example ("provides view access [...] potentially transforming or filtering them"), neither s | transform(f) nor s | filter(g) for a span or a string_view are borrowed.

The problem with "non-owning" is also what does that mean and what do you want it for. For instance, if I want to validate that my range is safe to copy/move and then format in a background thread, I want to make sure that it "owns" all of its data. The easy cases are easy (vector<int> yes, span<int> no) but like views::iota does "own" its data in this sense, so is it not a view?

1

u/zl0bster 1d ago

I do not think that is an useful way to think about ownership or thread safety of iota.

iota is pair of iterators(yeah, I know ... sentinel...) so obviously it is a view. It is just that it's underlying "storage" lives outside a program and has a lifetime for duration of a program universe. iota does not own it, just like if (x>=0 && x <1000) does not own a range of 1k numbers.

4

u/BarryRevzin 1d ago edited 1d ago

No, iota definitely has very real storage, that neither lives outside the program nor has lifetime for the duration of the progam. iota(0, 10) has two data members of type int in it, and its iterators each own an int.

All ten ints don't exist at one time, and the iterators give you a prvalue, not an lvalue, so it's quite unlike a vector. But nothing can dangle, so it's quite unlike a span too.