forwarding-range<T> is too subtle

Document #: P1870R0
Date: 2019-10-06
Project: Programming Language C++
LEWG
Reply-to: Barry Revzin
<>

1 Introduction

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:

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?

2 Naming

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.

3 Opting into 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?

3.1 History

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 as const 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 of begin.

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 in std::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’s subrange, 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 and dangling 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.

3.2 Why 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 ranges 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.

3.3 Issues with overloading

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:

friend constexpr I begin(same_as<subrange> auto r) { return r.begin(); }

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:

friend constexpr I begin(same-ish<subrange> auto&& r) { return r.begin(); }

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?

3.4 How many mechanisms do we need?

At this point, we have three concepts in Ranges that have some sort of mechanism to opt-in/opt-out:

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!)

3.5 Hard to get correct

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?

4 Proposal

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:

4.1 Wording

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 ]:

template<class charT, class traits = char_traits<charT>>
class basic_string_view {

- friend constexpr const_iterator begin(basic_string_view sv) noexcept { return sv.begin(); }
- friend constexpr const_iterator end(basic_string_view sv) noexcept { return sv.end(); }

};

1 In every specialization basic_string_view<charT, traits>, the type traits shall meet the character traits requirements ([char.traits]). [ Note: The program is ill-formed if traits​::​char_type is not the same type as charT. — end note ]

1* All specializations of basic_string_view model safe_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 model safe_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 expression ranges​::​​begin(E) for some subexpression E is expression-equivalent to:

  • (1.0) If E is an rvalue, ranges::begin(E) is ill-formed.

  • (1.1) Otherwise, E + 0 if E is an lvalue of array type ([basic.compound]).

  • (1.2) Otherwise, if E is an lvalue, decay-copy(E.begin()) if it is a valid expression and its type I models input_or_output_iterator.

  • (1.3) Otherwise, decay-copy(begin(E)) if it is a valid expression and its type I models input_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 when ranges​::​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 models input_or_output_iterator. — end note ]

Change 24.3.2 [range.access.end] similarly:

1 The name ranges​::​end denotes a customization point object. The expression ranges​::​end(E) for some subexpression E 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 is an lvalue of 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 type S models sentinel_for<decltype(ranges::begin(E))>.

  • (1.3) Otherwise, decay-copy(end(E)) if it is a valid expression and its type S models sentinel_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 when ranges​::​end(E) appears in the immediate context of a template instantiation. — end note ]

2 [ Note: Whenever ranges​::​end(E) is a valid expression, the types S and I of ranges​::​end(E) and ranges​::​begin(E) model sentinel_for<S, I>. — end note ]

Change 24.3.3 [ranges.access.cbegin]:

1 The name ranges​::​cbegin denotes a customization point object. The expression ranges​::​​cbegin(E) for some subexpression E of type T is expression-equivalent to:

  • (1.1) ranges​::​begin(static_cast<const T&>(E)) if E 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 models input_or_output_iterator. - end note ]

Change 24.3.4 [ranges.access.cend]:

1 The name ranges​::​cend denotes a customization point object. The expression ranges​::​​cend(E) for some subexpression E of type T is expression-equivalent to:

  • (1.1) ranges​::​end(static_cast<const T&>(E)) if E 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 types S and I of ranges​::​cend(E) and ranges​::​cbegin(E) model sentinel_for<S, I>. - end note ]

Change 24.3.5 [ranges.access.rbegin]:

1 The name ranges​::​rbegin denotes a customization point object. The expression ranges​::​​rbegin(E) for some subexpression E is expression-equivalent to:

  • (1.0) If E is an rvalue, ranges::rbegin(E) is ill-formed.`

  • (1.1) Otherwise If E is an lvalue, decay-copy(E.rbegin()) if it is a valid expression and its type I models input_or_output_iterator.

  • (1.2) Otherwise, decay-copy(rbegin(E)) if it is a valid expression and its type I models input_or_output_iterator with overload resolution performed in a context that includes the declaration:

    template<class T> void rbegin(T&&) = delete;

    and does not include a declaration of ranges​::​rbegin.

  • (1.3) Otherwise, make_reverse_iterator(ranges​::​end(E)) if both ranges​::​begin(E) and ranges​::​end(​E) are valid expressions of the same type I which models bidirectional_iterator ([iterator.concept.bidir]).

  • (1.4) Otherwise, ranges​::​rbegin(E) is ill-formed. [ Note: This case can result in substitution failure when ranges​::​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 models input_or_output_iterator. — end note ]

Change 24.3.6 [range.access.rend]:

1 The name ranges​::​rend denotes a customization point object. The expression ranges​::​rend(E) for some subexpression E is expression-equivalent to:

  • (1.0) If E is an rvalue, ranges::rend(E) is ill-formed.`

  • (1.1) Otherwise If E is an lvalue, decay-copy(E.rend()) if it is a valid expression and its type S models sentinel_for<decltype(ranges::rbegin(E))>.

  • (1.2) Otherwise, decay-copy(rend(E)) if it is a valid expression and its type S models sentinel_for<decltype(ranges::rbegin(E))>. with overload resolution performed in a context that includes the declaration:

    template<class T> void rend(T&&) = delete;

    and does not include a declaration of ranges​::​rend.

  • (1.3) Otherwise, make_reverse_iterator(ranges​::​begin(E)) if both ranges​::​begin(E) and ranges​::​​end(E) are valid expressions of the same type I which models bidirectional_iterator ([iterator.concept.bidir]).

  • (1.4) Otherwise, ranges​::​rend(E) is ill-formed. [ Note: This case can result in substitution failure when ranges​::​rend(E) appears in the immediate context of a template instantiation. — end note ]

