|Project:||Programming Language C++|
Since [P2325R0], added discussion of the different treatments of lvalue vs rvalue fixed-extent
span in pipelines.
view concept is defined in 24.4.4
template <class T> concept view = range<T> && movable<T> && default_initializable<T> && enable_view<T>;
Three of these four criteria, I understand. A
view clearly needs to be a
range, and it’s important that they be
movable for various operations to work. And the difference between a
range is largely semantic, and so there needs to be an explicit opt-in in the form of
But why does a view need to be
The history of the design of Ranges is split between many papers and github issues in both the range-v3 [range-v3] and stl2 [stl2] libraries. However, I simply am unable to find much information that motivates this particular choice.
In [N4128], we have (this paper predates the term
view, at the time the term “range” instead was used to refer to what is now called a
view. To alleviate confusion, I have editted this paragraph accordingly):
We’ve already decided that [Views] are copyable and assignable. They are, in the terminology of [EoP] and [N3351], Semiregular types. It follows that copies are independent, even though the copies are both aliases of the same underlying elements. The [views] are independent in the same way that a copy of a pointer or an iterator is independent from the original. Likewise, iterators from two [views] that are copies of each other are also independent. When the source [view] goes out of scope, it does not invalidate an iterator into the destination [view].
Semiregular also requires DefaultConstructible in [N3351]. We follow suit and require all [Views] to be DefaultConstructible. Although this complicates the implementation of some range types, it has proven useful in practice, so we have kept this requirement.
There is also [stl2-179], titled “Consider relaxing the DefaultConstructible requirements,” in which Casey Carter states (although the issue is about iterators rather than views):
There’s concern in the community that relaxing type invariants to allow for default construction of a type that would not otherwise provide it is a horrible idea.
Relaxing the default construction requirement for iterators would also remove one of the few “breaking” differences between input and output iterators in the Standard (which do not require default construction) and Ranges (which currently do require default construction).
Though, importantly, Casey points out one concern:
The recent trend of making everything in the standard library
constexpris in conflict with the desire to not require default construction. The traditional workaround for delayed initialization of a non-default-constructible
Tis to instead store an
optional<T>. Changing an
optional<T>from the empty to filled states is not possible in a constant expression
This was true at the time of the writing of the issue, but has since been resolved first at the core language level by [P1330R0] and then at the library level by [P2231R1]. As such, I’m simply unsure what the motivation is for requiring default construction of views.
The motivation for default construction of iterators comes from [N3644], although this doesn’t really apply to output iterators (which are also currently required to be default constructible).
I couldn’t find any other motivation for default construction of views from the paper trail, so I tried to discover the motivation for it in range-v3. I did this with a large hammer: I removed all the default constructors and saw what broke.
And the answer is… not much. The commit can be found here: [range-v3-no-dflt]. The full list of breakage is:
join_with_view need a default-constructed inner view. This clearly breaks if that view isn’t default constructible. I wrapped them in semiregular_box.
views::indices are interesting in range-v3 because it’s not just that
ints(0, 4) gives you the range
[0,4) but also that
ints by itself is also a range (from
0 to infinity). These two inherit from iota, so once I removed the default constructor from iota, these uses break. So I added default constructors to
One of range-v3’s mechanisms for easier implementation of views and iterators is called
view_facade. This is an implementation strategy that uses the view as part of the iterator as an implementation detail. As such, because the iterator has to be default constructible, the view must be as well. So
chunk_view (the specialization for input ranges) kept their defaulted default constructors. But this is simply an implementation strategy, there’s nothing inherent to these views that requires this approach.
There’s one test for
any_view that just tests that it’s default constructible.
That’s it. Broadly, just a few views that actually need default construction that can easily provide it, most simply don’t need this constraint.
Rather than providing a benefit, it seems like the default construction requirement causes harm.
If the argument for default construction is that it enables efficient deferred initialization during view composition, then I’m not sure I buy that argument.
join_view would have to use an optional where it wouldn’t have before, which makes it a little bigger. But conversely, right now, every range adaptor that takes a function has to use an optional:
filter_view, etc. all need to be default constructible so they have to wrap their callables in
semiregular-box to make them default constructible. If views didn’t have to be constructible, they wouldn’t have to do this. Or rather, they would still have to do some wrapping, but we’d only need the assignment parts of
semiregular-box, and not the default construction part, which means that
sizeof(copyable-box<T>) would be equal to
sizeof(semiregular-box<T>) could be larger.
My impression right now is that the default construction requirement actually adds storage cost to range adapters on the whole rather than removing storage cost.
Furthermore, there’s the question of requiring a partially formed state to types even they didn’t want to do that. This goes against the general advice of making bad states unrepresentable. Consider a type like
span<int, 5>. This should be a
view: it’s a non-owning, O(1)-everything range. But it’s not default constructible, so it’s not a
view. The consequence of this choice is the difference in behavior when using fixed-extent
span in pipelines that start with an lvalue vs an rvalue:
std::span<int, 5> s = /* ... */; // Because span<int, 5> is not a view, rather than copying s into // the resulting transform_view, this instead takes a // ref_view<span<int, 5>>. If s goes out of scope, this will dangle. auto lvalue = s | views::transform(f); // Because span<int, 5> is a borrowed range, this compiles. We still // don't copy the span<int, 5> directly, instead we end up with a // subrange<span<int, 5>::iterator>. auto rvalue = std::move(s) | views::transform(f);
Both alternatives are less efficient than they could be. The lvalue case requires an extra indirection and exposes an opportunity for a dangling range. The rvalue case won’t dangle, but ends up requiring storing two iterators, which requires twice the storage as storing the single
span would have. Either case is strictly worse than the behavior that would result from
span<int, 5> having been a
span isn’t default-constructible for good reason: if we were to add a default constructor that would make
span<int, 5> partially formed, this adds an extra state that needs to be carefully checked by users, and suddenly every operation has additional preconditions that need to be documented. But this is true for every other view, too!
ranges::ref_view (see 188.8.131.52
[range.ref.view]) is another such view. In the same way that
std::reference_wrapper<T> is a rebindable reference to
ref_view<R> is a rebindable reference to the range
reference_wrapper<T> isn’t default constructible, but
ref_view<R> is — it’s just that as a user, I have no way to check to see if a particular
ref_view<R> is fully formed or not. All of its member functions have this precondition that it really does refer to a range that I as the user can’t check. This is broadly true of all the range adapters: you can’t do anything with a default constructed range adapter except assign to it.
If the default construction requirement doesn’t add benefit (and I’m not sure that it does) and it causes harm (both in the sense of requiring invalid states on types and adding to the storage requirements on all range adapters and further adding to user confusion when their types fail to model
view), maybe we should get rid of it?
default_initializable constraint from
view, such that the concept becomes:
default_initializable constraint from
weakly_incrementable. This ends up removing the default constructible requirement from input-only and output iterators, while still keeping it on forward iterators (
incrementable which requires
iota_view, replace the
semiregular<W> constraint with
copyable<W>, and add a constraint on
iota_view<W, Bound>::iterator’s default constructor. This allows an input-only
iota_view with a non-default-constructible
W while preserving the current behavior for all forward-or-better
Remove the default constructors from the standard library views and iterators for which they only exist to satisfy the requirement (
insert_iterator). Constrain the other standard library views’ default constructors on the underlying types being default constructible.
join_view, store the inner view in a
span<ElementType, Extent> a
view regardless of
Extent. Currently, it is only a
Extent == 0 || Extent == dynamic_extent.
We currently use
semiregular-box<T> to make types
semiregular (see 24.7.3
[range.semi.wrap]), which we use to wrap function objects throughout. We can do a little bit better by introducing a
copyable-box<T> such that:
copyable-box<T>is basically just
copyable-box<T>can be a thin wrapper around
Tthat adds a copy assignment operator that does destroy-then-copy-construct.
semiregular-box<T>(we still need
optional<T>’s empty state here to handle the case where copy construction can throw, to avoid double-destruction).
Replace all function object
semiregular-box<F> wrappers throughout
At the moment, only libstdc++ and MSVC provide an implementation of ranges (and MSVC’s is incomplete). We either have to make this change now and soon, or never.
[EoP] Stepanov, A. and McJones, P. 2009. Elements of Programming. Addison-Wesley Professional.
[N3351] B. Stroustrup, A. Sutton. 2012-01-13. A Concept Design for the STL.
[N3644] Alan Talbot. 2013-04-18. Null Forward Iterators.
[N4128] E. Niebler, S. Parent, A. Sutton. 2014-10-10. Ranges for the Standard Library, Revision 1.
[P1330R0] Louis Dionne, David Vandevoorde. 2018-11-10. Changing the active member of a union inside constexpr.
[P2231R1] Barry Revzin. 2021. Missing
[P2325R0] Barry Revzin. 2021-02-17. Views should not be required to be default constructible.
[range-v3] Eric Niebler and Casey Carter. 2013. range-v3 repo.
[range-v3-no-dflt] Barry Revzin. 2021. Removing default construction from range-v3 views.
[stl2] Eric Niebler and Casey Carter. 2014. stl2 repo.
[stl2-179] Casey Carter. 2016. Consider relaxing the DefaultConstructible requirements.