3. Motivation

[P0789] introduces the notion of a range adaptor and twelve pioneering range adaptors that
improve declarative, range-based programming. For example, it is possible to perform an inline,
in-place, lazy reverse like so:

P1035 recognises that P0789 introduces only a few of the widely experimented-with range adaptors in [range-v3], and would like to add a few more to complete the C++20 phase of range adaptors.

4. Proposals

Unless otherwise requested, each sub-subsection below should be polled individually from other
sub-subsections. Two major questions are to be asked per range adaptor. It is up to LEWG to decide
the exact phrasing, but the author’s intentions are listed below.

Do we want this range adaptor in C++20?

As-is?

With modifications, as suggested by LEWG?

If we do not want this range adaptor in C++20, do we want it in C++23?

As-is?

With modificaitons, as suggested by LEWG?

4.1. zip_view

Note: This section was formerly titled zip_with_view in P1035R1. Due to
complications in the design process, zip_with_view will not be proposed, but zip_view shall be.
A subsubsection below articulates how to emulate zip_with_view below.

4.1.1. Motivation

A zip, also known as a [convolution] operation, performs a transformation on multiple input
ranges. The typical zip operation transforms several input ranges into a single input range
containing a tuple with the ith element from each range, and terminates when the smallest
finite range is delimited.

Iterating over multiple ranges simultaneously is a useful feature. Both [EWG43] and [P0026] ask
for this functionality at a language level. While these papers request a useful feature that
benefits raw loops, they don’t address algorithmic composition, which is important for ensuring both
correctness and simplicity. A zip_view will allow programmers to iterate over multiple ranges at
the same time when requiring either a raw loop or an algorithm.

Finally, people who work in the parallelism and heterogeneous industries are not able to take full
advantage of the Parallel STL at present due to the limited number of input ranges each algorithm
can support. zip_view will permit this (see [P0836] §2.1 for how this is beneficial).

The benefits of this proposed approach include:

More declared operations, leading to more declarative -- rather than imperative -- styled
programming.

Eliminates state.

A resulting expression can be declared const without needing to rely on IILE.

Temporary storage is eliminated, which helps to improve correctness, clarity, and performance.
P0836 §2.1 expands on the performance benefits for heterogeneous programming.

4.1.1.1. zip_with in C++20

zip_with is a generalisation of zip, such that we can apply an n-ary function in place of
the transforms above. The following is an example of how zip_with can refine zip when tuples
are not necessary.

This isn’t an ideal approach, but some of the finer details of zip_with_view that are independent
of zip_view are still being worked out, and this should not preclude the usage of zipping ranges
for maximum composability.

4.1.2. Problems with pair and tuple

zip_view requires a value type to store the values of the iterators to the ranges that are
iterated over, and a reference type to access those values via an iterator to the range.
Additionally, the concept "Writable wants const proxy references to be assignable, so we need a
tuple-like type with const-qualified assignment operators"[cmcstl2].

Both range-v3 and the cmcstl2 implementation above use an exposition-only type derived from tuple (both) or pair (range-v3 only) that permits a CommonReference between tuple<Ts...>& (an lvalue reference to the value type) and tuple<Ts&...> (the perceived reference
type). This is deemed to be a hack by both Eric Niebler and Casey Carter; a careful reader might
notice that the above implementation specifies this as __tuple_hack (which has conveniently been
left out of P1035).

Adding a third (and possibly fourth) tuple type that is exposition-only is not ideal: this
requires extensions to both pair and tuple so that they are compatible with __tuple_hack and __pair_hack, specialisations for all tuple utilities will be required, and overloads to get, apply, etc. are also necessary.

Alternatively, we can provide an implicit conversion from tuple<Ts...>& to tuple<Ts&...>, and
ensure that a common_reference exists (à la basic_common_reference), and similarly for the other
reference types.

4.1.3.2. zip_view begin

Effects: Returns an iterator containing a tuple of iterators to the beginning of each
range, such that the first iterator corresponds to the beginning of the first range, the second
tuple corresponds to the beginning of the second range, and so on.

4.1.3.3. zip_view end

constexprautoend()requiresall_forward;constexprautoend()const;

Effects: Returns a sentinel containing a tuple of sentinels to the end of each range, such
that the first sentinel corresponds to the end of the first range, the second sentinel
corresponds to the end of the second range, and so on.

4.2. view::zip

The name view::zip denotes a range view adaptor. Let rs... denote a pack expansion of the
parameter pack Rs.... Then, the expression view::zip(rs...) is
expression-equivalent to:

zip_view{rs...} if (Range<Rs>&&...)&&(is_object_v<Rs>&&...) is satisfied.

Otherwise view::zip(rs...) is ill-formed.

4.3. basic_istream_view

4.3.1. Motivation

istream_iterator is an abstraction over a basic_istream object, so that it may be used as though
it were an input iterator. It is a great way to populate a container from the get-go, or fill a
container later in its lifetime. This is great, as copy is a standard algorithm that cleanly
communicates that we’re copying something from one range into another. There aren’t any Hidden
SurprisesTM. This is also good when writing generic code, because the generic library
author does not need to care how things are inserted at the end of v, only that they are.

