1. Revision History
r2: Integrating SG9 feedback:
-
Removing references to p2578, after SG9 vote against it
-
Fix design suggested
-
Add design alternatives
r1: Improving many parts, following feedback from Inbal Levi and from Reddit users - [D2406R1]
r0: initial revision - [P2406R0]
2. Problem description
2.1. Range with the exact number of items
Look at this example code [CE-FILTER]:
#include <ranges>#include <iostream>namespace rv = std :: views ; int main () { for ( auto i : rv :: iota ( 0 ) | rv :: filter ([]( auto i ) { return i < 11 ; }) | rv :: take ( 11 )) std :: cout << i << '\n' ; }
Compiler explorer gets a timeout when trying to run this simple example, instead
of printing the numbers from 0 to 10. Running the same code locally, it runs for
very long time. Tracking the roots of the issue, the problem is that
uses
when the range isn’t
and
increments the internal iterator even if the counter has reached the requested
count. In this case, the filter never returns when trying to increment it once
again (at least not until
reaches the UB case of signed overflow).
The example above is just for illustration, but we can think about cases where
it isn’t clear for the user how many items the filter is expected to return, so
limiting the output count with
becomes dangerous and results in
unexpected behavior.
It means
isn’t usable on ranges if we don’t know in advance that there is
an extra element in the range.
2.2. input_iterator
case
Even more common problem is when using input ranges, e.g.
.
In most of these cases, advancing the internal iterator when reaching the count
means eating an additional input that can’t be retrieved again later, or hanging
forever if no additional input exists and the stream isn’t closed. For example [CE-ISTREAM]:
#include <ranges>#include <iostream>#include <sstream>#include <cassert>namespace rn = std :: ranges ; namespace rv = rn :: views ; int main () { auto iss = std :: istringstream ( "0 1 2" ); for ( auto i : rn :: istream_view < int > ( iss ) | rv :: take ( 1 )) std :: cout << i << '\n' ; auto i = 0 ; iss >> i ; std :: cout << i << std :: endl ; // flush it in case the assert fails assert ( i == 1 ); // FAILS, i == 2 }
It makes it harder to use ranges for things like parsing input, if the rest of the stream is still to be used or we aren’t sure there is any additional element in the stream.
Seems like this was discussed in [range-v3-issue57], and there was no decision what is the right solution.
3. Current behavior is what the standard mandates
Under 23.5.6.5 [counted.iter.nav], the standard defines the behavior of
for
as:
Effects: Equivalent to:
It means that even when
becomes 0, the internal iterator is
incremented, thus consuming an additional item from the range, and causing the
effects mentioned above for input iterator case or when
on the internal
iterator is costly (or never returns).
4. Desired behavior
As long as
is valid (not equal to
), it
must never try to access more than
items (when
is the given count). If
the range doesn’t have
items, the behavior is kept as is, i.e. it isn’t
defined (
might hang forever or access things that shouldn’t be
accessed etc.).
5. High-level design of the proposed solution
We propose adding a new iterator type,
. This type
behaves similarly to
, with changes to its operator definition
around 0 count so it doesn’t increment the internal iterator when reaching 0
count.
Additionally, this requires adding
and
that
uses the new iterator instead of
.
See below (§ 7 Design alternatives) for more details.
6. Design points for discussion
6.1. random_access_iterator
case
To reduce the amount of changes required, we kept the current behavior for
case, so we don’t have to touch the additional
operators defined only for this category. The rational behind it is that for
case we can expect the view to either have all the
items ready or able to compute all of them efficiently, so it doesn’t suffer
from an issue similar to the one
might have.
6.2. Consructing with 0 count
Similarly to
,
must allow constructing
with 0 count. In most design alternatives, this puts the iterator in an
inconsistent internal state, as the underlying iterator is expected to be "one
step back".
Please note that
and decrementing are the only operations involving the
state of the internal iterator and still legal for
constructed with
.
The options we see:
Option 1: Require that if
,
must be decrementable, and actually
decrement it in the c-tor. Please note that this obviously works only for
. Other kind of iterators can be left as UB, or just
advice against calling
on the resulted
(which
doesn’t sound correct, blocking a basic operation like
).
This option assumes the only reason to create such an
is to
decrement it later, so we always expect the given iterator to be decrementable.
Obviously, we can’t really assume it. The next operation could be to test for
before doing anything else and do nothing in the
case. This
happens naturally in a pipeline if the passed range becomes empty, for example.
Option 2: Require that if
,
must be "the one before" the actual
iterator (leaving it to the user to decide how to handle, and if neither
nor
are ever called on it, it doesn’t matter what the user does). This
option changes behavior of existing code so it’s isn’t relevant either.
Option 3: Mark this case internally (e.g. with
or a boolean flag)
and handle specially when decrementing (
"jumps" to
after
decrementing the internal iterator). Implementation must be careful if -1 is
used, instead of a separated flag, as comparison operators have to consider this
case too. Using a flag, OTOH, will probably push for separated specialization of
the whole class, so for random-access iterators this member will not exist.
6.3. base ()
When reaching 0 count, if
still simply returns the underlying iterator,
it returns the "one before" iterator in most cases.
If we want it to return the actual end (as users expect) and make the behavior
consistent, it means we must advance the underlying iterator first. This doesn’t
allow
to be
(at least logically) and invalidates other copies
of the iterator (in case of non-forward iterator). As this option seems the most
reasonable one, we based out § 7.5 Alternatives for base() suggestions on it.
Another option is to return by value, so this increment only a copy of the
underlying operator and keeps
as
member. Besides having the
invlidation problem here (maybe worse than before, as it invalidated the current
object too, so calling
twich isn’t allowed!), this prevents using it
with move-only iterators, as mentioned in [LWG3391].
6.4. Return type of operator ++ ( int )
For non-forward iterators, today counted_iterator::operator++(int) is defined
with
and
, as such an iterator might return
a different type or not return anything at all (e.g. if it’s move only
iterator).
is
, not
. As in this change we don’t always increment the iterator, there
is no consistent type to return. As a result, for non-forward iterators, we
define
as returning
.
6.5. Why lazy_take
instead of fixing take
?
We could have change
to use
when constructed with
input (non lazy) range. Besides ABI considerations, we find it wrong if
used to return one type (
) and now will start returning a
different one,
, as this is source-breaking change.
Additionally, as demonstrated above, there are cases where the user wants using
on forward iterators too, but this is something that
only the user know and we can’t automatically detect and decide on behalf of
them. We can’t change all cases of
to use
, due to
the differences in behavior both for lazy input iterators and forward iterators
(that are not random access), as described below.
We aren’t happy with the additional burden on teachability, but we believe in
most cases users can just use
and it does The Right Thing. The only
point where users must be aware of it is when they use
method, which we
expect to be quite advance usage in general. Users who care about absolute
performance, can choose using
when they know it works correctly for their
case.
6.5.1. Improve discoverability and teachability
An option mentioned in the discussions was to improve discoverability by
renaming
and
to
and
and calling the new tools
and
.
Another variation was to call the new tools
and
as suggested here, rename the existing one to have
prefix
and add new versions of
and
that alias the
version for
case and keep aliasing the
version for
the rest of the cases.
While leaving the user the option to choose the option that fits their specific use-case, it still provides a safer default.
We believe this option is problematic as it might be source breaking (e.g.
for the second variation, or any
usage of interface that isn’t provided by the new tools in any variation), but
still wanted to mention this option for discussion.
7. Design alternatives
Following are the design alternatives we came up with, after the discussions happend over the previous revisions:
7.1. Option 1 - As closer to counted_iterator
as possible
With this option,
is almost identical to
with the following changes:
-
For
, the behavior is the same.random_access_iterator
Reason: If an iterator is random access, we expect it to know when we reach the end. (Question: maybe we actually want
here?)sized_iterator -
Incrementing
doesn’t increment the underlying iterator when reaching 0 count.lazy_counted_iterator -
Decrementing
doesn’t decrement the underlying iterator when current count is 0.lazy_counted_iterator -
Implementation must handle correctly
constructed with 0 count (e.g. marking it differently so it knows to decrement the underlying iterator despite the previous point).lazy_counted_iterator
7.2. Option 2 - Capped to forward_iterator
With this option,
provides
interface
(at max). This removes the need to handle decrementing correctly. (Implementations
might still choose to track the case of
constructed with 0
count for validating the precondition of iterator comparison.)
7.3. Option 3 - Don’t read it if created with 0 count
This option is similar to option 1, but to simplify the handling of an iterator
constructed with 0 count, disallows using such an iterator with any operation
that access the underlying iterator (no
or
) or comparing to
another iterator (effectively allowing only calling
and comparing to
sentinel). Please note that if we choose option III below, it means that after
calling
, same restrictions apply on that iterator, including diallowing
another call to
.
7.4. Option 4 - Don’t increment the underlying iterator until really required
Increment the underlying iterator only on first dereference. This makes
dereferencing a non-
operation. Additionally, copying
of non-forward iterator (that wasn’t dereferenced yet
for the current item) and then dereferencing one of them resulted with
invalidation of the other one. To improve the situation we can make
non-copyable.
7.5. Alternatives for base ()
7.5.1. Option I - Don’t provide base ()
In this option, we just don’t provide
. Returning the underlying
iterator when count is 0 can be confusing (it depends if
was created with 0 count or reached this state by incrementing it) or invoking
the same issue we try to solve here (if we increment the iterator here).
Additionally, if we increment the underlying iterator on first access to
, it can’t be
(at least not logically), it invalidates other
copies of the iterator in case of
and (even if we go with
option 2 above) adds back the complexity of tracking if the iterator was
incremented already (e.g. constructed with 0 count).
If we go with option 1 above, it means that for
we keep
providing
, like
does, as there is no reason not to.
Wording must be adjustment when other functionality is defined in terms of
calling
and implementation can access the underlying iterator directly
when needed.
7.5.2. Option II - Make lazy_counted_iterator
non-copyable
Provide
function which increments the underlying iterator if needed.
Users are warned that calling
means non-lazy behavior. Implementation
must not invoke it if not explicitly requested (same wording adjustment is
required as in the previous option). We make
non-copyable even if the underlying iterator is copyable. If it’s non-copyable,
we don’t have to worry about invalidation (at least not directly; it still
invalidates copies of the underlying iterator).
7.5.3. Option III - Provide base ()
and mention it invalidates other copies
Provide
and specify it as invalidating other copies, leaving it to the
users to use it correctly. Implementation must increment the underlying iterator
if needed (reached 0 count and this is the first time
is called).
8. Wording
Wording is for the design recommended by SG9, Option 2 + I.
8.1. Wording for lazy_counted_iterator
Under Header
synopsis [iterator.syn] add the new type:
// [iterators.counted], counted iterators template < input_or_output_iterator I > class counted_iterator ; // freestanding template < input_iterator I > requires see below struct iterator_traits < counted_iterator < I >> ; // freestanding
// [iterators.lazy.counted], lazy counted iterators template < input_or_output_iterator I > class lazy_counted_iterator ; // freestanding template < input_iterator I > requires see below struct iterator_traits < lazy_counted_iterator < I >> ; // freestanding
In Iterator adaptors [predef.iterators], after 25.5.7 Counted iterators [iterators.counted] add new section:
25.5.x Lazy counted iterators [iterators.lazy.counted]Under this section add:
8.1.1. x.1 Class template lazy_counted_iterator
[lazy.counted.iterator]
Class template
is an iterator adaptor with the same behavior
as the underlying iterator except that it keeps track of the distance to the end
of its range. It can be used together with
in calls to generic
algorithms to operate on a range of N elements starting at a given position
without needing to know the end position a priori.
[Example 1:
— end example]list < string > s ; // populate the list s with at least 10 strings vector < string > v ; // copies 10 strings into v: ranges :: copy ( lazy_counted_iterator ( s . begin (), 10 ), default_sentinel , back_inserter ( v ));
Given two values
and
of types
and
,
let
denote the underlying iterator of
and
denote the underlying iterator of
.
and
refer to elements of the same sequence if and only if there exists some integer
n such that
and
refer to the same (possibly past-the-end) element.
namespace std { template < input_or_output_iterator I > class lazy_counted_iterator { public : using iterator_type = I ; using value_type = iter_value_t < I > ; // present only // if I models indirectly_readable using difference_type = iter_difference_t < I > ; using iterator_concept = see below ; // not always present using iterator_category = see below ; // not always present constexpr lazy_counted_iterator () requires default_initializable < I > = default ; constexpr lazy_counted_iterator ( I x , iter_difference_t < I > n ); template < class I2 > requires convertible_to < const I2 & , I > constexpr lazy_counted_iterator ( const lazy_counted_iterator < I2 >& x ); template < class I2 > requires assignable_from < I & , const I2 &> constexpr lazy_counted_iterator & operator = ( const lazy_counted_iterator < I2 >& x ); constexpr iter_difference_t < I > count () const noexcept ; constexpr decltype ( auto ) operator * (); constexpr decltype ( auto ) operator * () const requires dereferenceable < const I > ; constexpr lazy_counted_iterator & operator ++ (); constexpr void operator ++ ( int ); constexpr lazy_counted_iterator operator ++ ( int ) requires forward_iterator < I > ; template < common_with < I > I2 > friend constexpr iter_difference_t < I2 > operator - ( const lazy_counted_iterator & x , const lazy_counted_iterator < I2 >& y ); friend constexpr iter_difference_t < I > operator - ( const lazy_counted_iterator & x , default_sentinel_t ); friend constexpr iter_difference_t < I > operator - ( default_sentinel_t , const lazy_counted_iterator & y ); template < common_with < I > I2 > friend constexpr bool operator == ( const lazy_counted_iterator & x , const lazy_counted_iterator < I2 >& y ); friend constexpr bool operator == ( const lazy_counted_iterator & x , default_sentinel_t ); template < common_with < I > I2 > friend constexpr strong_ordering operator <=> ( const lazy_counted_iterator & x , const lazy_counted_iterator < I2 >& y ); friend constexpr iter_rvalue_reference_t < I > iter_move ( const lazy_counted_iterator & i ) noexcept ( noexcept ( ranges :: iter_move ( i . current ))) requires input_iterator < I > ; template < indirectly_swappable < I > I2 > friend constexpr void iter_swap ( const lazy_counted_iterator & x , const lazy_counted_iterator < I2 >& y ) noexcept ( noexcept ( ranges :: iter_swap ( x . current , y . current ))); private : I current = I (); // exposition only iter_difference_t < I > length = 0 ; // exposition only }; }
The member typedef-name
is defined if and only if the
qualified-id
is valid and denotes a type. In that case,
denotes
-
if the typeforward_iterator_tag
modelsI :: iterator_concept
, andderived_from < forward_iterator_tag > -
otherwise.I :: iterator_concept
The member typedef-name
is defined if and only if the
qualified-id
is valid and denotes a type. In that case,
denotes
-
if the typeforward_iterator_tag
modelsI :: iterator_category
, andderived_from < forward_iterator_tag > -
otherwise.I :: iterator_category
8.1.2. x.2 Constructors and conversions [lazy.counted.iter.const]
Preconditions: n >= 0.
Effects: Initializes
with
and
with
.
template < class I2 > requires convertible_to < const I2 & , I > constexpr lazy_counted_iterator ( const lazy_counted_iterator < I2 >& x );
Effects: Initializes
with
and
with
.
template < class I2 > requires assignable_from < I & , const I2 &> constexpr lazy_counted_iterator & operator = ( const lazy_counted_iterator < I2 >& x );
Effects: Assigns
to
and
to
.
Returns:
.
8.1.3. x.3 Accessors [lazy.counted.iter.access]
Effects: Equivalent to:
8.1.4. x.4 Element access [lazy.counted.iter.elem]
constexpr decltype ( auto ) operator * (); constexpr decltype ( auto ) operator * () const requires dereferenceable < const I > ;
Preconditions:
is true
.
Effects: Equivalent to:
8.1.5. x.5 Navigation [lazy.counted.iter.nav]
Preconditions:
.
Effects: Equivalent to:
if ( length > 1 ) ++ current ; -- length ; return * this ;
Preconditions:
.
Effects: Equivalent to:
-- length ; try { if ( length ) current ++ ; } catch (...) { ++ length ; throw ; }
Effects: Equivalent to:constexpr lazy_counted_iterator operator ++ ( int ) requires forward_iterator < I > ;
lazy_counted_iterator tmp = * this ; ++* this ; return tmp ;
Preconditions:template < common_with < I > I2 > friend constexpr iter_difference_t < I2 > operator - ( const lazy_counted_iterator & x , const lazy_counted_iterator < I2 >& y );
x
and y
refer to elements of the same sequence ([lazy.counted.iterator]).
Effects: Equivalent to:
Effects: Equivalent to:friend constexpr iter_difference_t < I > operator - ( const lazy_counted_iterator & x , default_sentinel_t );
return - x . length ;
Effects: Equivalent to:friend constexpr iter_difference_t < I > operator - ( default_sentinel_t , const lazy_counted_iterator & y );
return y . length ;
8.1.6. x.6 Comparisons [lazy.counted.iter.cmp]
Preconditions:template < common_with < I > I2 > friend constexpr bool operator == ( const lazy_counted_iterator & x , const lazy_counted_iterator < I2 >& y );
x
and y
refer to elements of the same sequence ([lazy.counted.iterator]).
Effects: Equivalent to:
Effects: Equivalent to:friend constexpr bool operator == ( const lazy_counted_iterator & x , default_sentinel_t );
return x . length == 0 ;
Preconditions:template < common_with < I > I2 > friend constexpr strong_ordering operator <=> ( const lazy_counted_iterator & x , const lazy_counted_iterator < I2 >& y );
x
and y
refer to elements of the same sequence ([lazy.counted.iterator]).
Effects: Equivalent to:
[Note 1: The argument order in the Effects: element is reversed because
counts down, not up. — end note]
8.1.7. x.7 Customizations [lazy.counted.iter.cust]
Preconditions:friend constexpr iter_rvalue_reference_t < I > iter_move ( const lazy_counted_iterator & i ) noexcept ( noexcept ( ranges :: iter_move ( i . current ))) requires input_iterator < I > ;
i . length > 0
is true
.
Effects: Equivalent to:
Preconditions: Bothtemplate < indirectly_swappable < I > I2 > friend constexpr void iter_swap ( const lazy_counted_iterator & x , const lazy_counted_iterator < I2 >& y ) noexcept ( noexcept ( ranges :: iter_swap ( x . current , y . current )));
x . length > 0
and y . length > 0
are true
.
Effects: Equivalent to
.
8.2. Wording for views :: lazy_counted
and lazy_take_view
Under Header
synopsis [ranges.syn] add the new types:
// [range.counted], counted view namespace views { inline constexpr unspecified counted = unspecified ; } // freestanding
// [range.lazy.counted], lazy counted view namespace views { inline constexpr unspecified lazy_counted = unspecified ; } // freestanding
// [range.take], take view template < view > class take_view ; // freestanding template < class T > constexpr bool enable_borrowed_range < take_view < T >> = // freestanding enable_borrowed_range < T > ; namespace views { inline constexpr unspecified take = unspecified ; } // freestanding
// [range.lazy.take], lazy take view template < view > class lazy_take_view ; // freestanding template < class T > constexpr bool enable_borrowed_range < lazy_take_view < T >> = // freestanding enable_borrowed_range < T > ; namespace views { inline constexpr unspecified lazy_take = unspecified ; } // freestanding
8.3. Wording for views :: lazy_counted
In Range adaptors [range.adaptors], after 26.7.18 Counted view [range.counted] add new section:
8.3.1. 26.7.x Lazy counted view [range.lazy.counted]
A counted view presents a view of the elements of the counted range
([iterator.requirements.general])
for an iterator
and
non-negative integer
.
The name
denotes a customization point object
([customization.point.object]). Let
and
be expressions, let
be
, and let
be
. If
does not model
,
is ill-formed.
[Note 1: This case can result in substitution failure when
appears in the immediate context of a template instantiation. — end note]
Otherwise,
is expression-equivalent to:
-
If
modelsT
, thencontiguous_iterator
.span ( to_address ( E ), static_cast < size_t > ( static_ - cast < D > ( F ))) -
Otherwise, if
modelsT
, thenrandom_access_iterator
, except thatsubrange ( E , E + static_cast < D > ( F ))
is evaluated only once.E -
Otherwise,
.subrange ( lazy_counted_iterator ( E , F ), default_sentinel )
8.4. Wording for lazy_take_view
After 26.7.10 Take view [range.take] add new section:
26.7.x Lazy take view [range.lazy.take]Under this section add:
8.4.1. x.1 Overview [range.lazy.take.overview]
produces a view of the first N elements from another view, or all
the elements if the adapted view contains fewer than N.
The name
denotes a range adaptor object
([range.adaptor.object]). Let
and
be expressions, let
be
, and let
be
. If
does not model
,
is ill-formed. Otherwise, the
expression
is expression-equivalent to:
-
If
is a specialization ofT
([range.empty.view]), thenranges :: empty_view
, except that the evaluations of(( void ) F , decay - copy ( E ))
andE
are indeterminately sequenced.F -
Otherwise, if
modelsT
andrandom_access_range
and is a specialization ofsized_range
([views.span]),span
([string.view]), orbasic_string_view
([range.subrange]), thenranges :: subrange
, except thatU ( ranges :: begin ( E ), ranges :: begin ( E ) + std :: min < D > ( ranges :: distance ( E ), F ))
is evaluated only once, whereE
is a type determined as follows:U -
if
is a specialization of span, thenT
isU
;span < typename T :: element_type > -
otherwise, if
is a specialization ofT
, thenbasic_string_view
isU
;T -
otherwise,
is a specialization ofT
, andranges :: subrange
isU
;ranges :: subrange < iterator_t < T >> -
otherwise, if
is a specialization ofT
([range.iota.view]) that modelsranges :: iota_view
andrandom_access_range
, thensized_range
, except thatranges :: iota_view ( * ranges :: begin ( E ), * ( ranges :: begin ( E ) + std :: min < D > ( ranges :: distance ( E ), F )))
is evaluated only once.E
-
-
Otherwise, if
is a specialization ofT
([range.repeat.view]):ranges :: repeat_view -
if
modelsT
, thensized_range
except thatviews :: repeat ( * E . value_ , std :: min < D > ( ranges :: distance ( E ), F ))
is evaluated only once;E -
otherwise,
.views :: repeat ( * E . value_ , static_cast < D > ( F ))
-
-
Otherwise,
.ranges :: lazy_take_view ( E , F )
[Example 1:
— end example]vector < int > is { 0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 }; for ( int i : is | views :: lazy_take ( 5 )) cout << i << ' ' ; // prints 0 1 2 3 4
8.4.2. x.2 Class template lazy_take_view
[range.lazy.take.view]
namespace std :: ranges { template < view V > class lazy_take_view : public view_interface < take_view < V >> { private : V base_ = V (); // exposition only range_difference_t < V > count_ = 0 ; // exposition only // [range.lazy.take.sentinel], class template lazy_take_view::sentinel template < bool > class sentinel ; // exposition only public : lazy_take_view () requires default_initializable < V > = default ; constexpr lazy_take_view ( V base , range_difference_t < V > count ); constexpr V base () const & requires copy_constructible < V > { return base_ ; } constexpr V base () && { return std :: move ( base_ ); } constexpr auto begin () requires ( ! simple - view < V > ) { if constexpr ( sized_range < V > ) { if constexpr ( random_access_range < V > ) { return ranges :: begin ( base_ ); } else { auto sz = range_difference_t < V > ( size ()); return lazy_counted_iterator ( ranges :: begin ( base_ ), sz ); } } else if constexpr ( sized_sentinel_for < sentinel_t < V > , iterator_t < V >> ) { auto it = ranges :: begin ( base_ ); auto sz = std :: min ( count_ , ranges :: end ( base_ ) - it ); return lazy_counted_iterator ( std :: move ( it ), sz ); } else { return lazy_counted_iterator ( ranges :: begin ( base_ ), count_ ); } } constexpr auto begin () const requires range < const V > { if constexpr ( sized_range < const V > ) { if constexpr ( random_access_range < const V > ) { return ranges :: begin ( base_ ); } else { auto sz = range_difference_t < const V > ( size ()); return lazy_counted_iterator ( ranges :: begin ( base_ ), sz ); } } else if constexpr ( sized_sentinel_for < sentinel_t < const V > , iterator_t < const V >> ) { auto it = ranges :: begin ( base_ ); auto sz = std :: min ( count_ , ranges :: end ( base_ ) - it ); return lazy_counted_iterator ( std :: move ( it ), sz ); } else { return lazy_counted_iterator ( ranges :: begin ( base_ ), count_ ); } } constexpr auto end () requires ( ! simple - view < V > ) { if constexpr ( sized_range < V > ) { if constexpr ( random_access_range < V > ) return ranges :: begin ( base_ ) + range_difference_t < V > ( size ()); else return default_sentinel ; } else if constexpr ( sized_sentinel_for < sentinel_t < V > , iterator_t < V >> ) { return default_sentinel ; } else { return sentinel < false> { ranges :: end ( base_ )}; } } constexpr auto end () const requires range < const V > { if constexpr ( sized_range < const V > ) { if constexpr ( random_access_range < const V > ) return ranges :: begin ( base_ ) + range_difference_t < const V > ( size ()); else return default_sentinel ; } else if constexpr ( sized_sentinel_for < sentinel_t < const V > , iterator_t < const V >> ) { return default_sentinel ; } else { return sentinel < true> { ranges :: end ( base_ )}; } } constexpr auto size () requires sized_range < V > { auto n = ranges :: size ( base_ ); return ranges :: min ( n , static_cast < decltype ( n ) > ( count_ )); } constexpr auto size () const requires sized_range < const V > { auto n = ranges :: size ( base_ ); return ranges :: min ( n , static_cast < decltype ( n ) > ( count_ )); } }; template < class R > lazy_take_view ( R && , range_difference_t < R > ) -> lazy_take_view < views :: all_t < R >> ; }
Preconditions:
is true
.
Effects: Initializes
with
and
with
.
8.4.3. x.3 Class template lazy_take_view :: sentinel
[range.lazy.take.sentinel]
namespace std :: ranges { template < view V > template < bool Const > class lazy_take_view < V >:: sentinel { private : using Base = maybe - const < Const , V > ; // exposition only template < bool OtherConst > using CI = lazy_counted_iterator < iterator_t < maybe - const < OtherConst , V >>> ; // exposition only sentinel_t < Base > end_ = sentinel_t < Base > (); // exposition only public : sentinel () = default ; constexpr explicit sentinel ( sentinel_t < Base > end ); constexpr sentinel ( sentinel <! Const > s ) requires Const && convertible_to < sentinel_t < V > , sentinel_t < Base >> ; constexpr sentinel_t < Base > base () const ; friend constexpr bool operator == ( const CI < Const >& y , const sentinel & x ); template < bool OtherConst = ! Const > requires sentinel_for < sentinel_t < Base > , iterator_t < maybe - const < OtherConst , V >>> friend constexpr bool operator == ( const CI < OtherConst >& y , const sentinel & x ); }; }
Effects: Initializes
with
.
constexpr sentinel ( sentinel <! Const > s ) requires Const && convertible_to < sentinel_t < V > , sentinel_t < Base >> ;
Effects: Initializes
with
.
Effects: Equivalent to:
friend constexpr bool operator == ( const CI < Const >& y , const sentinel & x ); template < bool OtherConst = ! Const > requires sentinel_for < sentinel_t < Base > , iterator_t < maybe - const < OtherConst , V >>> friend constexpr bool operator == ( const CI < OtherConst >& y , const sentinel & x );
Effects: Equivalent to:
8.5. Opens
8.5.1. iterator_concept
depends on I :: iterator_concept
We followed
defintion, which defines
as
equal to
and only if it’s found. As other iterators always
define
depending on what
models (only
depends on
), maybe
should
follow this for consistency?
9. Note about optimization
It’s interesting to note that with any level of optimization enabled (including
!), gcc is able to "fix the issue" [CE-OPT] for the filter+take case (but
not for
, of course). It’s maybe even more interesting to see
the mentioned optimization is not an optimizer bug, and when the filter will
never return another number, it doesn’t change the behavior [CE-OPT2].
10. Acknowledgements
Many thanks to the Israeli NB members for their feedback and support, in particular Inbal Levi, Dvir Yitzchaki, Dan Raviv and Andrei Zissu. Thanks r/cpp Reddit users for their feedback on P2406R0 [reddit-cpp]. Thanks SG9 members for their feedback and guidance.