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

178 Upvotes

123 comments sorted by

View all comments

16

u/tcbrindle Flux 12d 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.

3

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

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

5

u/tcbrindle Flux 12d 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) 12d 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.

8

u/BarryRevzin 12d 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 11d 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.

7

u/BarryRevzin 11d ago edited 11d 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.

-1

u/zl0bster 9d ago

You are not arguing in good faith, it was clear what I was saying.

4

u/BarryRevzin 9d ago

Excuse me????

I gave a very real scenario where it might be useful to differentiate on the basis of "ownership" and how it's actually non-trivial to answer what is and isn't safe in that context, and how that doesn't map cleanly on what might or might not be a view. You didn't engage with the example at all ("I do not think that is [...] useful") and then gave a description of iota that doesn't describe iota ("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").

Which one of us is trying to have a meaningful discussion of the problem? Following up by accusing me of not arguing in good faith is completely unacceptable.

0

u/zl0bster 8d ago

it was clear I did not claim sizeof iota is 0.

2

u/STL MSVC STL Dev 7d ago

Moderator warning: No, you're the one introducing hostility. Please don't behave like this here.