2 [ Note: Whenever ranges​::​rend(E) is a valid expression, the types S and I of ranges​::​rend(E) and ranges​::​rbegin(E) model sentinel_for<S, I>. — end note ]

Change 24.3.7 [ranges.access.crbegin]:

1 The name ranges​::​crbegin denotes a customization point object. The expression ranges​::​​crbegin(E) for some subexpression E of type T is expression-equivalent to:

  • (1.1) ranges​::​rbegin(static_cast<const T&>(E)) if E 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 models input_or_output_iterator. - end note ]

Change 24.3.8 [ranges.access.crend]:

1 The name ranges​::​crend denotes a customization point object. The expression ranges​::​​crend(E) for some subexpression E of type T is expression-equivalent to:

  • (1.1) ranges​::​rend(static_cast<const T&>(E)) if E 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 types S and I of ranges​::​crend(E) and ranges​::​crbegin(E) model sentinel_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 expression ranges​::​size(E) for some subexpression E with type T Given a subexpression E with type T and an lvalue t that denotes the same object as E, the expression ranges::size(E) is expression-equivalent to:

  • (1.1) decay-copy(extent_v<T>) if T is an array type ([basic.compound]).

  • (1.2) Otherwise, if disable_sized_range<remove_cv_t<T>> ([range.sized]) is false:

    • (1.2.1) decay-copy(E.size()) if it is a valid expression and its type I is integer-like ([iterator.concept.winc]).

    • (1.2.2) Otherwise, decay-copy(size(E)) if it is a valid expression and its type I is integer-like with overload resolution performed in a context that includes the declaration:

      template<class T> void size(T&&) = delete;

      and does not include a declaration of ranges​::​size.

  • (1.3) Otherwise, if decltype((E)) models safe_range ([range.range]), make-unsigned-like(ranges::end(E t) - ranges::begin(E t)) ([range.subrange]) if it is a valid expression and the types I and S of ranges​::​begin(E t) and ranges​::​end(E t) (respectively) model both sized_sentinel_for<S, I> ([iterator.concept.sizedsentinel]) and forward_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 when ranges​::​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 expression ranges​::​empty(E) for some subexpression E Given a subexpression E and an lvalue t that denotes the same object as E, 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)) models safe_range ([range.range]), EQ, where EQ is bool(ranges​::​begin(E t) == ranges​::​end(E t)) except that E is only evaluated once, if EQ if it is a valid expression and the type of ranges​::​begin(E t) models forward_iterator.
  • (1.4) Otherwise, ranges​::​empty(E) is ill-formed. [ Note: This case can result in substitution failure when ranges​::​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 type bool. - 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 expression ranges​::​data(E) for some subexpression E Given a subexpression E and an lvalue t that denotes the same object as E, the expression ranges::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)) models safe_range ([range.range]) and the type of if ranges​::​begin(E t) is a valid expression whose type models contiguous_iterator, to_address(ranges​::​begin(E t)).
  • (1.3) Otherwise, ranges​::​data(E) is ill-formed. [ Note: This case can result in substitution failure when ranges​::​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)) and ranges​::​end(std​::​forward<​T>(t)) of the range-impl concept ranges::begin(t) and ranges::end(t) of the range concept do not require implicit expression variations ([concepts.equality]).

3 Given an expression E such that decltype((E)) is T and an lvalue t that denotes the same object as E, T models range-impl range only if

  • (3.1) [ranges​::​begin(E t), ranges​::​end(E t)) denotes a range ([iterator.requirements.general]),
  • (3.2) both ranges​::​begin(E t) and ranges​::​end(E t) are amortized constant time and non-modifying, and
  • (3.3) if the type of ranges​::​begin(E t) models forward_iterator, ranges​::​begin(E t) is equality-preserving.

4 [ Note: Equality preservation of both ranges​::​begin and ranges​::​end enables passing a range whose iterator type models forward_iterator to multiple algorithms and making multiple passes over the range by repeated calls to ranges​::​begin and ranges​::​end. Since ranges​::​begin is not required to be equality-preserving when the return type does not model forward_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 that decltype((E)) is T and an lvalue t that denotes the same object as E, T models forwarding-range safe_range only if the validity of iterators obtained from the object denoted by t is not tied to the lifetime of that object.

  • (5.1) ranges​::​begin(E) and ranges​::​begin(t) are expression-equivalent,
  • (5.2) ranges​::​end(E) and ranges​::​end(t) are expression-equivalent, and
  • (5.3) the validity of iterators obtained from the object denoted by E 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 ]

+ template<class>
+   inline constexpr bool enable_safe_range = true;

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 type const bool.

7 [ Example: Specializations of class template subrange model forwarding-range safe_range. subrange provides non-member rvalue overloads of begin and end with the same semantics as its member lvalue overloads specializes enable_safe_range to true, and subrange’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 a range type that can be converted to a view safely.

  template<class T>
    concept viewable_range =
-     range<T> && (forwarding-range<T> || view<decay_t<T>>);
+     range<T> && (safe_range<T> || view<decay_t<T>>);

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 the view concept. Additionally, it models the sized_range concept when the final template parameter is subrange_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]:

  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>);

Change the name of the concept in 24.5.4 [range.dangling]:

1 The tag type dangling is used together with the template aliases safe_iterator_t and safe_subrange_t to indicate that an algorithm that typically returns an iterator into or subrange of a range argument does not return an iterator or subrange which could potentially reference a range whose lifetime has ended for a particular rvalue range argument which does not model forwarding-range safe_range ([range.range]).

2 [ Example: […]

The call to ranges​::​find at #1 returns ranges​::​dangling since f() is an rvalue vector; the vector 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 of subrange model forwarding-range safe_range. — end example ]

5 Acknowledgements

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.

6 References

[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


  1. 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.↩︎

  2. I intend this as a positive, not as being derogatory.↩︎