The problem with istream_iterator is that we need to provide a bogus istream_iterator<T>{} (or default_sentinel{}) every time we want to use it; this acts as the sentinel for istream_iterator. It is bogus, because the code is equivalent to saying "from the first element of
the istream range until the last element of the istream range", but an istream range does not have
a last element. Instead, we denote the end of an istream range to be when the istream's failbit is
set. This is otherwise known as the end-of-stream iterator value, but it doesn’t denote a
'past-the-last element' in the same way that call to vector<T>::end does. Because it is the same
for every range, the end-of-stream object may as well be dropped, so that we can write code
that resembles the code below.

4.3.2.10. basic_istream_view factory

4.4. Introducing associated types for ranges

Note: This section was formerly titled Reviewing iter_value_t, etc., and introducing range_value_t, etc. in P1035R1, but the current title is more accurate.

[P1037] introduced iter_difference_t, iter_value_t, iter_reference_t, and iter_rvalue_reference_t, which dispatch to templates that correclty identify the associated
types for iterators (formerly known as iterator traits). The current mechanism supports iterators,
but not ranges. For example, in order to extract an range’s value type, we are required to do one of
three things:

Use the traditional typenameT::value_type. As we have had template aliases since C++11,
this has probably been abandoned in many projects (not cited).

This was apparently possible in the Ranges TS (albeit in much more detail) [iter.assoc.types.value_type], but P1037 has redefined value_type_t as iter_value_t, which
focuses on iterators, rather than anything defining value_type alias. We could do this in
C++20, but we then risk R::value_type being different to R::iterator::value_type (consider vector<intconst>::value_type and vector<intconst>::iterator::value_type, which are intconst and int, respectively)[value_type].

Use iter_value_t<iterator_t<R>>, which what we probably want, and is also a mouthful.

When discussing extending iter_value_t so that it can also operate on ranges, Casey Carter had
this to say:

What should iter_value_t<T> be when T is an iterator with value type U and a range with
value type V?

Alternately: What is iter_value_t<array<constint>>? array<constint>::value_type is constint, but iter_value_t<iterator_t<array<constint>> is int.

Specific examples aside, early attempts to make the associated type aliases "polymorphic" kept
running into ambiguities that we had to disambiguate or simply declare broken.

I’d rather see range_value_t et al proposed.

P1035 introduces shorthands for iter_foo_t<iterator_t<R>> as range_foo_t<R>, which in turn have
been taken from range-v3.

These have an enormous amount of usage in range-v3, and will ensure consistency between generic code
that uses ranges and generic code that uses iterators (which are essential for underpinning all
range abstractions in C++).

4.4.1. Amendment to the associated iterator types

The previous revision of P1035 imposed explicit Range and InputRange requirements on the above
associated types. While reviewing a Pull Request to add these to cmcstl2, Casey noted that requiring Range and InputRange on the above was not the best approach. The rationale -- taken from a Slack
converation -- is:

Since the "Better Safer Range Access CPOs" work was merged, the definition of Range no longer
depends on iterator_t and sentinel_t; they could be constrained to require Range. If that
were the case, the range_foo_t=foo<iterator_t<X>> aliases wouldn’t need to be so constrained,
because they would already be ill-formed when passed a non-Range.

We’re big on pushing requirements into the "leaves" of the design to put errors closer to the user
and as early as possible, seems like a reasonable change to me.

We couldn’t do it in the TS era, because Range depended on iterator_t and sentinel_t - but
that’s no longer the case.

The former requires that a user define their own sentinel type: something that while not
expert-friendly, is yet to be established as a widespread idiom in C++, and providing a
range adaptor for this purpose will help programmers determine when a sentinel is _not_
necessary.

4.5.2. Notes

There is a slight programmer overhead in the naming: the author felt that both is_odd_sentinel and is_even_sentinel were applicable names: ultimately, the name is_odd_sentinel was chosen because it describes the delimiting condition. An equally
valid reason could probably be made for is_even_sentinel.

A sentinel that takes a lambda may be of interest to LEWG. If there is interest in
this, a proposal could be made in the C++23 timeframe.

4.9.2.3. drop_while_view begin

Remarks: In order to provide the amortized constant time complexity required by the Range concept, the first overload caches the result within the drop_while_view for use on subsequent
calls. Without this, applying a reverse_view over a drop_view would have quadratic iteration
complexity.

4.9.2.4. drop_while_view end

Effects: Equivalent to returnranges::end(base_);.

4.10. view::drop_while

The name view::drop_while denotes a range adaptor object. Let X and Y be expressions such that
type T is decltype((X)) and F is decltype((Y)). Then, the expression view::drop_while(X,Y)is expression-equivalent to:

drop_while_view{X,Y} if T models InputRange, and Y is an object, and F| and models IndirectUnaryPredicate.

Otherwise view::drop_while(E,F) is ill-formed.

4.11. keys and values

4.11.1. Motivation

It is frequent to want to iterate over the keys or the values of an associative container. There is
currently no easy way to do that. It can be approximated using ranges::view::transform, given how
frequently such operation is needed we think their addition would make manipulation of associative
containers easier by more clearly expressing the intent.

These views have been part of ranges-v3 and we also have implemented them in cmcstl2. A lot of
languages and frameworks (notably JS, Python, Qt, Java, [Boost.Range]) offer methods to extract
the keys or values of an associative container, often through .keys() and .values() operations,
respectively.

keys_view and the corresponding adaptor view::keys iterate over the first elements of a
sequence of tuple-like objects of size 2 (tuple of size other than two cannot be considered as
representing a key-value pair).

values_view and the corresponding adaptor view::values iterate over the second elements of a
sequence of tuple-like objects of size 2 (tuple of size other than two cannot be considered as
representing a key-value pair).