cbegin should always return a constant iterator| Document #: | P2278R4 |
| Date: | 2022-06-17 |
| Project: | Programming Language C++ |
| Audience: | LEWG |
| Reply-to: | Barry Revzin <barry.revzin@gmail.com> |
std::const_iteratorstd::basic_const_iterator<I>std::ranges::cbegin andstd::ranges::endviews::as_conststd::cbegin andstd::cend?make_const_iteratorspan<T>?Since[P2278R3], wording updates (including a revamp of all the comparison operators) and havingviews::as_const(s) for aspan<T> return aspan<Tconst>. This ensures that, for instance,s| views::take(n)| views::as_const ands| views::as_const| views::take(n) both produce the same type.
Since[P2278R2], renamedviews::all_const back toviews::as_const, seenaming. Wording fixes.
Since[P2278R1], renamedviews::as_const toviews::all_const. Added several additional alias templates and a feature-test macro. Fixed some wording issues.
Since[P2278R0], added wording (includingranges::cdata, which was omitted in the first revision, and adding membercbegin andcend toview_interface). Renamedviews::as_const toviews::as_const. Also fixedviews::as_const definition to handle deep-constviews (they do exist).
A tale in many acts.
25.3.1[iterator.requirements.general]/5 states:
Iterators that further meet the requirements of output iterators are calledmutable iterators. Nonmutable iterators are referred to asconstant iterators.
This paper uses those terms with those meanings: a mutable iterator is one that is writable to, a constant iterator is one that is not writable to.
cbeginIn 2004, C++0x had addedauto but not yet added the range-based for statement. So there was this problem: how do you write a for loop that is immutable? The goal of the paper was quite clear:
This paper proposes to improve user access to theconst versions of C++ containeriterators andreverse_iterators.
and:
However, when a container traversal is intended for inspection only, it is a generally preferred practice to use aconst_iterator in order to permit the compiler to diagnoseconst-correctness violations
The solution proposed in[N1674] (and later adopted by way of[N1913]) was to add memberscbegin() andcend() (andcrbegin() andcrend()) to all the standard library containers, facilitating this code:
c.cbegin() was specified in all of these containers to performas_const(c).begin(). Althoughstd::as_const itself was not added until much later - it is a C++17 feature, first proposed in[N4380].
cbeginC++11 thus added the free functionsstd::begin andstd::end, and member functionsc.cbegin() andc.cend(). But it did not yet have free functions to fetch constant iterators: those were added in 2013 by way of[LWG2128].
While,std::begin(c) always callsc.begin() (except for C arrays),std::cbegin(c) was not specified to callc.cbegin(). Instead it, too, calledstd::begin(c) (not evenc.begin()):
Implementstd::cbegin/cend() by callingstd::begin/end(). This has numerous advantages:
cbegin/cend() members were invented.initializer_list, which is extremely minimal and lackscbegin/cend() members.cbegin/cend() members.There are two important goals here to highlight.
First, the goal is still to provide constant iterators, not just callbegin()const. The latter is an implementation strategy for the former.
Second, the goal is to avoid boilerplate. An implementation wherestd::cbegin(c) calledc.cbegin() would requirec.cbegin() to exist, which, as is clear from the list above, is not the case for a lot of useful types.
As a result,std::cbegin(c) is basically specified to bestd::begin(as_const(c)) (although, again, predatingstd::as_const) which is basicallyas_const(c).begin().
The status quo at this point is thatc.cbegin(),as_const(c).begin(), andstd::cbegin(c) are all equivalent (where they are all valid) and all yield constant iterators.
Before 2018, the standard library had two non-owning range types:std::initializer_list<T> (since C++11) andstd::string_view (since C++17). Non-owning ranges are shallow-const, but both of these types arealways-const so that distinction was insignificant.
That soon changed. 2018 opened with the addition ofstd::span[P0122R7] and closed with the adoption of Ranges[P0896R4], with a few more views added the subsequent year by way of[P1035R7]. Now, for the first time, the C++ standard library had non-owning ranges that were nevertheless mutable. Ranges itself was but a small part of the range-v3 library, so there is a promise of many more views to come.
These types really throw a wrench in thecbegin design: because nowbegin()const does not necessarily yield a constant iterator, whereas this had previously always been the case.
It’s important to note that while it had previously always been the casein the standard library, that is not true for the broad C++ community. In particular, Boost.Range (which begat range-v3 which begat C++20 Ranges) has for a very long time had a type namedboost::iterator_range<It> (the predecessor tostd::ranges::subrange<It>). This is a view, although that term hadn’t existed yet, and so it had abegin()const member function that just returned anIt. Which means thatstd::cbegin on aniterator_range<int*> gives you anint* - a mutable iterator.
Where this discrepancy became most apparently visible was the specification ofstd::span during the ballot resolution (by way of[LWG3320]). For the sake of simplicity, I am going to assume that the iterator types ofspan<T> andspan<Tconst> are justT* andTconst*, respectively.
span<T>::begin()const, like all the other views, is shallowconst, and so returnsT*.span<T>::cbegin()const, like the other standard library containers, was provided for convenient access to a constant iterator. This returnedTconst*. Unlike the other standard library containers, this did not simply defer tobegin()const.So far so good. But becausestd::cbegin(s) is specified to dostd::begin(as_const(s)), we end up having different behavior betweens.cbegin() andstd::cbegin(s). This is the first (and, thus far, only) type in the standard library for which this is the case - and whiles.cbegin() would have yielded a constant iterator,std::cbegin(s) does not.
As a result of NB comment resolution, to ship a coherent C++20,span’scbegin() andcend() members were removed, for consistency.
This leaves us in a state where:
for all the standard library containers,r.cbegin() andstd::cbegin(r) are equivalent, both meaningas_const(r).begin(), and both yielding a constant iterator. This is likely true for many containers defined outside of the standard library as well.
for most of the standard library views,r.cbegin() does not exist andstd::cbegin(r) is a valid expression that could yield a mutable iterator (e.g. std::span<T>). There are three different kinds of exceptions:
std::string_view::cbegin() exists and is a constant iterator (since it isconst-only).std::initializer_list<T>::cbegin() doesnot exist, butstd::cbegin(il) also yields a constant iterator.std::ranges::single_view<T> is an owning view and is actually thus deepconst. While it does not have acbegin() member function,std::cbegin(v) nevertheless yields a constant iterator (the proposedviews::maybe in[P1255R6] would also fit into this category).std::ranges::filter_view<V, F> is not actuallyconst-iterable at all, so it is neither the case thatfilt.cbegin() exists as a member function nor thatstd::cbegin(filt) (norstd::ranges::cbegin(filt)) is well-formed. Many other views fit this category as well (drop_view being the most obvious, butdrop,reverse, andjoin may not be either, etc.). Other future views may fit into this category as well (e.g. my proposed improvement toviews::split in[P2210R0]).Put differently, the C++20 status quo is thatstd::cbegin on an owning range always provides a constant iterator whilestd::cbegin on a non-owning view could provide a mutable iterator or not compile at all.
The original desire of Walter’s paper from more than 15 years ago (which, in 2020 terms, may as well have happened at the last Jupiter/Saturn conjunction) still holds today:
However, when a container traversal is intended for inspection only, it is a generally preferred practice to use aconst_iterator in order to permit the compiler to diagnoseconst-correctness violations
How could we addconst-correctness to views?
cbegin()One approach we could take to provide reliableconst-traversal of unknown ranges is to push the problem onto the ranges:
std::cbegin(c) (andstd::ranges::cbegin(c) as well) first tries to callc.cbegin() if that exists and only if it doesn’t to fall-back to its present behavior ofstd::begin(as_const(c)).cbegin()const that yields a constant iterator. Even the ones likestd::initializer_list<T> that don’t currently have such a member?Such a design would ensure that for all standard library ranges,r.cbegin() andstd::cbegin(r) are equivalent and yield a constant iterator. Except forfilter_view, for whichstd::cbegin(filt) would continue to not compile as it takes aCconst&.
What does this do for all the views outside of the standard library? It does nothing.std::cbegin(v) on such views would continue to yield a mutable iterator, as it does today withboost::iterator_range. That, in of itself, makes this change somewhat unsatisfactory.
But what would it actually mean to add a membercbegin()const to every view type? What would such a member function do? What itshould do is the exact same thing for every view — the same exact same thing that all views external to the standard library would have to do in order to opt in toconst-traversal-on-demand.
But if every type needs to do the same thing, that’s an algorithm. The standard library should provide it once rather than having every view re-implement it. Or, more likely, have every view delegate to the algorithm and just have boilerplate member function implementations. A substantial amount of view implementations are already boilerplate, we do not need more.
std::const_iteratorThe problem we actually have is this: given an iterator, how do I create an iterator that is identical in all respects except for top-level mutability? This is, ultimately, the problem that from the very beginningvector<T>::const_iterator is intending to solve. It is avector<T>::iterator in all respects (it’s contiguous, its value type isT, it would have the same bounds coming from the same container) except that dereferencing such an iterator would give aTconst& instead of aT&.
We’re already used to the fact that some iterators are generic wrappers over other iterators.vector<T> is already specified as:
namespace std{template<class T,class Allocator= allocator<T>>class vector{public:// typesusing iterator=implementation-defined;// see [container.requirements]using const_iterator=implementation-defined;// see [container.requirements]using reverse_iterator= std::reverse_iterator<iterator>;using const_reverse_iterator= std::reverse_iterator<const_iterator>;Nobody is especially surprised by the fact that every container isn’t manually implementing its own bespoke reverse iterators.std::reverse_iterator<It> does the job. Yet,std::rbegin(c) always callsc.rbegin() (except for arrays). Even though we’ve had this perfectly generic solution for a long time, if you wanted your container to support reverse-iteration, you just had to write these boilerplaterbegin()/rend() member functions that wrapped your iterators.
Ranges improved this situation.std::ranges::rbegin(E) is a much more complicated algorithm that takes many steps (see26.3.6[range.access.rbegin] for complete description), but a key aspect of the design there is that ifstd::ranges::begin(E) andstd::ranges::end(E) give youbidirectional_iterators, thenstd::ranges::rbegin(E) itself does the wrapping and provides youmake_reverse_iterator(ranges::end(E)). No more pushing work onto the containers. That means that it works even in this case:
struct simple_span{int* begin()const;int* end()const;};void algo(simple_span ss){auto rit= std::ranges::rbegin(ss);// okauto rend= std::ranges::rend(ss);// ok// ...}std::rbegin would’ve failed in this case, because we don’t have the boilerplate necessary to make it work. But instead of pushing that boilerplate ontosimple_span, we consigned it intostd::ranges::rbegin. A much better solution.
A genericreverse_iterator<It> is not that complicated. We’re basically inverting operations. But the crux of the iterator remains the same:*it passes through to its underlying iterator.
A genericconst_iterator<It> at first seems much less complicated.Every operation is passthrough, except for one. We areonly modifying the behavior of the dereference operator. Yet, doing the right thing for dereference is decidedly non-trivial. Let’s go through some cases. We’re going to look at both the value type and reference type of several ranges and say what we want the desiredconst_iterator<iterator_t<R>> to dereference into:
range_value_t<R> | range_reference_t<R> | desired result type | |
|---|---|---|---|
vector<int> | int | int& | intconst& |
vector<int>const | int | intconst& | intconst& |
array<intconst, N> | int | intconst& | intconst& |
a range of prvalueint | int | int | int |
vector<bool>const | bool | bool | bool |
vector<bool> | bool | vector<bool>::reference | bool |
zipping avector<T> andvector<U> | tuple<T, U> | tuple<T&, U&> | tuple<Tconst&, Uconst&> |
This table points out a few things:
range_value_t<R>const&, but while that works in some cases, it would lead to every element dangling in other cases.It is already a constant iterator, so we would want to actively avoid wrapping in such a case.Thankfully, this is a solved problem. Theviews::const_ adapterin range-v3 has for many years used a formula that works for all of these cases. In C++20 Ranges terms, I would spell it this way:
template<std::input_iterator It>using const_ref_for= std::common_reference_t< std::iter_value_t<It>const&&, std::iter_reference_t<It>>;This does not yield the correct result for the last row in my table at the moment, but now that we are making the changes tostd::tuple prescribed in[P2321R2], it soon will.
Avoiding unnecessary wrapping can be achieved through a factory function that checks to see if such wrapping would change type:
// a type is a constant iterator if its an iterator whose reference type is// the same as the type that const_ref_for would pick for ittemplate<typename It>conceptconstant-iterator= std::input_iterator<It>&& std::same_as<const_ref_for<It>, std::iter_reference_t<It>>;// a type is a constant range if it is a range whose iterator is a constant iteratortemplate<typename R>conceptconstant-range= std::ranges::range<R>&&constant-iterator<std::ranges::iterator_t<R>>;template<std::input_iterator It>constexprauto make_const_iterator(It it){ifconstexpr(constant-iterator<It>){// already a constant iteratorreturn it;}else{return basic_const_iterator<It>(it);}}template<std::input_iterator It>using const_iterator=decltype(make_const_iterator(std::declval<It>()));Unfortunately we have a lot of names here:
const_iterator<I> is an alias template that gives you a constant iterator version ofI. IfI is already a constant iterator, thenconst_iterator<I> isI.make_const_iterator<I>(i) is a factory function template that takes an input iterator and produces aconst_iterator<I>. Likewise, ifI is already a constant iterator, then this function returnsi.basic_const_iterator<I> is an implementation detail of the library to satisfy the requirements of the above to ensure that we get a constant iterator.It’s important to haveconst_iterator<intconst*> produce the typeintconst*. If we just had a single class templateconst_iterator (that itself was the implementation for a constant iterator), then it could lead to subtle misuse:
template<typename T>struct span{using iterator=/* ... */;using const_iterator= std::const_iterator<iterator>;};span<Tconst>::iterator is already a constant iterator, it doesn’t need to be wrapped further. Sospan<Tconst>::const_iterator should really be the same type. It would be nice if the above were already just correct. Hence, the extra names. Users probably never need to usestd::basic_const_iterator<I> directly (or, indeed, evenmake_const_iterator).
std::basic_const_iterator<I>There’s a lot of boilerplate in implementing a C++20 iterator. And especially forbasic_const_iterator<I> where just about every operation is simply a pass-through to the underlying iterator. Only one function ends up having a body consisting of more than a singlereturn statement.
Despite that, there’s one especially important aspect to implementing abasic_const_iterator<I> that adds complexity: conversions and comparisons. We have this expectation that a range’s mutable iterator is convertible to its constant iterator. That is, given any typeR suchrange<R> andrange<Rconst> both hold, thatiterator_t<R> is convertible toiterator_t<Rconst>. For example, we expectvector<T>::iterator to be convertible tovector<T>::const_iterator, and likewise for any other container or view.
Adding the concept of aconst_iterator further complicates matters because now we have the following cross-convertibility and cross-comparability graph:
A black arrow fromT toU indicates thatT needs to be convertible toU, while the blue bidirectional dotted arrows betweenT andU indicate thatequality_comparable_with<T, U> holds (i.e. not only that the types can be compared with== and!= but also that there is a common type between them). That is, every pair of types here needs to modelequality_comparable_with.
Even thoughiterator_t<Rconst> andconst_iterator<iterator_t<R>> are not convertible to each other, they still have a common type that both are convertible to (const_iterator<iterator_t<Rconst>>) which also needs to be reflected.
The implementation ofbasic_const_iterator<I> needs to properly support this graph: which means ensuring the right set of constructors, comparison operators, and even specializations ofcommon_type.
The same sort of idea holds for sentinels. How would we wrap sentinels? Imagine a hypotheticalconst_sentinel<I, S>. It would have to have the following behavior:
Becauseiterator_t<Rconst> would be comparable tosentinel_t<R>, it needs to follow thatconst_iterator<iterator_t<R>> needs to be comparable tosentinel_t<R> as well. That is,sentinel_for<const_iterator<I>, S> needs to hold wheneversentinel_for<I, S> holds. This begs the question of if we need aconst_sentinel<I, S> type at all, given that we need to support comparisons to the unwrapped sentinel anyway. It’s a complex interplay of types to get right, and it doesn’t seem like aconst_sentinel type adds value for us at all. Hopefully, we don’t find a problem that necessitates wrapping in the future (c.f.[LWG3386]).
The tricky part of the implementation is to avoid constraint recursion in ensuring that wrapped random access iterators are totally ordered and ensuring that wrapped sized sentinels are still sized sentinels. A first attempt at attempting to define anoperator<=> forbasic_const_iterator<I> might start with:
template<std::input_iterator It>struct basic_const_iterator{template<std::totally_ordered_with<It> Rhs>requires std::random_access_iterator<It>autooperator<=>(Rhsconst& rhs)const;};But when checking to see iftotally_ordered<basic_const_iterator<int*>>, we would check to see if we can instantiateoperator<=>, which requires checking iftotally_ordered_with<basic_const_iterator<int*>, basic_const_iterator<int*>> (sinceRhs is our same type), which itself requires checkingtotally_ordered<basic_const_iterator<int*>>. And now we’ve completed the cycle.
The way I chose to handle this problem is to split the implementation into two functions: a same-type, non-template comparison and a template that is constrained on different types:
template<std::input_iterator It>struct basic_const_iterator{autooperator<=>(basic_const_iteratorconst& rhs)constrequires std::random_access_iterator<It>;template<different-from<basic_const_iterator> Rhs>requires std::random_access_iterator<It>and std::totally_ordered_with<It, Rhs>autooperator<=>(Rhsconst& rhs)const;};Other things to note about this implementation:
iterator_concept= contiguous_iterator_tag; ensures that wrapping a contiguous mutable iterator produces a contiguous constant iterator.iterator_category forforward_iterators ensures that we correctly handle C++20 input iterators (more on this later, and see also[P2259R0]).reference type for this iterator, described earlier.template<typename It>struct iterator_concept_for{};template<typename It>requires std::contiguous_iterator<It>struct iterator_concept_for<It>{using iterator_concept= std::contiguous_iterator_tag;};template<typename It>struct iterator_category_for{};template<std::forward_iterator It>struct iterator_category_for<It>{using iterator_category=typename std::iterator_traits<It>::iterator_category;};template<std::input_iterator It>class basic_const_iterator:public iterator_concept_for<It> ,public iterator_category_for<It>{ It it;public:using value_type= std::iter_value_t<It>;using difference_type= std::iter_difference_t<It>;using reference= const_ref_for<It>; basic_const_iterator()=default; basic_const_iterator(It it): it(std::move(it)){}template<std::convertible_to<It> U> basic_const_iterator(basic_const_iterator<U> c): it(std::move(c.base())){} basic_const_iterator(std::convertible_to<It>auto&& c): it(FWD(c)){}autooperator++()-> basic_const_iterator&{++it;return*this;}autooperator++(int)-> basic_const_iteratorrequires std::forward_iterator<It>{auto cpy=*this;++*this;return cpy;}voidoperator++(int){++*this;}autooperator--()-> basic_const_iterator&requires std::bidirectional_iterator<It>{--it;return*this;}autooperator--(int)-> basic_const_iteratorrequires std::bidirectional_iterator<It>{auto cpy=*this;--*this;return cpy;}autooperator+(difference_type n)const-> basic_const_iteratorrequires std::random_access_iterator<It>{return basic_const_iterator(it+ n);}autooperator-(difference_type n)const-> basic_const_iteratorrequires std::random_access_iterator<It>{return basic_const_iterator(it- n);}friendautooperator+(difference_type n, basic_const_iteratorconst& rhs)-> basic_const_iterator{return rhs+ n;}autooperator+=(difference_type n)-> basic_const_iterator&requires std::random_access_iterator<It>{ it+= n;return*this;}autooperator-=(difference_type n)-> basic_const_iterator&requires std::random_access_iterator<It>{ it-= n;return*this;}autooperator-(basic_const_iteratorconst& rhs)const-> difference_typerequires std::random_access_iterator<It>{return it- rhs.it;}autooperator[](difference_type n)const-> referencerequires std::random_access_iterator<It>{return it[n];}autooperator*()const-> reference{return*it;}autooperator->()const-> value_typeconst*requires std::contiguous_iterator<It>{return std::to_address(it);}template<sentinel_for<It> S>autooperator==(Sconst& s)const->bool{return it== s;}autooperator<=>(basic_const_iteratorconst& rhs)constrequires std::random_access_iterator<It>{return it<=> rhs;}template<different-from<basic_const_iterator> Rhs>requires std::random_access_iterator<It>and std::totally_ordered_with<It, Rhs>autooperator<=>(Rhsconst& rhs)const{ifconstexpr(std::three_way_comparable_with<It, Rhs>){return it<=> rhs;}elseifconstexpr(std::sized_sentinel_for<Rhs, It>){return(it- rhs)<=>0;}else{if(it< rhs)return std::strong_ordering::less;if(rhs< it)return std::strong_ordering::greater;return std::strong_ordering::equal;}}template<std::sized_sentinel_for<It> S>autooperator-(Sconst& s)const-> std::iter_difference_t<It>{return it- s;}template<different-from<basic_const_iterator> S>requires std::sized_sentinel_for<S, It>friendautooperator-(Sconst& s, basic_const_iteratorconst& rhs)-> std::iter_difference_t<It>{return s- rhs.it;}auto base()-> It&{return it;}auto base()const-> Itconst&{return it;}};template<typename T, std::common_with<T> U>struct std::common_type<basic_const_iterator<T>, U>{using type= basic_const_iterator<std::common_type_t<T, U>>;};template<typename T, std::common_with<T> U>struct std::common_type<U, basic_const_iterator<T>>{using type= basic_const_iterator<std::common_type_t<T, U>>;};template<typename T, std::common_with<T> U>struct std::common_type<basic_const_iterator<T>, basic_const_iterator<U>>{using type= basic_const_iterator<std::common_type_t<T, U>>;};Since the above implementation satisfies the requirements for asentinel where appropriate, we can complete our implementation by providing amake_const_sentinel to mirror themake_const_iterator shown earlier:
template<typename S>constexprauto make_const_sentinel(S s){ifconstexpr(std::input_iterator<S>){// the sentinel here is an iterator in its own right, so we need to (possibly) wrap it the same wayreturn make_const_iterator(std::move(s));}else{return s;}}We could take the iterator type as a template parameter to enforce thatS satisfiessentinel_for<I>, but this function is only used as a building block of an algorithm that would already enforce this, so it’s probably not necessary.
std::ranges::cbegin andstd::ranges::endstd::ranges::cbegin today (26.3.4[range.access.cbegin]) unconditionally callsstd::ranges::begin. Similarly,std::cbegin today (25.7[iterator.range]) unconditionally callsstd::begin. The status quo in the library is that nothing anywhere invokesmembercbegin. The goal is to provide a constant iterator version ofbegin() — we have not had a customization point for this facility in the past and we can achieve this goal without having to add a customization point for the future.
With the above pieces, we implement aranges::cbegin andranges::end to ensure that we get a constant iterator (see full implementation[const-impl], complete with many tests):
inlineconstexprauto possibly_const=[]<std::ranges::range R>(R& r)->auto&{// we only cast to const if it is meaningful to do soifconstexpr(constant-range<Rconst>andnotconstant-range<R>){returnconst_cast<Rconst&>(r);}else{return r;}};inlineconstexprauto cbegin= first_of(// 1. non-borrowed rvalue delete_if_nonborrowed_rvalue,// 2. possibly-wrapped begin of possibly-const-range r[](std::ranges::rangeauto&& r) RETURNS(make_const_iterator(std::ranges::begin(possibly_const(r)))));inlineconstexprauto cend= first_of(// 1. non-borrowed rvalue delete_if_nonborrowed_rvalue,// 2. possibly-wrapped end of possibly-const-range r[](std::ranges::rangeauto&& r) RETURNS(make_const_sentinel(std::ranges::end(possibly_const(r)))));Here,cbegin(r) andcend(r) produce a range that is top-level const over any underlying range, without having to modify any of those underlying ranges to opt in to this behavior. This works forstd::vector<int> andstd::span<int> andboost::iterator_range<int*> and even views likestd::ranges::filter_view (possibly_const ensures that if get passed a non-constvector<int>, we treat it asconst first — which is both valid and necessary — whilefilter_viewconst isn’t arange so we cannot treat it asconst first).
Avoiding a customization point here lets us give an easy answer to the question of whether or not types should provide a membercbegin going forward: no, they shouldn’t. Users that want a constant iterator can use this facility, which will work for all ranges.
In addition to simply working across all ranges, it has a few other features worth noting:
vector<int> v,cbegin(v) gives precisely the typevector<int>::const_iterator (notconst_iterator<vector<int>::iterator>).span<int> s,cbegin(s) provides a contiguous iterator overintconst&, not just any kind of iterator.transform_view<V, F>, weavoid casting toconst, since it doesn’t do anything for us. But for asingle_view<T> (which is an owning view and thus deepconst), wedo cast toconst.Note that here,ranges::end already checks that the type returned is asentinel for the type returned byranges::begin. Given that fact, the implementation here already ensures thatsentinel_for<decltype(cbegin(r)),decltype(cend(r))> holds.
views::as_constA whole const-view can be implemented on top of these pieces, in the same way thatviews::reverse is implemented on top ofreverse_iterator (see the full implementation[const-impl]):
template<std::ranges::input_range V>requires std::ranges::view<V>class const_view:public std::ranges::view_interface<const_view<V>>{ V base= V();public:constexpr const_view()=default;constexpr const_view(V base): base(std::move(base)){}constexprauto begin()requires(!simple-view<V>){return cbegin(base);}constexprauto end()requires(!simple-view<V>){return cend(base);}constexprauto size()requires std::ranges::sized_range<V>{return std::ranges::size(base);}constexprauto begin()constrequires std::ranges::range<Vconst>{return cbegin(base);}constexprauto end()constrequires std::ranges::range<Vconst>{return cend(base);}constexprauto size()constrequires std::ranges::sized_range<Vconst>{return std::ranges::size(base);}};template<typename V>inlineconstexprbool::std::ranges::enable_borrowed_range<const_view<V>>= std::ranges::enable_borrowed_range<V>;// libstdc++ specific (hopefully standard version coming soon from P2387!)inlineconstexpr std::views::__adaptor::_RangeAdaptorClosure as_const=[]<std::ranges::viewable_range R>(R&& r){using U= std::remove_cvref_t<R>;ifconstexpr(constant-range<std::views::all_t<R>>){return std::views::all(FWD(r));}elseifconstexpr(std::is_lvalue_reference_v<R>andconstant-range<Uconst>andnot std::ranges::view<U>){return std::views::all(std::as_const(r));}else{return const_view<std::views::all_t<R>>(std::views::all(FWD(r)));}};The three cases here are:
r is already a constant range, no need to do anything, pass it through. Examples arestd::span<Tconst> orstd::vector<T>const orstd::set<T>. We specifically checkall_t<R> rather thanR to handle the case whereR might be a view that is a constant range, but we have a const object that is a deep-const view (e.g. ifr is asingle_view<int>const, thenviews::all(r) would be asingle_view<int> which is no longer a constant range).r is not a constant range butstd::as_const(r) would be. Rather than do any wrapping ourselves, we defer tostd::as_const. Example isstd::vector<T>. We explicitly removeviews from this set, becauseviews::all() would drop theconst anyway and we may need to preserve it (e.g. single_view<T>). We only allow lvalue ranges (necessary in a post-[P2415R2] world) because rvalue ranges (like views) would also drop theconst even if we added it.r is neither a constant range nor can easily be made one, so we have to wrap ourselves. Examples are basically any mutable view.To me, being able to provide aviews::as_const is the ultimate solution for this problem, since it’s the one that guarantees correctness even in the presence of a range-based for statement:
Or passing a constant range to an algorithm:
As far as naming goes, obviously we can’t just call itviews::const. range-v3 uses the nameviews::const_.
[P2278R0] used range-v3’sviews::const_.[P2278R1] switched toviews::as_const. The advantage ofviews::as_const is that it is a mirror ofstd::as_const, so sharing a name seems reasonable. Indeed, when ranges are involved,views::as_const is superior tostd::as_const as it ensures immutable elements.
A recent LWg telecon[p2278-minutes] preferred the nameviews::all_const under the premise thatviews::as_const is too much likestd::as_const and that users might think it appliesconst to the top-level range rather than each element. But, as[P2501R0] points out,views::all_const is… not a great name. While[P2278R2] used the nameviews::all_const, the main motivation of theall_meow naming was in conjunction with renaming[P2446R0]’sviews::move toviews::all_move, and that rename was driven by examples like this:
Here, the user intended tostd::move and now accidentally usesstd::views::move. A very different outcome.
But this isn’t really the same level of issue for usingstd::views::as_const instead ofstd::as_const (indeed, it may even be an improvement to accidentally use the range adaptor here). The LEWG discussion on that paper suggested that the desire to avoidviews::move was much stronger than the desire to avoidviews::as_const, and the polling did not provide a clear direction for this adaptor.
As such, this revision (R3) reverts to[P2278R1]’s choice of name:views::as_const. This strikes me as the best name amongst the options.
std::cbegin andstd::cend?The above presents an implementation strategy forstd::ranges::cbegin andstd::ranges::cend.
But what aboutstd::cbegin andstd::cend? The problem is, while the former is C++20 technology and so can make use of the C++20 iterator model,std::cbegin is C++11 technology and so has to remain fixed with the C++11 iterator model. The biggest difference for these purposes has to do with input iterators.
In C++11, even for an input iterator, this is valid code:
But in C++20, an input iterator’s postfix increment operator need not return a copy of itself. All the ones in the standard library returnvoid. This is the safer design, since any use of postfix increment that isn’t either ignoring the result or exactly the above expression are simply wrong for input iterators.
Trying to be backwards compatible with pre-C++20 iterators is quite hard (again, see[P2259R0]), and thebasic_const_iterator<It> implementation provided in this paper wouldnot be a valid C++17 input iterator. Additionally, C++20 input iterators are required to be default constructible while C++17 input iterators were not.
On the other hand, is it critically important to have a constant iterator for a C++17 input iterator? You’re not going to mutate anything meaningful anyway.
A simple solution could havestd::cbegin(c) pass through C++17 input iterators unconditionally, and otherwise domake_const_iterator(as_const(c).begin()) (i.e. thestd::ranges::cbegin described above) for all iterators that are either C++20 iterators or C++17 forward iterators. This probably addresses the majority of the use-cases with minimal fuss.
Moreover, would we want to extendstd::cbegin to also handle non-const-iterable ranges? This is technically doable:
template<typename C>concept can_begin=requires(C& c){ std::begin(c);}template<class C>requires can_begin<const C>constexprauto cbegin(const C& c){// today's overload, but also conditionally wrap}template<class C>requires(can_begin<C>&&!can_begin<const C>)constexprauto cbegin(C& c){// fallback for non-const-iterable ranges}I negated the constraint to prefer theconst C& overload even for non-const arguments.
But we didn’t introduce a C++20 iteration model to then go back and give us more work to keep two models in lock-step. The C++20 one on its own is hard enough, and is a better model. We should just commit to it.
We could consider doing something like:
template<class C>constexprauto cbegin(Cconst& c)requiresrequires{ std::begin(c);}{ifconstexpr(std::forward_iterator<decltype(std::begin(c))>){return make_const_iterator(std::begin(c));}else{// leave input iterators (or... whatever) alonereturn std::begin(c);}}And similarly forstd::cend. This isn’t entirely without peril: currently we say nothing about the constraints forstd::cbegin(r); just that it callsr.begin(),whatever that is. There isn’t a requirement that this ends up giving an iterator and there’s no requirement that any of the operations thatstd::forward_iterator would check are SFINAE-friendly. I don’t know if we necessarily care about such (mis)uses ofstd::cbegin, but it is worth noting.
But what would we gain by making this change? Sure,std::cbegin(x) is now more likely to produce a constant iterator than it was before since we’re going to improve cases likespan. But it still won’t work for non-const-iterable ranges (likedrop_while orfilter), or shallow-const input ranges, and has a chance to break existing code. As such, it doesn’t really feel worth doing, sincestd::ranges::cbegin simply works for all of these cases, so why not use that?
Now that we can produce a constant iterator for a range, producing a reversed constant iterator is a matter of composing operations. We don’t have to worry about sentinels in this case:
inlineconstexprauto crbegin= first_of(// 1. non-borrowed rvalue delete_if_nonborrowed_rvalue,// 2. possibly-wrapped reversed begin of possibly-const-range range[](std::ranges::rangeauto&& r) RETURNS(make_const_iterator(std::ranges::rbegin(possibly_const(r)))));inlineconstexprauto crend= first_of(// 1. non-borrowed rvalue delete_if_nonborrowed_rvalue,// 2. possibly-wrapped reversed end of possibly-const-range range[](std::ranges::rangeauto&& r) RETURNS(make_const_iterator(std::ranges::rend(possibly_const(r)))));Notably here,ranges::rbegin andranges::rend already themselves will try to produce astd::reverse_iterator if the underlying range is bidirectional. Similarly, we’re ourselves trying to produce astd::const_iterator.
The extension tostd::crbegin andstd::crend mirrors the similar extension tostd::cbegin andstd::cend:
template<class C>constexprauto crbegin(Cconst& c)requiresrequires{ std::rbegin(c);}{if consetxpr(std::forward_iterator<decltype(std::rbegin(c))>){return make_const_iterator(std::rbegin(c));}else{// leave input iterators (or... whatever) alonereturn std::rbegin(c);}}Even though in the standard library we defineconst_reverse_iterator asstd::reverse_iterator<const_iterator> and the algorithm presented here constifies a reverse iterator, it still produces the same result for all standard library containers becauserbegin()const returnsconst_reverse_iterator, so we already have a constant iterator just fromrbegin.
make_const_iteratorThe job ofmake_const_iterator(it) is to ensure that its result is a constant iterator that is as compatible as possible withit (ideally, exactlyit where possible). For a typical iterator,I, the best we could do ifI is a mutable iterator is to return aconst_iterator<I>. But what if we had more information. Could we do better?Should we do better?
Considerchar*.char* is not a constant iterator, and themake_const_iterator presented here will yield aconst_iterator<char*>. But there is a different kind of constant iterator we could return in this case that satisfies all the complicated requirements we’ve laid out for how a constant iterator should behave, and it’s kind of the obvious choice:charconst*. Returningconstchar* here does the advantage in that we avoid having to do this extra template instantiation, which is certainly not free.
The question is: shouldmake_const_iterator(char*) yield acharconst* instead of aconst_iterator<char*>?
Let’s take a look at a test-case. Here is a contiguous range that is a null-terminated mutable character string:
struct zsentinel{autooperator==(char* p)const->bool{return*p=='\0';}};struct zstring{char* p;auto begin()const->char*{return p;}auto end()const-> zsentinel{return{};}};You might thinkzentinel is a strange type (why does it compare tochar* instead ofcharconst* when it clearly does not need mutability), but the important thing is that this is a perfectly valid range.zsentinel does modelsentinel_for<char*>, andzstring does modelcontiguous_range. As such, it is important that the facilities in this paperwork; we need to be able to provide a constant iterator here such that we end up with a constant range.
But here, we would end up in a situation where, given azstring z:
cbegin(z) would return acharconst*cend(z) would return azsentinel (zsentinel is not an iterator, so we do not wrap it)But whilezsentinel modelssentinel_for<char*> and I can ensure in the implementation ofconst_iterator<T> thatzsentinel also modelssentinel_for<const_iterator<char*>>, it is not the case thatzsentinel modelssentinel_for<charconst*>. So we would not end up with a range at all!
It is possible to work around this problem by adding more complexity.
We could add complexity tomake_const_sentinel, passing through the underlying iterator to be able to wrapzsentinel such that it performs aconst_cast internally. That is,cend(z) would return a type like:
struct const_zsentinel{ zsentinel z;autooperator==(charconst* p)const->bool{return z==const_cast<char*>(p);}};Such aconst_cast would be safe since we know we must have originated from achar* to begin with, if thecharconst* we’re comparing originated from thezstring. But there are othercharconst*s that don’t come from azstring, that may not necessarily be safe to compare against, and now we’re supporting those. This also doesn’t generalize to any other kind ofmake_const_iterator customization: this implementation is specific toconst_cast, which is only valid for pointers. We would have to add something like aconst_iterator_cast.
Let’s instead consider adding complexity in the other direction, tomake_const_iterator. We can pass through the underlying sentinel such that we only turnchar* intocharconst* ifS satisfiessentinel_for<charconst*>, and otherwise turnchar* intoconst_iterator<char*>. We could make this more specific and say that we turnchar*intocharconst* only ifboth theiterator andsentinel types arechar*, but I don’t think the generalization thatsentinel_for<charconst*> is sufficient.
This direction can be made to work and I think, overall, has fewer problems with the other direction. That is, something like this:
template<typename S, std::input_iterator It>requires std::sentinel_for<S, I>constexprauto make_const_iterator(It it){ifconstexpr(ConstantIterator<It>){return it;}elseifconstexpr(std::is_pointer_v<It>and std::sentinel_for<S, std::remove_pointer_t<It>const*>){returnstatic_cast<std::remove_pointer_t<It>const*>(it);}else{return basic_const_iterator<It>(it);}}But is it worth it?
A benefit might be that for a simple implementation ofspan, we could have this:
template<typename T>struct simple_span{auto begin()const-> T*;auto end()const-> T*;auto cbegin()const-> Tconst*;auto cend()const-> Tconst*;};Which out of the box satisfies thatsimple_span<int>::cbegin() andcbegin(simple_span<int>) both give you anintconst*. Whereas, with the design presented up until now, membercbegin andcend would have to returnconst_iterator<T*> instead.
I’m not sure it’s worth it. The simpler design is easier to understand. There’s something nice aboutmake_const_iterator being able to act on an iterator in a vacuum, and being able to definecbegin simply as:
As opposed to:
auto& cr= possibly_const(r);make_const_iterator<std::ranges::sentinel_t<decltype(cr)>>(std::ranges::begin(cr))and having to extend the alias template to:
template<std::input_iterator I, std::sentinel_for<I> S= I>using const_iterator=decltype(make_const_iterator<S>(std::declval<I>()));which still allowsstd::const_iterator<intconst*> to beintconst*. But I’m not sure it’s worth it forjust the pointer case forjust being able to definesimple_span. In this model,simple_span wouldn’t even need to define membercbegin/cend, so why would it do so, and provide something different?
I could be convinced otherwise though.
span<T>?There has been desire expressed to provide membercbegin forspan<T>. This paper allows a meaningful path to get there:
template<typename T>struct span{using iterator=implementation-defined;using const_iterator= std::const_iterator<iterator>;using reverse_iterator= std::reverse_iterator<iterator>;using const_reverse_iterator= std::const_iterator<reverse_iterator>;auto begin()constnoexcept-> iterator;auto cbegin()constnoexcept-> const_iterator;auto rbegin()constnoexcept-> reverse_iterator;auto crbegin()constnoexcept-> const_reverse_iterator;// similar for end};The above design ensures that given aspan<int> s (or really, for anyT that is non-const):
std::ranges::begin(s) ands.begin() have the same typestd::ranges::cbegin(s) ands.cbegin() have the same typestd::ranges::rbegin(s) ands.rbegin() have the same typestd::ranges::crbegin(s) ands.crbegin() have the same typeAll without having to add new customization points to the standard library. Notably,span<int>::const_iterator andspan<intconst>::iterator will not be the same type (which arguably a good thing). Butspan<intconst>::iterator andspan<intconst>::const_iterator will be the same type (which is important).
The one tricky part here isconst_reverse_iterator. For all the containers currently in the standard library, we defineconst_reverse_iterator asstd::reverse_iterator<const_iterator>, but if we did that here, we’d end up with a mismatch in (4):crbegin(s) would return aconst_iterator<reverse_iterator<iterator>> whiles.crbegin() would return areverse_iterator<const_iterator<iterator>>. We end up applying the layers in a different order.
But this is fine. None of these member functions are even strictly necessary, and there isn’t any need for future shallow-const views to provide them. If we want to duplicate already-existing functionality, we should just make sure that we duplicate it consistently, rather than inconsistently.
The status quo is that we have an algorithm namedcbegin whose job is to provide a constant iterator, but it does not always do that, and sometimes it doesn’t even provide a mutable iterator. This is an unfortunate situation.
We can resolve this by extendingstd::ranges::cbegin andstd::ranges::cend to conditionally wrap their provided range’siterator/sentinel pairs to ensure that the result is a constant iterator, and use these tools to build up aviews::as_const range adapter. This completely solves the problem without any imposed boilerplate per range.
However,std::cbegin andstd::cend are harder to extend. If we changed them at all, we would probably punt on handling C++17 input iterators and non-const-iterable ranges. This means thatstd::cbegin andstd::ranges::cbegin do different things, butstd::rbegin andstd::ranges::rbeginalready do different things.std::ranges::rbegin is already a superiorstd::rbegin, so havingstd::ranges::cbegin be a superiorstd::cbegin only follows from that. In other words,std::cbegin is constrained to not deviate too much from its current behavior, whereasstd::ranges::cbegin is new and Can Do Better.
Would it be worth making such a change tostd::cbegin? I don’t think so, and this paper doesn’t propose any.
This paper also proposes adding memberc{,r}{begin,end} tospan and memberc{begin,end} (but notcr{begin,end}) toranges::view_interface so that all the range adaptors the standard library (and user-defined range adaptors) pick those up as well.
Assuming we want to add membercmeow to span, the following would ensure thatranges::cmeow(s) ands.cmeow() all return the same thing. This effectively reverts[LWG3320].[ Drafting note: While typically in the standard library,const_reverse_iterator is an alias forstd::reverse_iterator<const_iterator>, here it is flipped, becauseranges::rbegin will attempt to call memberrbegin butranges::cbegin will not. So this ensures that the algorithms and members reutrn the same thing ]
Add to the synopsis in24.7.3.1[span.overview]:
namespace std { template<class ElementType, size_t Extent = dynamic_extent> class span { public: // constants and types using element_type = ElementType; using value_type = remove_cv_t<ElementType>; using size_type = size_t; using difference_type = ptrdiff_t; using pointer = element_type*; using const_pointer = const element_type*; using reference = element_type&; using const_reference = const element_type&; using iterator =implementation-defined; // see [span.iterators]+ using const_iterator = std::const_iterator<iterator>; using reverse_iterator = std::reverse_iterator<iterator>;+ using const_reverse_iterator = std::const_iterator<reverse_iterator>; static constexpr size_type extent = Extent; // ... // [span.iterators], iterator support constexpr iterator begin() const noexcept; constexpr iterator end() const noexcept;+ constexpr const_iterator cbegin() const noexcept { return begin(); }+ constexpr const_iterator cend() const noexcept { return end(); } constexpr reverse_iterator rbegin() const noexcept; constexpr reverse_iterator rend() const noexcept;+ constexpr const_reverse_iterator crbegin() const noexcept { return rbegin(); }+ constexpr const_reverse_iterator crend() const noexcept { return rend(); } private: pointer data_; // exposition only size_type size_; // exposition only }; // ...}
Add to25.2[iterator.synopsis]:
#include <compare> // see [compare.syn]#include <concepts> // see [concepts.syn]namespace std { // ... // [insert.iterators], insert iterators template<class Container> class back_insert_iterator; template<class Container> constexpr back_insert_iterator<Container> back_inserter(Container& x); template<class Container> class front_insert_iterator; template<class Container> constexpr front_insert_iterator<Container> front_inserter(Container& x); template<class Container> class insert_iterator; template<class Container> constexpr insert_iterator<Container> inserter(Container& x, ranges::iterator_t<Container> i);+ // [const.iterators], constant iterators and sentinels+ template <indirectly_readable I>+ using iter_const_reference_t =see below;+ template<class Iterator>+ conceptconstant-iterator =see below; // exposition only++ template<class Iterator>+ class basic_const_iterator;++ template<class T, common_with<T> U>+ struct common_type<basic_const_iterator<T>, U> {+ using type = basic_const_iterator<common_type_t<T, U>>;+ };+ template<class T, common_with<T> U>+ struct common_type<U, basic_const_iterator<T>> {+ using type = basic_const_iterator<common_type_t<T, U>>;+ };+ template<class T, common_with<T> U>+ struct common_type<basic_const_iterator<T>, basic_const_iterator<U>> {+ using type = basic_const_iterator<common_type_t<T, U>>;+ };++ template<input_iterator I>+ using const_iterator =see below;++ template<class S>+ using const_sentinel =see below;++ template<input_iterator I>+ constexpr const_iterator<I> make_const_iterator(I it) { return it; }++ template<class S>+ constexpr const_sentinel<S> make_const_sentinel(S s) { return s; } // [move.iterators], move iterators and sentinels template<class Iterator> class move_iterator; // ...}
Add definitions for all this stuff in the new clause [const.iterators], ahead of [move.iterators]:
1 Class template
basic_const_iteratoris an iterator adaptor with the same behavior as the underlying iterator except that its indirection operator implicitly converts the value returned by the underlying iterator’s indirection operator to a type such that the adapted iterator is a constant iterator ([iterator.requirements]). Some generic algorithms can be called with constant iterators to avoid mutation.2 Specializations of
basic_const_iteratorare constant iterators.template<indirectly_readable It>using iter_const_reference_t= common_reference_t<const iter_value_t<It>&&, iter_reference_t<It>>;template<class It>conceptconstant-iterator= input_iterator<It>&& same_as<iter_const_reference_t<It>, iter_reference_t<It>>;template<input_iterator I>using const_iterator=see below;3Result: If
Imodelsconstant-iterator,I. Otherwise,basic_const_iterator<I>.4Result: If
Smodelsinput_iterator,const_iterator<S>. Otherwise,S.template<class I>conceptnot-a-const-iterator=see below;template<input_iterator Iterator>class basic_const_iterator{ Iteratorcurrent_= Iterator();usingreference= iter_const_reference_t<Iterator>;// exposition onlypublic:using iterator_concept=see below;using iterator_category=see below;// not always presentusing value_type= iter_value_t<Iterator>;using difference_type= iter_difference_t<Iterator>; basic_const_iterator()requires default_initializable<Iterator>=default;constexpr basic_const_iterator(Iterator current);template<convertible_to<Iterator> U>constexpr basic_const_iterator(basic_const_iterator<U> current);template<different-from<basic_const_iterator> T>requires convertible_to<T, Iterator>constexpr basic_const_iterator(T&& current);constexprconst Iterator& base()const&noexcept;constexpr Iterator base()&&;constexprreferenceoperator*()const;constexprconst value_type*operator->()constrequires is_lvalue_reference_v<iter_reference_t<Iterator>>&& same_as<remove_cvref_t<iter_reference_t<Iterator>>, value_type>;constexpr basic_const_iterator&operator++();constexprvoidoperator++(int);constexpr basic_const_iteratoroperator++(int)requires forward_iterator<Iterator>;constexpr basic_const_iterator&operator--()requires bidirectional_iterator<Iterator>;constexpr basic_const_iteratoroperator--(int)requires bidirectional_iterator<Iterator>;constexpr basic_const_iterator&operator+=(difference_type n)requires random_access_iterator<Iterator>;constexpr basic_const_iterator&operator-=(difference_type n)requires random_access_iterator<Iterator>;constexprreferenceoperator[](difference_type n)constrequires random_access_iterator<Iterator>;template<sentinel_for<Iterator> S>friendconstexprbooloperator==(const basic_const_iterator& x,const S& s);friendconstexprbooloperator<(const basic_const_iterator& x,const basic_const_iterator& y)requires random_access_iterator<Iterator>;friendconstexprbooloperator>(const basic_const_iterator& x,const basic_const_iterator& y)requires random_access_iterator<Iterator>;friendconstexprbooloperator<=(const basic_const_iterator& x,const basic_const_iterator& y)requires random_access_iterator<Iterator>;friendconstexprbooloperator>=(const basic_const_iterator& x,const basic_const_iterator& y)requires random_access_iterator<Iterator>;friendconstexprautooperator<=>(const basic_const_iterator& x,const basic_const_iterator& y)requires random_access_iterator<Iterator>&& three_way_comparable<Iterator>;template<different-from<basic_const_iterator> I>friendconstexprbooloperator<(const basic_const_iterator& x,const I& y)requires random_access_iterator<Iterator>&& totally_ordered_with<Iterator, I>;template<different-from<basic_const_iterator> I>friendconstexprbooloperator>(const basic_const_iterator& x,const I& y)requires random_access_iterator<Iterator>&& totally_ordered_with<Iterator, I>;template<different-from<basic_const_iterator> I>friendconstexprbooloperator<=(const basic_const_iterator& x,const I& y)requires random_access_iterator<Iterator>&& totally_ordered_with<Iterator, I>;template<different-from<basic_const_iterator> I>friendconstexprbooloperator>=(const basic_const_iterator& x,const I& y)requires random_access_iterator<Iterator>&& totally_ordered_with<Iterator, I>;template<not-a-const-iterator I>friendconstexprbooloperator<(const I& x,const basic_const_iterator& y)requires random_access_iterator<Iterator>&& totally_ordered_with<Iterator, I>;template<not-a-const-iterator I>friendconstexprbooloperator>(const I& x,const basic_const_iterator& y)requires random_access_iterator<Iterator>&& totally_ordered_with<Iterator, I>;template<not-a-const-iterator I>friendconstexprbooloperator<=(const I& x,const basic_const_iterator& y)requires random_access_iterator<Iterator>&& totally_ordered_with<Iterator, I>;template<not-a-const-iterator I>friendconstexprbooloperator>=(const I& x,const basic_const_iterator& y)requires random_access_iterator<Iterator>&& totally_ordered_with<Iterator, I>;template<different-from<basic_const_iterator> I>friendconstexprautooperator<=>(const basic_const_iterator& x,const I& y)requires random_access_iterator<Iterator>&& totally_ordered_with<Iterator, I>&& three_way_comparable_with<Iterator, I>;friendconstexpr basic_const_iteratoroperator+(const basic_const_iterator& i, difference_type n)requires random_access_iterator<Iterator>;friendconstexpr basic_const_iteratoroperator+(difference_type n,const basic_const_iterator& i)requires random_access_iterator<Iterator>;friendconstexpr basic_const_iteratoroperator-(const basic_const_iterator& i, difference_type n)requires random_access_iterator<Iterator>;template<sized_sentinel_for<Iterator> S>friendconstexpr difference_typeoperator-(const basic_const_iterator& x,const S& y);template<sized_sentinel_for<Iterator> S>requiresdifferent-from<S, basic_const_iterator>friendconstexpr difference_typeoperator-(const S& x,const basic_const_iterator& y);};5 Given some type
I, the conceptnot-a-const-iteratoris defined asfalseifIis a specialization ofbasic_const_iteratorandtrueotherwise.6
basic_const_iterator<Iterator>::iterator_conceptis defined as follows:
- (6.1) If
Iteratormodelscontiguous_iterator, theniterator_conceptdenotescontiguous_iterator_tag.- (6.2) Otherwise, if
Iteratormodelsrandom_access_iterator, theniterator_conceptdenotesrandom_access_iterator_tag.- (6.3) Otherwise, if
Iteratormodelsbidirectional_iterator, theniterator_conceptdenotesbidirectional_iterator_tag.- (6.4) Otherwise, if
Iteratormodelsforward_iterator, theniterator_conceptdenotesforward_iterator_tag.- (6.5) Otherwise,
iterator_conceptdenotesinput_iterator_tag.7 The membertypedef-name
iterator_categoryis defined if and only ifIteratormodelsforward_iterator. In that case,basic_const_iterator<Iterator>::iterator_categorydenotes the typeiterator_traits<Iterator>::iterator_category.8Effects: Initializes
current_withstd::move(current).template<convertible_to<Iterator> U>constexpr basic_const_iterator(basic_const_iterator<U> current);9Effects: Initializes
current_withstd::move(current.current_).template<different-from<basic_const_iterator> T>requires convertible_to<T, Iterator>constexpr basic_const_iterator(T&& current);10Effects: Initializes
current_withstd::forward<T>(current).11Effects: Equivalent to:
returncurrent_;12Effects: Equivalent to:
return std::move(current_);13Effects: Equivalent to:
returnstatic_cast<reference>(*current_);constexprconst value_type*operator->()constrequires is_lvalue_reference_v<iter_reference_t<Iterator>>&& same_as<remove_cvref_t<iter_reference_t<Iterator>>, value_type>;14Returns: If
Iteratormodelscontiguous_iterator,to_address(current_); otherwise,addressof(*current_).15Effects: Equivalent to:
16Effects: Equivalent to:
++current_;17Effects: Equivalent to:
18Effects: Equivalent to:
19Effects: Equivalent to:
constexpr basic_const_iterator&operator+=(difference_type n)requires random_access_iterator<Iterator>;constexpr basic_const_iterator&operator-=(difference_type n)requires random_access_iterator<Iterator>;20 Let
opbe the operator.21Effects: Equivalent to:
22Effects: Equivalent to:
returnstatic_cast<reference>(current_[n]);template<sentinel_for<Iterator> S>friendconstexprbooloperator==(const basic_const_iterator& x,const S& s);23Effects: Equivalent to:
return x.current_== s;friendconstexprbooloperator<(const basic_const_iterator& x,const basic_const_iterator& y)requires random_access_iterator<Iterator>;friendconstexprbooloperator>(const basic_const_iterator& x,const basic_const_iterator& y)requires random_access_iterator<Iterator>;friendconstexprbooloperator<=(const basic_const_iterator& x,const basic_const_iterator& y)requires random_access_iterator<Iterator>;friendconstexprbooloperator>=(const basic_const_iterator& x,const basic_const_iterator& y)requires random_access_iterator<Iterator>;friendconstexprautooperator<=>(const basic_const_iterator& x,const basic_const_iterator& y)requires random_access_iterator<Iterator>&& three_way_comparable<Iterator>;24 Let
opbe the operator.25Effects: Equivalent to:
return x.current_op y.current_;template<different-from<basic_const_iterator> I>friendconstexprbooloperator<(const basic_const_iterator& x,const I& y)requires random_access_iterator<Iterator>&& totally_ordered_with<Iterator, I>;template<different-from<basic_const_iterator> I>friendconstexprbooloperator>(const basic_const_iterator& x,const I& y)requires random_access_iterator<Iterator>&& totally_ordered_with<Iterator, I>;template<different-from<basic_const_iterator> I>friendconstexprbooloperator<=(const basic_const_iterator& x,const I& y)requires random_access_iterator<Iterator>&& totally_ordered_with<Iterator, I>;template<different-from<basic_const_iterator> I>friendconstexprbooloperator>=(const basic_const_iterator& x,const I& y)requires random_access_iterator<Iterator>&& totally_ordered_with<Iterator, I>;template<different-from<basic_const_iterator> I>friendconstexprautooperator<=>(const basic_const_iterator& x,const I& y)requires random_access_iterator<Iterator>&& totally_ordered_with<Iterator, I>&& three_way_comparable_with<Iterator, I>;26 Let
opbe the operator.27Returns: Equivalent to:
return x.current_op y;template<not-a-const-iterator I>friendconstexprbooloperator<(const I& x,const basic_const_iterator& y)requires random_access_iterator<Iterator>&& totally_ordered_with<Iterator, I>;template<not-a-const-iterator I>friendconstexprbooloperator>(const I& x,const basic_const_iterator& y)requires random_access_iterator<Iterator>&& totally_ordered_with<Iterator, I>;template<not-a-const-iterator I>friendconstexprbooloperator<=(const I& x,const basic_const_iterator& y)requires random_access_iterator<Iterator>&& totally_ordered_with<Iterator, I>;template<not-a-const-iterator I>friendconstexprbooloperator>=(const I& x,const basic_const_iterator& y)requires random_access_iterator<Iterator>&& totally_ordered_with<Iterator, I>;28 Let
opbe the operator.29Returns: Equivalent to:
return xop y.current_;friendconstexpr basic_const_iteratoroperator+(const basic_const_iterator& i, difference_type n)requires random_access_iterator<Iterator>;friendconstexpr basic_const_iteratoroperator+(difference_type n,const basic_const_iterator& i)requires random_access_iterator<Iterator>;30Effects: Equivalent to:
return basic_const_iterator(i.current_+ n);friendconstexpr basic_const_iteratoroperator-(const basic_const_iterator& i, difference_type n)requires random_access_iterator<Iterator>;31Effects: Equivalent to:
return basic_const_iterator(i.current_- n);template<sized_sentinel_for<Iterator> S>friendconstexpr difference_typeoperator-(const basic_const_iterator& x,const S& y);32Effects: Equivalent to:
return x.current_- y;template<sized_sentinel_for<Iterator> S>requiresdifferent-from<S, basic_const_iterator>friendconstexpr difference_typeoperator-(const S& x,const basic_const_iterator& y);33Effects: Equivalent to:
return x- y.current_;
[ Drafting note:constant-iterator is exposition-only whileconstant_range is not, largely on the basis that I’m aware of use-cases for the latter[coerce-const] but not the former. ]
Add to26.2[ranges.syn]:
#include <compare> // see [compare.syn]#include <initializer_list> // see [initializer.list.syn]#include <iterator> // see [iterator.synopsis]namespace std::ranges { // ... // [range.range], ranges template<class T> concept range = see below; template<class T> inline constexpr bool enable_borrowed_range = false; template<class T> concept borrowed_range = see below; template<class T> using iterator_t = decltype(ranges::begin(declval<T&>())); template<range R> using sentinel_t = decltype(ranges::end(declval<R&>()));+ template<range R>+ using const_iterator_t = const_iterator<iterator_t<R>>; template<range R> using range_difference_t = iter_difference_t<iterator_t<R>>; template<sized_range R> using range_size_t = decltype(ranges::size(declval<R&>())); template<range R> using range_value_t = iter_value_t<iterator_t<R>>; template<range R> using range_reference_t = iter_reference_t<iterator_t<R>>;+ template<range R>+ using range_const_reference_t = iter_const_reference_t<iterator_t<R>>; template<range R> using range_rvalue_reference_t = iter_rvalue_reference_t<iterator_t<R>>; // ... // [range.refinements], other range refinements template<class R, class T> concept output_range = see below; template<class T> concept input_range = see below; template<class T> concept forward_range = see below; template<class T> concept bidirectional_range = see below; template<class T> concept random_access_range = see below; template<class T> concept contiguous_range = see below; template<class T> concept common_range = see below; template<class T> concept viewable_range = see below;+ template<class T>+ concept constant_range =see below; // [view.interface], class template view_interface // ... // [range.reverse], reverse view template<view V> requires bidirectional_range<V> class reverse_view; template<class T> inline constexpr bool enable_borrowed_range<reverse_view<T>> = enable_borrowed_range<T>; namespace views { inline constexprunspecified reverse =unspecified; }+ // [range.const], constant view+ template <input_range R>+ constexpr auto&possibly-const-range(R& r) { // exposition only+ if constexpr (constant_range<const R> && !constant_range<R>) {+ return const_cast<const R&>(r);+ } else {+ return r;+ }+ }++ template<view V>+ requires input_range<V>+ class as_const_view;++ template<class T>+ inline constexpr bool enable_borrowed_range<as_const_view<T>> = enable_borrowed_range<T>;++ namespace views { inline constexprunspecified as_const =unspecified; } // ...}
Updateranges::cbegin in26.3.4[range.access.cbegin][ Drafting note: The complicated formulation (here and throughout) avoids an extra move in the case that we don’t need to do additional wrapping ]:
1 The name
ranges::cbegindenotes a customization point object ([customization.point.object]).The expressionGiven a subexpressionranges::cbegin(E)for a subexpressionEof typeTis expression-equivalent toEwith typeT, lettbe an lvalue that denotes the reified object forE. Then:2 [Note 1: Whenever
ranges::cbegin(E)is a valid expression, its type modelsinput_or_output_iteratorandconstant-iterator. —end note]
Updateranges::cend in26.3.5[range.access.cend]:
1 The name
ranges::cenddenotes a customization point object ([customization.point.object]).The expressionGiven a subexpressionranges::cend(E)for a subexpressionEof typeTis expression-equivalent toEwith typeT, lettbe an lvalue that denotes the reified object forE. Then:2 [Note 1: Whenever
ranges::cend(E)is a valid expression, the typesSandIof the expressionsranges::cend(E)andranges::cbegin(E)modelsentinel_for<S, I>.IfSmodelsinput_iterator, thenSalso modelsconstant-iterator. —end note]
Updateranges::crbegin in26.3.8[range.access.crbegin]:
1 The name
ranges::crbegindenotes a customization point object ([customization.point.object]).The expressionGiven a subexpressionranges::crbegin(E)for a subexpressionEof typeTis expression-equivalent toEwith typeT, lettbe an lvalue that denotes the reified object forE. Then:2 [Note 1: Whenever
ranges::crbegin(E)is a valid expression, its type modelsinput_or_output_iteratorandconstant-iterator. —end note]
Updateranges::crend in26.3.9[range.access.crend]:
1 The name
ranges::crenddenotes a customization point object ([customization.point.object]).The expressionGiven a subexpressionranges::crend(E)for a subexpressionEof typeTis expression-equivalent toEwith typeT, lettbe an lvalue that denotes the reified object forE. Then:2 [Note 1: Whenever
ranges::cend(E)is a valid expression, the typesSandIof the expressionsranges::crend(E)andranges::crbegin(E)modelsentinel_for<S, I>.IfSmodelsinput_iterator, thenSalso modelsconstant-iterator. —end note]
Updateranges::cdata in26.3.14[range.prim.cdata]:
1 The name
ranges::cdatadenotes a customization point object ([customization.point.object]).The expressionGiven a subexpressionranges::cdata(E)for a subexpressionEof typeTis expression-equivalent to:Ewith typeT, lettbe an lvalue that denotes the reified object forE. Then:2 [Note 1: Whenever
ranges::cdata(E)is a valid expression, it has pointer toconstant object type. — end note]
Addconstant_range in26.4.5[range.refinements]:
6 The
viewable_rangeconcept specifies the requirements of arangetype that can be converted to aviewsafely.template<class T> concept viewable_range = range<T> && ((view<remove_cvref_t<T>> && constructible_from<remove_cvref_t<T>, T>) || (!view<remove_cvref_t<T>> && (is_lvalue_reference_v<T> || (movable<remove_reference_t<T>> && !is-initializer-list<T>))));7 The
constant_rangeconcept specifies the requirements of arangetype whose elements are not modifiable.
Addcbegin andcend toview_interface in26.5.3.1[view.interface.general]:
namespace std::ranges { template<class D> requires is_class_v<D> && same_as<D, remove_cv_t<D>> class view_interface { private: constexpr D&derived() noexcept { // exposition only return static_cast<D&>(*this); } constexpr const D&derived() const noexcept { // exposition only return static_cast<const D&>(*this); } public: constexpr bool empty() requires forward_range<D> { return ranges::begin(derived()) == ranges::end(derived()); } constexpr bool empty() const requires forward_range<const D> { return ranges::begin(derived()) == ranges::end(derived()); }+ constexpr auto cbegin() {+ return ranges::cbegin(derived());+ }+ constexpr auto cbegin() const requires range<const D> {+ return ranges::cbegin(derived());+ }+ constexpr auto cend() {+ return ranges::cend(derived());+ }+ constexpr auto cend() const requires range<const D> {+ return ranges::cend(derived());+ } // ... };}
1
as_const_viewpresents aviewof an underlying sequence as constant. That is, the elements of anas_const_viewcannot be modified.2 The name
views::as_constdenotes a range adaptor object ([range.adaptor.object]). LetEbe an expression, letTbedecltype((E)), and letUberemove_cvref_t<T>. The expressionviews::as_const(E)is expression-equivalent to:
- (2.1) If
views::all_t<T>modelsconstant_range, thenviews::all(E).- (2.2) Otherwise, if
Udenotesspan<X, Extent>for some typeXand some extentExtent, thenspan<const X, Extent>(E).- (2.3) Otherwise, if
Eis an lvalue,const Umodelsconstant_range, andUdoes not modelview, thenref_view(static_cast<const U&>(E)).- (2.4) Otherwise,
as_const_view(E).3 [Example:
template<ranges::constant_range R>void cant_touch_this(R&&);vector<char> hammer={'m','c'};span<char> beat= hammer;cant_touch_this(views::as_const(beat));// will not modify the elements of hammer-end example]
as_const_view [range.as.const.view]namespace std::ranges{template<input_range V>requires view<V>class as_const_view:public view_interface<as_const_view<V>>{ Vbase_= V();// exposition onlypublic: as_const_view()requires default_initializable<V>=default;constexprexplicit as_const_view(V base);constexpr V base()const&requires copy_constructible<V>{returnbase_;}constexpr V base()&&{return std::move(base_);}constexprauto begin()requires(!simple-view<V>){return ranges::cbegin(base_);}constexprauto begin()constrequires range<const V>{return ranges::cbegin(base_);}constexprauto end()requires(!simple-view<V>){return ranges::cend(base_);}constexprauto end()constrequires range<const V>{return ranges::cend(base_);}constexprauto size()requires sized_range<V>{return ranges::size(base_);}constexprauto size()constrequires sized_range<const V>{return ranges::size(base_);}};template<class R> as_const_view(R&&)-> as_const_view<views::all_t<R>>;}1Effects: Initializes
base_withstd::move(base).
Add the following macro definition to17.3.2[version.syn], with the value selected by the editor to reflect the date of adoption of this paper:
Thanks to Tim Song for helping me work through the design and implementation details of this paper. Thanks to Peter Dimov and Tomasz Kamiński for insisting on design sanity (even as they insisted on different designs) and providing feedback. Thanks to Eric Niebler for having already solved the problem of how to come up with the right reference type for aconst_iterator<It> in range-v3.
[coerce-const] Barry Revzin. 2021. Coercing deep const-ness.
https://brevzin.github.io/c++/2021/09/10/deep-const/
[const-impl] Barry Revzin. 2020. Implementingcbegin,cend, andconst_view.
https://godbolt.org/z/rc81fn1Ej
[LWG2128] Dmitry Polukhin. Absence of global functions cbegin/cend.
https://wg21.link/lwg2128
[LWG3320] Poland. span::cbegin/cend methods produce different results than std::[ranges::]cbegin/cend.
https://wg21.link/lwg3320
[LWG3386] Tim Song. elements_view needs its own sentinel type.
https://wg21.link/lwg3386
[N1674] Walter E. Brown. 2004-08-31. A Proposal to Improve const_iterator Use from C++0X Containers.
https://wg21.link/n1674
[N1913] Walter E. Brown. 2005-10-20. A Proposal to Improve const_iterator Use (version 3).
https://wg21.link/n1913
[N4380] ADAM David Alan Martin, Alisdair Meredith. 2015-02-05. Constant View: A proposal for a std::as_const helper function template.
https://wg21.link/n4380
[P0122R7] Neil MacIntosh, Stephan T. Lavavej. 2018-03-16. span: bounds-safe views for sequences of objects.
https://wg21.link/p0122r7
[P0896R4] Eric Niebler, Casey Carter, Christopher Di Bella. 2018-11-09. The One Ranges Proposal.
https://wg21.link/p0896r4
[P1035R7] Christopher Di Bella, Casey Carter, Corentin Jabot. 2019-08-02. Input Range Adaptors.
https://wg21.link/p1035r7
[P1255R6] Steve Downey. 2020-04-05. A view of 0 or 1 elements: views::maybe.
https://wg21.link/p1255r6
[P2210R0] Barry Revzin. 2020-08-14. Superior String Splitting.
https://wg21.link/p2210r0
[P2259R0] Tim Song. 2020-11-20. Repairing input range adaptors and counted_iterator.
https://wg21.link/p2259r0
[p2278-minutes] LEWG. 2021. LEWG minutes for P2278.
https://wiki.edg.com/bin/view/Wg21telecons2021/P2278#Library-Evolution-2021-11-09
[P2278R0] Barry Revzin. 2021-01-10. cbegin should always return a constant iterator.
https://wg21.link/p2278r0
[P2278R1] Barry Revzin. 2021-09-15. cbegin should always return a constant iterator.
https://wg21.link/p2278r1
[P2278R2] Barry Revzin. 2021-11-17. cbegin should always return a constant iterator.
https://wg21.link/p2278r2
[P2278R3] Barry Revzin. 2022-04-12. cbegin should always return a constant iterator.
https://wg21.link/p2278r3
[P2321R2] Tim Song. 2021-06-11. zip.
https://wg21.link/p2321r2
[P2415R2] Barry Revzin, Tim Song. 2021-10-15. What is a view?
https://wg21.link/p2415r2
[P2446R0] Barry Revzin. 2021-09-18. views::move.
https://wg21.link/p2446r0
[P2501R0] Ville Voutilainen. 2021-12-14. Undo the rename of views::move and views::as_const.
https://wg21.link/p2501r0