forwarding-range
<T>
is too subtleDocument #: | P1870R0 |
Date: | 2019-10-06 |
Project: | Programming Language C++ LEWG |
Reply-to: |
Barry Revzin <barry.revzin@gmail.com> |
One of the concepts introduces by Ranges is forwarding-range
. The salient aspect of what makes a forwarding-range
is stated in [range.range]:
the validity of iterators obtained from the object denoted by
E
is not tied to the lifetime of that object.
clarified more in the subsequent note:
[ Note: Since the validity of iterators is not tied to the lifetime of an object whose type models
forwarding-range
, a function can accept arguments of such a type by value and return iterators obtained from it without danger of dangling. — end note ]
For example, std::vector<T>
is not a forwarding-range
because any iterator into a vector
is of course dependent on the lifetime of the vector
itself. On the other hand, std::string_view
is a forwarding-range
because it does not actually own anything - any iterator you get out of it has its lifetime tied to some other object entirely.
But while span
and subrange
each model forwarding-range
, not all views do. For instance, transform_view
would not because its iterators’ validity would be tied to the unary function that is the actual transform. You could increment those iterators, but you couldn’t dereference them. Likewise, filter_view
’s iterator validity is going to be based on its predicate.
Really, a forwarding-range
is quite a rare creature.1
Value category also plays into this. Notably, lvalue ranges all model forwarding-range
– the “object” in question in this case is an lvalue reference, and the validity of iterators into a range are never going to be tied to the lifetime of some reference to that range. For instance, std::vector<T>
is not a forwarding-range
, but std::vector<T>&
is. The only question is about rvalue ranges. If I have a function that either takes a range by forwarding reference or by value, I have to know what I can do with it.
Ranges uses this in two kinds of places:
iterator_t<R>
or dangling
based on whether or not R
satisfies forwarding-range
(because if R
did not, then such iterators would not be valid, so they are not returned). This type is called safe_iterator_t<R>
and appears over 100 times in [algorithms].forwarding-range
or they decay to a view
. The former because you may need to keep iterators into them past their lifetime, and the latter because if you can cheaply copy it than that works too. This higher-level concept is called viewable_range
, and every range adapter depends on it.That is, forwarding-range
is a very important concept. It is used practically everywhere. It also conveys a pretty subtle and very rare feature of a type: that its iterators can outlive it. Syntactically, there is no difference between a range
, a view
, and a forwarding-range
, so the question is - how does a type declare itself to have this feature?
The name forwarding-range
is problematic. There is a concept std::forward_range
which is completely unrelated. A fairly common first response is that it has something to do with forwarding iterators. But the name actually comes from the question of whether you can use a forwarding reference range
safely.
However, coming up with a good name for it is very difficult. The concept has to refer to the range, but the salient aspect really has more to do with the iterators. Words that seem relevant are detachable, untethered, unfettered, nondangling. But then applying them to the range ends up being a mouthful: range_with_detachable_iterators
. Granted, this concept isn’t directly used in too many places so maybe a long name is fine.
The naming direction this proposal takes is to use the name safe_range
, based on the existence of safe_iterator
and safe_subrange
. It still doesn’t seem like a great name though, but at least all the relevant library things are similarly named.
Also the concept is still exposition-only, despite being a fairly important concept that people may want to use in their own code. This can be worked around:
template<class R>
concept my_forwarding_range = std::range<R>
&& std::same_as<std::safe_iterator_t<R>, std::iterator_t<R>>;
But this seems like the kind of thing the standard library should provide directly.
forwarding-range
Types must opt into forwarding-range
, and this is done by having non-member begin()
and end()
overloads that must take the type by either value or rvalue reference. At first glance, it might seem like this is impossible to do in the language but Ranges accomplishes this through the clever2 use of poison-pill overload:
namespace __begin {
// poison pill
template <typename T> void begin(T&&) = delete;
template <typename R>
concept has_non_member = requires (R&& r) {
begin(std::forward<R>(r));
};
}
namespace N {
struct my_vector { /* ... */ };
auto begin(my_vector const&);
}
Does N::my_vector
satisfy the concept __begin::has_non_member
? It does not. The reason is that the poison pill candidate binds an rvalue reference to the argument while the ADL candidate binds an lvalue reference, and tiebreaker of rvalue reference to lvalue reference happens much earlier than non-template to template. The only way to have a better match than the poison pill for rvalues is to either have a function/function template that takes its argument by value or rvalue reference, or to have a function template that takes a constrained forwarding reference.
This is a pretty subtle design decision - why did we decide to use the existence of non-member overloads as the opt-in?
This design comes from [stl2.547], with the expressed intent:
Redesign begin/end CPOs to eliminate deprecated behavior and to force range types to opt in to working with rvalues, thereby giving users a way to detect that, for a particular range type, iterator validity does not depend on the range’s lifetime.
which led to [P0970R1], which describes the earlier problems with ranges::begin()
thusly:
1 For the sake of compatibility with
std::begin
and ease of migration,std::ranges::begin
accepted rvalues and treated them the same asconst
lvalues. This behavior was deprecated because it is fundamentally unsound: any iterator returned by such an overload is highly likely to dangle after the full-expression that contained the invocation ofbegin
.2 Another problem, and one that until recently seemed unrelated to the design of
begin
, was that algorithms that return iterators will wrap those iterators instd::ranges::dangling<>
if the range passed to them is an rvalue. This ignores the fact that for some range types —std::span
,std::string_view
, and P0789’ssubrange
, in particular — the iterator’s validity does not depend on the range’s lifetime at all. In the case where an rvalue of one of the above types is passed to an algorithm, returning a wrapped iterator is totally unnecessary.3 The author believed that to fix the problem with
subrange
anddangling
would require the addition of a new trait to give the authors of range types a way to say whether its iterators can safely outlive the range. That felt like a hack.
This paper was presented in Rapperswil 2018, partially jointly with [P0896R1], and as far as I can tell from the minutes, this subtlety was not discussed.
ranges::begin()
on an rvalue?The design of forwarding-range
is based on the ability to invoke ranges::begin()
on an rvalue. But what is the actual motivation of doing such a thing? Why would I want to forward a range into begin()
? Even in contexts of algorithms taking range
s by forwarding reference, we could just call begin()
on the lvalue range that we get passed in. It’s not like any iterator transformations are performed - we get the same iterator either way (and the cases in which we would not get the same iterator are errors, such types would fail the expression-equivalence requirement).
The machinery for ranges::begin()
being invocable on an rvalue is entirely driven by the desire to detect iterator validity exceeding range lifetime.
In [stl2.592], Eric Niebler points out that the current wording has the non-member begin()
and end()
for subrange
taking it by rvalue reference instead of by value, meaning that const subrange
doesn’t count as a forwarding-range
. But there is a potentially broader problem, which is that overload resolution will consider the begin()
and end()
functions for subrange
even in contexts where they would be a worse match than the poison pill (i.e. they would involve conversions), and some of those contexts could lead to hard instantiation errors. So Eric suggests that the overload should be:
Of the types in the standard library that should model forwarding-range
, three of the four should take the same treatment (only iota_view
doesn’t need to worry). That is, in order to really ensure correctness by avoiding any potential hard instantiation errors, we have to write non-member begin()
and end()
function templates that constrain their argument via same_as<R>
?
The issue goes on to further suggest that perhaps the currect overload is really:
And there is an NB comment that suggests the same-ish
<T> auto&&
spelling for some times and same_as<T> auto
spelling for others. What’s the distinction? To be honest, I do not understand.
Now we’ve started from needing a non-member begin()
/end()
that take an argument by value or rvalue reference – not necessarily to actually be invoked on an rvalue – but that runs into potential problems, that need to be solved by making that non-member a constrained template that either takes by value or forwarding reference, but constrained to a single type?
At this point, we have three concepts in Ranges that have some sort of mechanism to opt-in/opt-out:
forwarding-range
: provide a non-member begin()
/end()
that take their argument by value or rvalue reference (but really probably a constrained function template)view
: opt-in via the enable_view
type traitsized_range
: opt-out via the disable_sized_range
trait (itself problematic due to the double negative, see [P1871R0])I don’t think we need different mechanisms for each trait. I know Eric and Casey viewed having to have a type trait as a hack, but it’s a hack around not having a language mechanism to express opt-in (see also [P1900R0]). It’s still the best hack we have, that’s the easiest to understand, that’s probably more compiler-efficient as well (overload resolution is expensive!)
Now that MSVC’s standard library implementation is open source, we can take a look at how they went about implementing the opt-in for forwarding-range
in their implementation of basic_string_view
[msvc.basic_string_view]:
#ifdef __cpp_lib_concepts
_NODISCARD friend constexpr const_iterator begin(const basic_string_view& _Right) noexcept {
// non-member overload that accepts rvalues to model the exposition-only forwarding-range concept
return _Right.begin();
}
_NODISCARD friend constexpr const_iterator end(const basic_string_view& _Right) noexcept {
// Ditto modeling forwarding-range
return _Right.end();
}
#endif // __cpp_lib_concepts
Note that these overloads take their arguments by reference-to-const
. But the non-member overloads need to take their arguments by either value or rvalue reference, otherwise the poison pill is a better match, as described earlier. So at this moment, std::string_view
fails to satisfy forwarding-range
. If even Casey can make this mistake, how is anybody going to get it right?
The naming direction this proposal takes is to use the name safe_range
, based on the existence of safe_iterator
and safe_subrange
. If a alternate name is preferred, the wording can simply be block replaced following the naming convention proposed in [P1871R0]. The proposal has four parts:
enable_safe_range
, with default value false.
forwarding-range
to safe_range
, make it non-exposition only, and have its definition be based on the type trait. Replace all uses of forwarding-range
with safe_range
as appropriate.ranges::begin()
and ranges::end()
, and their const and reverse cousins, only allow lvalues. Some of the other CPOs that invoke begin()
and end()
have to be adjusted to ensure that they only propagate lvalues too.forwarding-range
by providing non-member begin
and end
instead specialize enable_safe_range
, and remove those overloads non-member overloads.Change 21.4.2 [string.view.template] to remove the non-member begin
/end
overloads that were the old opt-in and opt-in to safe_range
[ Editor's note: Can’t just provide a specialization for enable_safe_range
since we cannot forward-declare it ]:
1 In every specialization
basic_string_view<charT, traits>
, the typetraits
shall meet the character traits requirements ([char.traits]). [ Note: The program is ill-formed iftraits::char_type
is not the same type ascharT
. — end note ]1* All specializations of
basic_string_view
modelsafe_range
([range.range])
Change 22.7.3.1 [span.overview] to remove the non-member begin
/end
overloads that were the old opt-in and add the new specialization to opt-in [ Editor's note: Can’t just provide a specialization for enable_safe_range
since we cannot forward-declare it ]:
1 A
span
is a view over a contiguous sequence of objects, the storage of which is owned by some other object.2 All member functions of
span
have constant time complexity.3 All specializations of
span
modelsafe_range
([range.range])namespace std { template<class ElementType, size_t Extent = dynamic_extent> class span { [...] - friend constexpr iterator begin(span s) noexcept { return s.begin(); } - friend constexpr iterator end(span s) noexcept { return s.end(); } private: pointer data_; // exposition only index_type size_; // exposition only }; template<class Container> span(const Container&) -> span<const typename Container::value_type>; }
Change 24.2 [ranges.syn] to introduce the new trait and the new non-exposition-only concept:
#include <initializer_list> #include <iterator> namespace std::ranges { [ ... ] // [range.range], ranges template<class T> concept range = see below; + template <std::range T> + inline constexpr bool enable_safe_range = false; + + template<class T> + concept safe_range = see below; [ ... ] // [range.dangling], dangling iterator handling struct dangling; template<range R> - using safe_iterator_t = conditional_t<forwarding-range<R>, iterator_t<R>, dangling>; + using safe_iterator_t = conditional_t<safe_range<R>, iterator_t<R>, dangling>; template<range R> using safe_subrange_t = - conditional_t<forwarding-range<R>, subrange<iterator_t<R>>, dangling>; + conditional_t<safe_range<R>, subrange<iterator_t<R>>, dangling>; [...] }
Change the definitions of ranges::begin()
, ranges::end()
, and their c
and r
cousins, to only allow lvalues, and then be indifferent to member vs non-member (see also [stl2.429]). That is, the poison pill no longer needs to force an overload taking a value or rvalue reference, it now only needs to force ADL - see also [LWG3247]). Some of these changes aren’t strictly necessary (e.g. the changes to ranges::cbegin
explicitly call out being ill-formed for rvalues, but this could’ve been inherited from ranges::begin
), they are made simply to make the specification easier to read.
Change. 24.3.1 [range.access.begin]:
1 The name
ranges::begin
denotes a customization point object. The expressionranges::begin(E)
for some subexpressionE
is expression-equivalent to:
(1.0) If
E
is an rvalue,ranges::begin(E)
is ill-formed.(1.1) Otherwise,
E + 0
ifE
isan lvalueof array type ([basic.compound]).(1.2) Otherwise,
ifE
is an lvalue,decay-copy
(E.begin())
if it is a valid expression and its typeI
modelsinput_or_output_iterator
.(1.3) Otherwise,
decay-copy
(begin(E))
if it is a valid expression and its typeI
modelsinput_or_output_iterator
with overload resolution performed in a context that includes the declarations:template<class T> void begin(T&&) = delete; template<class T> void begin(initializer_list<T>&&) = delete;
and does not include a declaration of
ranges::begin
.(1.4) Otherwise,
ranges::begin(E)
is ill-formed. [ Note: This case can result in substitution failure whenranges::begin(E)
appears in the immediate context of a template instantiation. — end note ]2 [ Note: Whenever
ranges::begin(E)
is a valid expression, its type modelsinput_or_output_iterator
. — end note ]
Change 24.3.2 [range.access.end] similarly:
1 The name
ranges::end
denotes a customization point object. The expressionranges::end(E)
for some subexpressionE
is expression-equivalent to:
(1.0) If
E
is an rvalue,ranges::end(E)
is ill-formed.(1.1) Otherwise,
E + extent_v<T>
if E isan lvalueof array type ([basic.compound])T
.(1.2) Otherwise,
if E is an lvalue,decay-copy
(E.end())
if it is a valid expression and its typeS
modelssentinel_for<decltype(ranges::begin(E))>
.(1.3) Otherwise,
decay-copy
(end(E))
if it is a valid expression and its typeS
modelssentinel_for<decltype(ranges::begin(E))>
with overload resolution performed in a context that includes the declarations:template<class T> void end(T&&) = delete; template<class T> void end(initializer_list<T>&&) = delete;
and does not include a declaration of
ranges::end
.(1.4) Otherwise,
ranges::end(E)
is ill-formed. [ Note: This case can result in substitution failure whenranges::end(E)
appears in the immediate context of a template instantiation. — end note ]2 [ Note: Whenever
ranges::end(E)
is a valid expression, the typesS
andI
ofranges::end(E)
andranges::begin(E)
modelsentinel_for<S, I>
. — end note ]
Change 24.3.3 [ranges.access.cbegin]:
1 The name
ranges::cbegin
denotes a customization point object. The expressionranges::cbegin(E)
for some subexpressionE
of typeT
is expression-equivalent to:
- (1.1)
ranges::begin(static_cast<const T&>(E))
ifE
is an lvalue.- (1.2) Otherwise,
ranges::begin(static_cast<const T&&>(E))
ranges::cbegin(E)
is ill-formed.2 [ Note: Whenever
ranges::cbegin(E)
is a valid expression, its type modelsinput_or_output_iterator
. - end note ]
Change 24.3.4 [ranges.access.cend]:
1 The name
ranges::cend
denotes a customization point object. The expressionranges::cend(E)
for some subexpressionE
of typeT
is expression-equivalent to:
- (1.1)
ranges::end(static_cast<const T&>(E))
ifE
is an lvalue.- (1.2) Otherwise,
ranges::end(static_cast<const T&&>(E))
ranges::cend(E)
is ill-formed.2 [ Note: Whenever
ranges::cend(E)
is a valid expression, the typesS
andI
ofranges::cend(E)
andranges::cbegin(E)
modelsentinel_for<S, I>
. - end note ]
Change 24.3.5 [ranges.access.rbegin]:
1 The name
ranges::rbegin
denotes a customization point object. The expressionranges::rbegin(E)
for some subexpressionE
is expression-equivalent to:
(1.0) If
E
is an rvalue,ranges::rbegin(E)
is ill-formed.`(1.1) Otherwise
If,E
is an lvaluedecay-copy
(E.rbegin())
if it is a valid expression and its typeI
modelsinput_or_output_iterator
.(1.2) Otherwise,
decay-copy
(rbegin(E))
if it is a valid expression and its typeI
modelsinput_or_output_iterator
with overload resolution performed in a context that includes the declaration:and does not include a declaration of
ranges::rbegin
.(1.3) Otherwise,
make_reverse_iterator(ranges::end(E))
ifboth ranges::begin(E)
andranges::end(E)
are valid expressions of the same typeI
which modelsbidirectional_iterator
([iterator.concept.bidir]).(1.4) Otherwise,
ranges::rbegin(E)
is ill-formed. [ Note: This case can result in substitution failure whenranges::rbegin(E)
appears in the immediate context of a template instantiation. — end note ]2 [ Note: Whenever
ranges::rbegin(E)
is a valid expression, its type modelsinput_or_output_iterator
. — end note ]
Change 24.3.6 [range.access.rend]:
1 The name
ranges::rend
denotes a customization point object. The expressionranges::rend(E)
for some subexpressionE
is expression-equivalent to:
(1.0) If
E
is an rvalue,ranges::rend(E)
is ill-formed.`(1.1) Otherwise
If,E
is an lvaluedecay-copy
(E.rend())
if it is a valid expression and its typeS
modelssentinel_for<decltype(ranges::rbegin(E))>
.(1.2) Otherwise,
decay-copy
(rend(E))
if it is a valid expression and its typeS
modelssentinel_for<decltype(ranges::rbegin(E))>
. with overload resolution performed in a context that includes the declaration:and does not include a declaration of
ranges::rend
.(1.3) Otherwise,
make_reverse_iterator(ranges::begin(E))
if bothranges::begin(E)
andranges::end(E)
are valid expressions of the same typeI
which modelsbidirectional_iterator
([iterator.concept.bidir]).(1.4) Otherwise,
ranges::rend(E)
is ill-formed. [ Note: This case can result in substitution failure whenranges::rend(E)
appears in the immediate context of a template instantiation. — end note ]2 [ Note: Whenever
ranges::rend(E)
is a valid expression, the typesS
andI
ofranges::rend(E)
andranges::rbegin(E)
modelsentinel_for<S, I>
. — end note ]
Change 24.3.7 [ranges.access.crbegin]:
1 The name
ranges::crbegin
denotes a customization point object. The expressionranges::crbegin(E)
for some subexpressionE
of typeT
is expression-equivalent to:
- (1.1)
ranges::rbegin(static_cast<const T&>(E))
ifE
is an lvalue.- (1.2) Otherwise,
ranges::rbegin(static_cast<const T&&>(E))
ranges::crbegin(E)
is ill-formed.2 [ Note: Whenever
ranges::crbegin(E)
is a valid expression, its type modelsinput_or_output_iterator
. - end note ]
Change 24.3.8 [ranges.access.crend]:
1 The name
ranges::crend
denotes a customization point object. The expressionranges::crend(E)
for some subexpressionE
of typeT
is expression-equivalent to:
- (1.1)
ranges::rend(static_cast<const T&>(E))
ifE
is an lvalue.- (1.2) Otherwise,
ranges::rend(static_cast<const T&&>(E))
ranges::crend(E)
is ill-formed.2 [ Note: Whenever
ranges::crend(E)
is a valid expression, the typesS
andI
ofranges::crend(E)
andranges::crbegin(E)
modelsentinel_for<S, I>
. - end note ]
For ranges::size
, ranges::empty
, and ranges::data
, we want to allow them to be invoked on rvalues that satisfy safe_range
. The specification this needs to ensure that ranges::begin
and ranges::end
are still only called on lvalues. ranges::cdata
requires no changes.
Change 24.3.9 [range.prim.size]:
1 The name
size
denotes a customization point object.The expressionGiven a subexpressionranges::size(E)
for some subexpressionE
with typeT
E
with typeT
and an lvaluet
that denotes the same object asE
, the expressionranges::size(E)
is expression-equivalent to:
(1.1)
decay-copy
(extent_v<T>)
ifT
is an array type ([basic.compound]).(1.2) Otherwise, if
disable_sized_range<remove_cv_t<T>>
([range.sized]) isfalse
:
(1.2.1)
decay-copy
(E.size())
if it is a valid expression and its typeI
is integer-like ([iterator.concept.winc]).(1.2.2) Otherwise,
decay-copy
(size(E))
if it is a valid expression and its typeI
is integer-like with overload resolution performed in a context that includes the declaration:and does not include a declaration of
ranges::size
.(1.3) Otherwise, if
decltype((E))
modelssafe_range
([range.range]),make-unsigned-like(ranges::end(
([range.subrange]) if it is a valid expression and the typesEt) - ranges::begin(Et))I
andS
ofranges::begin(
andEt)ranges::end(
(respectively) model bothEt)sized_sentinel_for<S, I>
([iterator.concept.sizedsentinel]) andforward_iterator<I>
.However,E
is evaluated only once.(1.4) Otherwise,
ranges::size(E)
is ill-formed. [ Note: This case can result in substitution failure whenranges::size(E)
appears in the immediate context of a template instantiation. — end note ]2 [ Note: Whenever
ranges::size(E)
is a valid expression, its type is integer-like. - end note ]
Change 24.3.10 [range.prim.empty]:
1 The name
empty
denotes a customization point object.The expressionGiven a subexpressionranges::empty(E)
for some subexpressionE
E
and an lvaluet
that denotes the same object asE
, the expression `ranges::empty(E)
is expression-equivalent to:
- (1.1)
bool((E).empty())
if it is a valid expression.- (1.2) Otherwise,
(ranges::size(E) == 0)
if it is a valid expression.- (1.3) Otherwise, if
decltype((E))
modelssafe_range
([range.range]),EQ
, whereEQ
isbool(ranges::begin(
Et) == ranges::end(Et))except thatif it is a valid expression and the type ofE
is only evaluated once, ifEQ
ranges::begin(
modelsEt)forward_iterator
.- (1.4) Otherwise,
ranges::empty(E)
is ill-formed. [ Note: This case can result in substitution failure whenranges::empty(E)
appears in the immediate context of a template instantiation. — end note ]2 [ Note: Whenever
ranges::empty(E)
is a valid expression, it has typebool
. - end note ]
Change 24.3.11 [range.prim.data]: [ Editor's note: In this wording, in order for decltype((E))
to model safe_range
, it must necessarily follow that ranges::begin(t)
is valid so we don’t need extra wording to check it again. ]
1 The name
data
denotes a customization point object.The expressionGiven a subexpressionranges::data(E)
for some subexpressionE
E
and an lvaluet
that denotes the same object asE
, the expressionranges::data(E)
is expression-equivalent to:
- (1.1) If
E
is an lvalue,decay-copy
(E.data())
if it is a valid expression of pointer to object type.- (1.2) Otherwise, if
decltype((E))
modelssafe_range
([range.range]) and the type ofifranges::begin(
Et)is a valid expression whose typemodelscontiguous_iterator
,to_address(ranges::begin(
.Et))- (1.3) Otherwise,
ranges::data(E)
is ill-formed. [ Note: This case can result in substitution failure whenranges::data(E)
appears in the immediate context of a template instantiation. — end note ]2 [ Note: Whenever
ranges::data(E)
is a valid expression, it has pointer to object type. — end note ]
Change 24.4.2 [range.range]:
1 The
range
concept defines the requirements of a type that allows iteration over its elements by providing an iterator and sentinel that denote the elements of the range.- template<class T> - concept range-impl = // exposition only - requires(T&& t) { - ranges::begin(std::forward<T>(t)); // sometimes equality-preserving (see below) - ranges::end(std::forward<T>(t)); - }; - - template<class T> - concept range = range-impl<T&>; - - template<class T> - concept forwarding-range = // exposition only - range<T> && range-impl<T>; + template<class T> + concept range = + requires(T& t) { + ranges::begin(t); // sometimes equality-preserving (see below) + ranges::end(t); + }; + + template<class T> + concept safe_range = + range<T> && (is_lvalue_reference_v<T> || enable_safe_range<remove_cvref_t<T>>);
2 The required expressions
ranges::begin(std::forward<T>(t))
andranges::end(std::forward<T>(t))
of therange-impl
conceptranges::begin(t)
andranges::end(t)
of therange
concept do not require implicit expression variations ([concepts.equality]).3 Given an expression
E
such thatdecltype((E))
isT
and an lvaluet
that denotes the same object asE
,T
modelsrange-impl
range
only if
- (3.1)
[ranges::begin(
denotes a range ([iterator.requirements.general]),Et), ranges::end(Et))- (3.2) both
ranges::begin(
andEt)ranges::end(
are amortized constant time and non-modifying, andEt)- (3.3) if the type of
ranges::begin(
modelsEt)forward_iterator
,ranges::begin(
is equality-preserving.Et)4 [ Note: Equality preservation of both
ranges::begin
andranges::end
enables passing a range whose iterator type modelsforward_iterator
to multiple algorithms and making multiple passes over the range by repeated calls toranges::begin
andranges::end
. Sinceranges::begin
is not required to be equality-preserving when the return type does not modelforward_iterator
, repeated calls might not return equal values or might not be well-defined;ranges::begin
should be called at most once for such a range. - end note ]5 Given an expression
E
such thatdecltype((E))
isT
and an lvaluet
that denotes the same object asE
,T
modelsforwarding-range
safe_range
only if the validity of iterators obtained from the object denoted byt
is not tied to the lifetime of that object.6 [ Note: Since the validity of iterators is not tied to the lifetime of an object whose type models
forwarding-range
safe_range
, a function can accept arguments of such a type by value and return iterators obtained from it without danger of dangling. — end note ]6* Remarks: Pursuant to [namespace.std], users may specialize
enable_safe_range
for cv-unqualified program-defined types. Such specializations shall be usable in constant expressions ([expr.const]) and have typeconst bool
.7 [ Example: Specializations of class template
subrange
modelforwarding-range
safe_range
.subrange
provides non-member rvalue overloads ofspecializesbegin
andend
with the same semantics as its member lvalue overloadsenable_safe_range
totrue
, andsubrange
’s iterators - since they are “borrowed” from some other range - do not have validity tied to the lifetime of a subrange object. — end example ]
Change 24.4.5 [range.refinements], the definition of the viewable_range
concept:
4 The
viewable_range
concept specifies the requirements of arange
type that can be converted to aview
safely.
Change 24.5.3 [range.subrange], to use safe_range
instead of forwarding-range
, to remove the non-member begin
/end
overloads that were the old opt-in, and to add a specialization for enable_safe_range
which is the new opt-in:
1 The
subrange
class template combines together an iterator and a sentinel into a single object that models theview
concept. Additionally, it models thesized_range
concept when the final template parameter issubrange_kind::sized
.namespace std::ranges { template<input_or_output_iterator I, sentinel_for<I> S = I, subrange_kind K = sized_sentinel_for<S, I> ? subrange_kind::sized : subrange_kind::unsized> requires (K == subrange_kind::sized || !sized_sentinel_for<S, I>) class subrange : public view_interface<subrange<I, S, K>> { template<not-same-as<subrange> R> - requires forwarding-range<R> && + requires safe_range<R> && convertible_to<iterator_t<R>, I> && convertible_to<sentinel_t<R>, S> constexpr subrange(R&& r) requires (!StoreSize || sized_range<R>); - template<forwarding-range R> + template<safe_range R> requires convertible_to<iterator_t<R>, I> && convertible_to<sentinel_t<R>, S> constexpr subrange(R&& r, make-unsigned-like-t(iter_difference_t<I>) n) requires (K == subrange_kind::sized) : subrange{ranges::begin(r), ranges::end(r), n} {} - friend constexpr I begin(subrange&& r) { return r.begin(); } - friend constexpr S end(subrange&& r) { return r.end(); } }; - template<forwarding-range R> + template<safe_range R> subrange(R&&) -> subrange<iterator_t<R>, sentinel_t<R>, (sized_range<R> || sized_sentinel_for<sentinel_t<R>, iterator_t<R>>) ? subrange_kind::sized : subrange_kind::unsized>; - template<forwarding-range R> + template<safe_range R> subrange(R&&, make-unsigned-like-t(range_difference_t<R>)) -> subrange<iterator_t<R>, sentinel_t<R>, subrange_kind::sized>; template<size_t N, class I, class S, subrange_kind K> requires (N < 2) constexpr auto get(const subrange<I, S, K>& r); + template<input_or_output_iterator I, sentinel_for<I> S, subrange_kind K> + inline constexpr bool enable_sized_range<subrange<I, S, K>> = true; }
Change the name of the concept in 24.5.3.1 [range.subrange.ctor]:
Change the name of the concept in 24.5.4 [range.dangling]:
1 The tag type
dangling
is used together with the template aliasessafe_iterator_t
andsafe_subrange_t
to indicate that an algorithm that typically returns an iterator into or subrange of arange
argument does not return an iterator or subrange which could potentially reference a range whose lifetime has ended for a particular rvaluerange
argument which does not modelforwarding-range
safe_range
([range.range]).2 [ Example: […]
The call to
ranges::find
at#1
returnsranges::dangling
sincef()
is an rvaluevector
; thevector
could potentially be destroyed before a returned iterator is dereferenced. However, the calls at#2
and#3
both return iterators since the lvalue vec and specializations ofsubrange
modelforwarding-range
safe_range
. — end example ]
Thanks to Eric Niebler and Casey Carter for going over this paper with me, and correcting some serious misconceptions earlier drafts had. Thanks to Tim Song and Agustín Bergé for going over the details. Thanks to Tony van Eerd for helping with naming.
[LWG3247] Casey Carter. ranges::iter_move should perform ADL-only lookup of iter_move.
https://wg21.link/lwg3247
[msvc.basic_string_view] non-member begin()
/end()
for basic_string_view
.
https://github.com/microsoft/STL/blame/92508bed6387cbdae433fc86279bc446af6f1b1a/stl/inc/xstring#L1207-L1216
[P0896R1] Eric Niebler, Casey Carter. 2018. Merging the Ranges TS.
https://wg21.link/p0896r1
[P0970R1] Eric Niebler. 2018. Better, Safer Range Access Customization Points.
https://wg21.link/p0970r1
[P1871R0] Barry Revzin. 2019. Should concepts be enabled or disabled?
https://wg21.link/p1871r0
[P1900R0] Barry Revzin. 2019. Concepts-adjacent problems.
https://wg21.link/p1900r0
[stl2.429] Casey Carter. 2018. Consider removing support for rvalue ranges from range access CPOs.
https://github.com/ericniebler/stl2/issues/429
[stl2.547] Eric Niebler. 2018. Redesign begin/end CPOs to eliminate deprecated behavior.
https://github.com/ericniebler/stl2/issues/547
[stl2.592] Eric Niebler. 2018. const subrange<I,S,[un]sized>
is not a forwarding-range
.
https://github.com/ericniebler/stl2/issues/592
There is a hypothetical kind of range where the range itself owns its data by shared_ptr
, and the iterators also share ownership of the data. In this way, the iterators’ validity isn’t tied to the range’s lifetime not because the range doesn’t own the elements (as in the span
case) but because the iterators also own the elements. I’m not sure if anybody has ever written such a thing.↩︎
I intend this as a positive, not as being derogatory.↩︎