Why are views required to be (move-)assignable?
Asked Answered
L

2

5

The std::ranges::view concept in C++23 requires a view to be movable, which includes move-assignability. I understand why we want a view to be move-constructible, but why is the assignment necessary?

I ask because it seems that the assignability complicates things, i.e., range adaptors need the movable-box, which is exposition-only. Programmers thus need to implement it themselves to write their adaptors that hold function objects.

(I have tried looking at the original proposals but could not find any rationale specific for the move assignment.)

EDIT: To be more specific, can someone provide a realistic example where a view (created by a view adaptor or in a generic context) is assigned to?

Laboy answered 26/4, 2024 at 7:37 Comment(7)
Great question, which is something I've thought about as well. I think the main reason is to ensure the move semantics (which requires assignability) of the view.Flossi
Yes, but are there any actual examples of a view being assigned to?Laboy
For the standard view types string_view or span, it is very common to see them being assigned or reassigned. But for those in <ranges>, I never invoke their assignment operator because I've never encountered a situation where I needed to. Taking filter_view as an example, why do I need to reassign it instead of just making a new filter_view?Flossi
OK, let me rephrase that. Are there any actual examples of a view created from a view adaptor being assigned to?Laboy
Or even better, are there any examples of a view being assigned to in a generic context? (I.e., when this view comes from a templated argument.)Laboy
No. I personally think it is more appropriate to remove the assignment of view. No one would want to reassign zip_view or cartesian_product_view. movable-box does bring unnecessary complexity in my opinion and I see no real observable benefit.Flossi
It's somewhat easier to use a vector of those, I suppose. But I don't know if this particular piece of design actually has a rationale.Viscounty
I
4

Firstly, for unaware readers: std::ranges::view is defined in C++20 as:

template<class T>
  concept view =
    range<T> && movable<T> && default_­initializable<T> && enable_view<T>;

The fact that it's move-assignable simply stems from the use of std::movable, which requires both move construction and move assignment to be valid. Also note that std::movable requires a non-explicit assignment operator, and non-explicit move constructor.

Rationale

It may seem like over-constraining at first to have all these requirements, especially considering that views are very rarely assigned. However, this "over-constraining" avoids a lot of headaches for the user. If you put requires std::ranges::view<...> (and by proxy, requires std::movable<...>) on your function template, you have peace of mind that you can assign and construct it without weird gotchas, like explicit move constructors that would force you into direct-initialization.

There's also no particular reason why views cannot be assignable. If you're following the "rule of five" or "rule of three" (What is The Rule of Three?), then a view should have a move assignment operator anyway. Types with only a move constructor but no move assignment operator are highly unusual.

Last but not least, views have reference semantics and might not own resources, so making them assignable is sometimes trivial. movable-box also behaves very similarly to std::optional, and for most purposes, you can use that class template instead of having to implement your own. To store invocables, you can also use std::move_only_function in C++23. You're not forced into recreating the style of implementation that standard library range range adaptors use; it's not necessary to satisfy the concept.

Conclusion

C++20 in its concepts library makes a trade-off between making it harder to satisfy concepts (burden on library developers) and ease of using them (burden on most users). Here, C++20 simply prioritizes most language users, which seems reasonable.

Incardinate answered 26/4, 2024 at 8:0 Comment(5)
I disagree that types without move assignments are highly unusual. There are many such types that are normally used, most notably lambdas with non-empty capture context. The whole movable-box snafu also shows that making views assignable is often not trivial – using std::optional instead does not work directly (you still need to implement the move assignment by hand), std::move_only_function uses type erasure. Both go against the zero-cost principle. It would be nice to see whether there is any actual reason for the assignability that is worth the extra cost.Laboy
The cost is actually more about the copy operations. To have the cost, the most likely situation is something copy-constructible and not copy-assignable (e.g. a capturing lambda). If you have something instead that's (a) copyable or (b) not copy-constructible but is nothrow-move-constructible, movable-box<T> need only store a T. If movable-box<T> in fact only had move operations, there would be very few types incurring the cost. So, the tradeoff is in providing conditional copyability (specifically copy-assignability)...Saffren
Views however aren't required to be copyable, so the cost isn't to model view, it's to provide this extra functionality for the library's included views to its users. That is obviously a tradeoff: supporting unusual types with lower cost or supporting copying.Saffren
But that is not what my question is about. I am asking whether there exists a reason for including move-assignability to views. So far, you only gave me “there's also no particular reason why views cannot be assignable”, but that is not an answer I am satisfied with. I still haven't seen at least a simple toy example of a view (in generic context, not specific one like string_view) being assigned to.Laboy
Furthermore, you are missing the point about lambdas. Lambdas with non-empty context are usually move-constructible but not move-assignable. This means that all range adaptors that want to hold function objects need to use the movable-box trick (or use std::optional/std::move_only function, which both incur non-zero cost in space and/or time). My question is whether this complexity brings something to the “user”.Laboy
T
2

In my opinion it's just a historical artifact.

Views as originally proposed in P0896R4 were required to be semiregular (that is, copyable and default_initializable). This probably stems from the idea that views are simple and cheap ranges.

Later, P1456R1 "Move-only views" weakens the requirements to movable and default_initializable (notably, so that coroutine generators can be views). P2325R3 "Views should not be required to be default constructible" further removes the default_initializable requirement (to permit std::span<int, 42> to be a view).

So, it's a long journey to weaken the view requirements from semiregular to movable, and the motivation for further weakening is just not strong enough for anyone to write another proposal.

Tor answered 4/5, 2024 at 3:59 Comment(1)
OG (pre-P2328) join_view relied on move assignment when joining a range of prvalue views. I can't think of another example right now.Viscounty

© 2022 - 2025 — McMap. All rights reserved.