title | document | date | audience | author | toc | ||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
`views::concat` |
P2542R8 |
2024-03-20 |
SG9, LEWG, LWG |
|
true |
- requires expression update in
operator-(it, default_sentinel)
- Various minor wording fixes
- update wording w.r.t
!common_range && random_access_range && sized_range
changes - fix const-conversion constructor
- Various wording fixes
- Added a section
!common_range && random_access_range && sized_range
- Removed
concat_expert
(reverting to R3) per SG9 poll. - Fix
static_cast<difference_type>
- Reuse utilities in
cartesian_product_view
to defineconcat-is-bidirectional
- Various wording fixes
- Add
concat_expert
- Redesigned
iter_swap
- Relaxed
random_access_range
constraints - Fixed conversions of
difference
types - Various wording fixes
- Adding extra semantic constraints in the concept
concat-indirectly-readable
to prevent non-equality-preserving behaviour ofoperator*
anditer_move
.
-
Removed the
common_range
support for underlying ranges that are!common_range && random_access_range && sized_range
. -
Introduced extra exposition concepts to simplify the wording that defines
concatable
.
This paper proposes views::concat
as very briefly introduced in
Section 4.7 of [@P2214R1]. It is a view factory that takes an
arbitrary number of ranges as an argument list, and provides a view that starts
at the first element of the first range, ends at the last element of the last
range, with all range elements sequenced in between respectively in the order
given in the arguments, effectively concatenating, or chaining together the
argument ranges.
::: cmptable
std::vector<int> v1{1,2,3}, v2{4,5}, v3{};
std::array a{6,7,8};
int s = 9;
std::print("[{:n}, {:n}, {:n}, {:n}, {}]\n",
v1, v2, v3, a, s);
// output: [1, 2, 3, 4, 5, @**,**@ 6, 7, 8, 9]
std::vector<int> v1{1,2,3}, v2{4,5}, v3{};
std::array a{6,7,8};
auto s = std::views::single(9);
std::print("{}\n",
std::views::concat(v1, v2, v3, a, s));
// output: [1, 2, 3, 4, 5, 6, 7, 8, 9]
class Foo;
class Bar{
public:
const Foo& getFoo() const;
};
class MyClass{
std::vector<Foo> foos_;
Bar bar_;
public:
auto getFoos () const{
std::vector<std::reference_wrapper<Foo const>> fooRefs;
fooRefs.reserve(foos_.size() + 1);
for (auto const& f : foos_) {
fooRefs.emplace_back(f);
}
fooRefs.emplace_back(bar_.getFoo());
return fooRefs;
}
};
// user
for(const auto& foo: myClass.getFoos()){
// `foo` is std::reference_wrapper<Foo const>, not simply a Foo
// ...
}
class Foo;
class Bar{
public:
const Foo& getFoo() const;
};
class MyClass{
std::vector<Foo> foos_;
Bar bar_;
public:
auto getFoos () const{
return std::views::concat(
foos_,
std::views::single(std::cref(bar_))
| std::views::transform(&Bar::getFoo)
);
}
};
// user
for(const auto& foo: myClass.getFoos()){
// use foo, which is Foo const &
}
:::
The first example shows that dealing with multiple ranges require care even in
the simplest of cases: The "Before" version manually concatenates all the
ranges in a formatting string, but empty ranges aren't handled and the result
contains an extra comma and a space. With concat_view
, empty ranges are
handled per design, and the construct expresses the intent cleanly and
directly.
In the second example, the user has a class composed of fragmented and indirect
data. They want to implement a member function that provides a range-like view
to all this data sequenced together in some order, and without creating any
copies. The "Before" implementation is needlessly complex and creates a
temporary container with a potentially problematic indirection in its value type.
concat_view
based implementation is a neat one-liner.
This is a generator factory as described in [@P2214R1] Section 4.7. As such, it
can not be piped to. It takes the list of ranges to concatenate as arguments to
ranges::concat_view
constructor, or to ranges::views::concat
customization
point object.
Adaptability of any given two or more distinct range
s into a sequence that
itself models a range
, depends on the compatibility of the reference and the
value types of these ranges. A precise formulation is made in terms of
std::common_reference_t
and std::common_type_t
, and is captured by the
exposition only concept concatable
. See
Wording. Proposed concat_view
is then additionally constrained by this concept. (Note that, this is an
improvement over [@rangev3] concat_view
which lacks such constraints, and
fails with hard errors instead.)
The reference
type is the common_reference_t
of all underlying range's
range_reference_t
. In addition, as the result of common_reference_t
is not
necessarily a reference type, an extra constraint is needed to make sure that
each underlying range's range_reference_t
is convertible to that common
reference.
To support the cases where underlying ranges have proxy iterators, such as
zip_view
, the value_type
cannot simply be the remove_cvref_t
of the
reference
type, and it needs to respect underlying ranges' value_type
.
Therefore, in this proposal the value_type
is defined as the common_type_t
of all underlying range's range_value_t
.
To make concat_view
's iterator's iter_move
behave correctly for the cases
where underlying iterators customise iter_move
, such as zip_view
,
concat_view
has to respect those customizations. Therefore, concat_view
requires common_reference_t
of all underlying ranges's
range_rvalue_reference_t
exist and can be converted to from each underlying
range's range_rvalue_reference_t
.
In order to make concat_view
model input_range
, reference
, value_type
,
and range_rvalue_reference_t
have to be constrained so that the iterator of
the concat_view
models indirectly_readable
.
In the following example,
std::vector<std::string> v = ...;
auto r1 = v | std::views::transform([](auto&& s) -> std::string&& {return std::move(s);});
auto r2 = std::views::iota(0, 2)
| std::views::transform([](auto i){return std::to_string(i)});
auto cv = std::views::concat(r1, r2);
auto it = cv.begin();
*it; // first deref
*it; // second deref
r1
is a range of which range_reference_t
is std::string&&
, while r2
's
range_reference_t
is std::string
. The common_reference_t
between these two
reference
s would be std::string
. After the "first deref", even though the
result is unused, there is a conversion from std::string&&
to std::string
when the result of underlying iterator's operator*
is converted to the
common_reference_t
. This is a move construction and the underlying vector
's
element is modified to a moved-from state. Later when the "second deref" is
called, the result is a moved-from std::string
. This breaks the "equality
preserving" requirements.
Similar to operator*
, ranges::iter_move
has this same issue, and it is more
common to run into this problem. For example,
std::vector<std::string> v = ...;
auto r = std::views::iota(0, 2)
| std::views::transform([](auto i){return std::to_string(i)});
auto cv = std::views::concat(v, r);
auto it = cv.begin();
std::ranges::iter_move(it); // first iter_move
std::ranges::iter_move(it); // second iter_move
v
's range_rvalue_reference_t
is std::string&&
, and r
's
range_rvalue_reference_t
is std::string
, the common_reference_t
between
them is std::string
. After the first iter_move
, the underlying vector
's
first element is moved to construct the temporary common_reference_t
, aka
std::string
. As a result, the second iter_move
results in a moved-from state
std::string
. This breaks the "non-modifying" equality-preserving contract in
indirectly_readable
concept.
A naive solution for this problem is to ban all the usages of mixing ranges of
references with ranges of prvalues. However, not all of this kind of mixing are
problematic. For example, concat
ing a range of std::string&&
with a range of
prvalue std::string_view
is OK, because converting std::string&&
to
std::string_view
does not modify the std::string&&
. In general, it is not
possible to detect whether the conversion from T&&
to U
modifies T&&
through syntactic requirements. Therefore, the authors of this paper propose to
use the "equality-preserving" semantic requirements of the requires-expression
and the notational convention that constant lvalues shall not be modified in a
manner observable to equality-preserving as defined in
[concepts.equality]{.sref}. See
Wording.
Common type and reference based concatable
logic is a practical and convenient
solution that satisfies the motivation as outlined, and is what the authors
propose in this paper. However, there are several potentially important use
cases that get left out:
- Concatenating ranges of different subclasses, into a range of their common base.
- Concatenating ranges of unrelated types into a range of a user-determined
common type, e.g. a
std::variant
.
Here is an example of the first case where common_reference
formulation can
manifest a rather counter-intuitive behavior: Let D1
and D2
be two types
only related by their common base B
, and d1
, d2
, and b
be some range of
these types, respectively. concat(b, d1, d2)
is a well-formed range of B
by
the current formulation, suggesting such usage is supported. However, a mere
reordering of the sequence, say to concat(d1, d2, b)
, yields one that is
not.
The authors believe that such cases should be supported, but can only be done so
via an adaptor that needs at least one explicit type argument at its interface.
A future extension may satisfy these use cases, for example a concat_as
view,
or by even generally via an as
view that is a type-generalized
version of the as_const
view of [@P2278R1].
- No argument
views::concat()
is ill-formed. It can not be aviews::empty<T>
because there is no reasonable way to determine an element typeT
. - Single argument
views::concat(r)
is expression equivalent toviews::all(r)
, which intuitively follows.
Hidden O(N) time complexity for N adapted ranges
Time complexities as required by the ranges
concepts are formally expressed
with respect to the total number of elements (the size) of a given range, and
not to the statically known parameters of that range. Hence, the complexity of
concat_view
or its iterators' operations are documented to be constant time,
even though some of these are a linear function of the number of ranges it
concatenates which is a statically known parameter of this view.
Some examples of these operations for concat_view
are copy, begin
and
size
, and its iterators' increment, decrement, advance, distance. This
characteristic (but not necessarily the specifics) are very much similar to the
other n-ary adaptors like zip_view
[@P2321R2] and cartesian_view
[@P2374R3].
concat_view
can be designed to be a borrowed_range
, if all the underlying
ranges are. However, this requires the iterator implementation to contain a copy
of all iterators and sentinels of all underlying ranges at all times (just like
that of views::zip
[@P2321R2]). On the other hand (unlike views::zip
), a
much cheaper implementation can satisfy all the proposed functionality provided
it is permitted to be unconditionally not borrowed. This implementation would
maintain only a single active iterator at a time and simply refers to the parent
view for the bounds.
Experience shows the borrowed-ness of concat
is not a major requirement; and
the existing implementation in [@rangev3] seems to have picked the cheaper
alternative. This paper proposes the same.
concat_view
can be common_range
if the last underlying range models common_range
In the R0 version, concat_view
can be common_range
if the last underlying
range models common_range || (random_access_range && sized_range)
The end
function was defined as
if constexpr (common_range<last-view>) {
constexpr auto N = sizeof...(Views);
return iterator<is-const>{this, in_place_index<N - 1>,
ranges::end(get<N - 1>(views_))};
} else if constexpr (random_access_range<last-view> && sized_range<last-view>) {
constexpr auto N = sizeof...(Views);
return iterator<is-const>{
this, in_place_index<N - 1>,
ranges::begin(get<N - 1>(views_)) + ranges::size(get<N - 1>(views_))};
} else {
return default_sentinel;
}
Following SG9's direction, the random_access_range && sized_range
was removed in R1 version for simplicity
if constexpr (common_range<last-view>) {
constexpr auto N = sizeof...(Views);
return iterator<is-const>{this, in_place_index<N - 1>,
ranges::end(get<N - 1>(views_))};
} else {
return default_sentinel;
}
However, cartesian_product_view
[range.cartesian.view]{.sref} defines
its end
function in a way that is very similar to concat_view
's R0 version.
template<class R>
concept cartesian-product-common-arg = // exposition only
common_range<R> || (sized_range<R> && random_access_range<R>);
template<class First, class... Vs>
concept cartesian-product-is-common = // exposition only
cartesian-product-common-arg<First>;
template<cartesian-product-common-arg R>
constexpr auto cartesian-common-arg-end(R& r) { // exposition only
if constexpr (common_range<R>) {
return ranges::end(r);
} else {
return ranges::begin(r) + ranges::distance(r);
}
}
constexpr iterator<false> end()
requires ((!simple-view<First> || ... || !simple-view<Vs>) &&
cartesian-product-is-common<First, Vs...>);
constexpr iterator<true> end() const
requires cartesian-product-is-common<const First, const Vs...>;
constexpr default_sentinel_t end() const noexcept;
For consistency between cartesian_product_view
and concat_view
, it would be
good to add back the support of random_access_range && size_range
. Those exposition
only concepts and functions can be reused for both of the views, and their definitions
of end
function would be very similar, except that cartesian_product_view
checks
the first underlying range and concat_view
checks the last underlying range.
As per SG9 direction, this section is dropped.
In R0 version, concat_view
is a common_range
if the last range satisfies
common_range || (random_accessed_range && sized_range)
As per SG9's direction, concat_view
should not pretend to be a common_range
,
when the last underlying range is a !common_range && random_accessed_range && sized_range
,
even though the end iterator could be computed from ranges::begin(r) + ranges::size(r)
.
Therefore in the current version the common_range
is simplified to,
concat-is-common = common_range<last-view>
However, the current design of concat_view
's bidirectional behaviour does use
ranges::begin(r) + ranges::size(r) - 1
to get to the previous range's last iterator,
when the previous range is !common_range && random_accessed_range && sized_range
.
Its bidirectional_range
requirement is designed as follows
template <class R>
concept common-ish = common_range<R> || (random_accessed_range<R> && sized_range<R>)
concat-is-bidi = (bidirectional_range<all-views> && ...) && (common-ish<all-but-last-views> && ...)
In the LWG wording review on 2023-09-13, it was suggested that concat_view
's bidirectional_range
behaviour should be consistent with its common_range
behaviour, i.e, do not support
!common_range && random_accessed_range && sized_range
ranges. This would simplify the wording,
so that to get to the end of the previous range, --ranges::end(r)
would always work.
So,
concat-is-bidi = (bidirectional_range<all-views> && ...) && (common_range<all-but-last-views> && ...)
Similarly, random_access_range
could follow the same design.
To implement it + n
, it is necessary to know if n
is larger than the distance from it
to the
end of the current underlying range. If the underlying range is
!common_range && random_accessed_range && sized_range
, to compute the distance from it
to the end
of the range, an implementation must do ranges::size(r) - (it - ranges::begin(r))
. If it is common_range
,
the distance is simply ranges::distance(it, ranges::end(r))
.
If the support of !common_range && random_accessed_range && sized_range
is dropped,
it would simplify the exposition only helper functions advance_fwd
, advance_bwd
and all other random access related functions.
So change
concat-is-random-access = (random_access_range<all-views> && ...) && (sized_range<all-but-last-views> && ...)
to
concat-is-random-access = (random_access_range<all-views> && ...) && (common_range<all-but-last-views> && ...)
Ranges that are !common_range && random_accessed_range && sized_range
are rare, and if
the user does have one of these ranges and would like to have common, bidirectional or random
access support in the concat_view
, they can do
auto cv = views::concat(r | views::common, other_views...)
As per LEWG direction, the change proposed in this section is adopted
concat_view
can model bidirectional_range
if the underlying ranges satisfy
the following conditions:
-
Every underlying range models
bidirectional_range
-
If the iterator is at nth range's begin position, after
operator--
it should go to (n-1)th's range's end-1 position. This means, the (n-1)th range has to support eithercommon_range && bidirectional_range
, so the position can be reached by--ranges::end(n-1th range)
, assuming n-1th range is not empty, orrandom_access_range && sized_range
, so the position can be reached byranges::begin(n-1th range) + (ranges::size(n-1th range) - 1)
in constant time, assuming n-1th range is not empty. However, as per LEWG's direction,!common_range && random_access_range && sized_range
is removed
Note that the last underlying range does not have to satisfy this constraint because n-1 can never be the last underlying range. If neither of the two constraints is satisfied, in theory we can cache the end-1 position for every single underlying range inside the
concat_view
itself. But the authors do not consider this type of ranges as worth supporting bidirectional
In the concat
implementation in [@rangev3], operator--
is only constrained
on all underlying ranges being bidirectional_range
on the declaration, but its
implementation is using ranges::next(ranges::begin(r), ranges::end(r))
which
implicitly requires random access to make the operation constant time. So it
went with the second constraint.
concat_view
can model random_access_range
if the underlying ranges satisfy
the following conditions:
- Every underlying range models
random_access_range
- All except the last range model
common_range
concat_view
can be sized_range
if all the underlying ranges model
sized_range
This option was originally adopted in the paper. If all the combinations of the
underlying iterators model indirectly_swappable
, then
ranges::iter_swap(x, y)
could delegate to the underlying iterators
std::visit(ranges::iter_swap, x.it_, y.it_);
This would allow swapping elements across heterogeneous ranges, which is very
powerful. However, ranges::iter_swap
is too permissive. Consider the following
example:
int v1[] = {1, 2};
long v2[] = {2147483649, 4};
auto cv = std::views::concat(v1, v2);
auto it1 = cv.begin();
auto it2 = cv.begin() + 2;
std::ranges::iter_swap(it1, it2);
Delegating
ranges::iter_swap(const concat_view::iterator&, const concat_view::iterator&)
to
ranges::iter_swap(int*, long*)
in this case would result in a tripple-move and
leave v1[0]
overflowed.
Another example discussed in SG9 mailing list is
std::string_view v1[] = {"aa"};
std::string v2[] = {"bbb"};
auto cv = std::views::concat(v1, v2);
auto it1 = cv.begin();
auto it2 = cv.begin()+1;
std::ranges::iter_swap(it1, it2);
ranges::iter_swap(string_view*, string*)
in this case would result in dangling
reference due to the tripple move, which is effectively
void iter_swap_impl(string_view* x, string* y)
{
std::string tmp(std::move(*y));
*y = std::move(*x);
*x = std::move(tmp); // *x points to local string tmp
}
It turned out that allowing swapping elements across heterogeneous ranges could result in lots of undesired behaviours.
If the iter_swap
customization is removed, the above examples are no longer an
issue because ranges::iter_swap
would be ill-formed. This is because
iter_reference_t<concat_view::iterator>
are prvalues in those cases.
For the trivial cases where underlying ranges share the same iter_reference_t
,
iter_value_t
and iter_rvalue_reference_t
, the genereated tripple-move swap
would just work.
However, all the user iter_swap
customizations will be ignored, even if the
user tries to concat
the same type of ranges with iter_swap
customizations.
This option was suggested by Tomasz in the SG9 mailing list. The idea is to
-
- use
ranges::iter_swap(x.it_, y.it_)
if they have the same underlying iterator type
- use
-
- otherwise,
ranges::swap(*x, *y)
if it is valid
- otherwise,
The issues decribed in Option 1 are avoided because we only delegate to
underlying iter_swap
if they have the same underlying iterators.
The authors believe that this apporach is a good compromise thus it is adopted in this revision.
views::concat
has been implemented in [@rangev3], with equivalent semantics as
proposed here. The authors also implemented a version that directly follows the
proposed wording below without any issue [@ours].
Add the following to [ranges.syn]{.sref}, header <ranges>
synopsis:
// [...]
namespace std::ranges {
// [...]
// [range.concat], concat view
template <input_range... Views>
requires @*see below*@
class concat_view;
namespace views {
inline constexpr @_unspecified_@ concat = @_unspecified_@;
}
}
Move @*all-random-access*@
, @*all-bidirectional*@
, and @*all-forward*@
from [range.zip.iterator]{.sref} to [range.adaptor.helpers]{.sref}.
Add the following to [range.adaptor.helpers]{.sref}
namespace std::ranges {
// [...]
+ template<bool Const, class... Views>
+ concept @*all-random-access*@ = // exposition only
+ (random_access_range<@*maybe-const*@<Const, Views>> && ...);
+ template<bool Const, class... Views>
+ concept @*all-bidirectional*@ = // exposition only
+ (bidirectional_range<@*maybe-const*@<Const, Views>> && ...);
+ template<bool Const, class... Views>
+ concept @*all-forward*@ = // exposition only
+ (forward_range<@*maybe-const*@<Const, Views>> && ...);
// [...]
}
Remove the following from [range.zip.iterator]{.sref}
namespace std::ranges {
- template<bool Const, class... Views>
- concept @*all-random-access*@ = // exposition only
- (random_access_range<@*maybe-const*@<Const, Views>> && ...);
- template<bool Const, class... Views>
- concept @*all-bidirectional*@ = // exposition only
- (bidirectional_range<@*maybe-const*@<Const, Views>> && ...);
- template<bool Const, class... Views>
- concept @*all-forward*@ = // exposition only
- (forward_range<@*maybe-const*@<Const, Views>> && ...);
// [...]
}
Add the following subclause to [range.adaptors]{.sref}.
[1]{.pnum} concat_view
presents a view
that concatenates all the underlying
ranges.
[2]{.pnum} The name views::concat
denotes a customization point object
([customization.point.object]{.sref}). Given a pack of subexpressions Es...
,
the expression views::concat(Es...)
is expression-equivalent to
- [2.1]{.pnum}
views::all(Es...)
ifEs
is a pack with only one element, - [2.2]{.pnum} otherwise,
concat_view(Es...)
[Example:
std::vector<int> v1{1,2,3}, v2{4,5}, v3{};
std::array a{6,7,8};
auto s = std::views::single(9);
for(auto&& i : std::views::concat(v1, v2, v3, a, s)){
std::print("{} ", i); // prints: 1 2 3 4 5 6 7 8 9
}
- end example]
namespace std::ranges {
template <class... Rs>
using @*concat-reference-t*@ = common_reference_t<range_reference_t<Rs>...>; // exposition only
template <class... Rs>
using @*concat-value-t*@ = common_type_t<range_value_t<Rs>...>; // exposition only
template <class... Rs>
using @*concat-rvalue-reference-t*@ = common_reference_t<range_rvalue_reference_t<Rs>...>; // exposition only
template <class... Rs>
concept @*concat-indirectly-readable*@ = @*see below*@; // exposition only
template <class... Rs>
concept @_concatable_@ = @*see below*@; // exposition only
template <bool Const, class... Rs>
concept @_concat-is-random-access_@ = @*see below*@; // exposition only
template <bool Const, class... Rs>
concept @_concat-is-bidirectional_@ = @*see below*@; // exposition only
template <input_range... Views>
requires (view<Views> && ...) && (sizeof...(Views) > 0) &&
@_concatable_@<Views...>
class concat_view : public view_interface<concat_view<Views...>> {
tuple<Views...> @*views_*@; // exposition only
template <bool Const>
class @_iterator_@; // exposition only
public:
constexpr concat_view() = default;
constexpr explicit concat_view(Views... views);
constexpr @_iterator_@<false> begin() requires(!(@_simple-view_@<Views> && ...));
constexpr @_iterator_@<true> begin() const
requires((range<const Views> && ...) && @_concatable_@<const Views...>);
constexpr auto end() requires(!(@_simple-view_@<Views> && ...));
constexpr auto end() const
requires((range<const Views> && ...) && @_concatable_@<const Views...>);
constexpr auto size() requires(sized_range<Views>&&...);
constexpr auto size() const requires(sized_range<const Views>&&...);
};
template <class... R>
concat_view(R&&...) -> concat_view<views::all_t<R>...>;
}
template <class... Rs>
concept @*concat-indirectly-readable*@ = @*see below*@; // exposition only
:::bq
[1]{.pnum} []{#concat-indirectly-readable-definition} The exposition-only
@*concat-indirectly-readable*@
concept is equivalent to:
template <class Ref, class RRef, class It>
concept @*concat-indirectly-readable-impl*@ = // exposition only
requires (const It it) {
{ *it } -> convertible_to<Ref>;
{ ranges::iter_move(it) } -> convertible_to<RRef>;
};
template <class... Rs>
concept @*concat-indirectly-readable*@ = // exposition only
common_reference_with<@*concat-reference-t*@<Rs...>&&,
@*concat-value-t*@<Rs...>&> &&
common_reference_with<@*concat-reference-t*@<Rs...>&&,
@*concat-rvalue-reference-t*@<Rs...>&&> &&
common_reference_with<@*concat-rvalue-reference-t*@<Rs...>&&,
@*concat-value-t*@<Rs...> const&> &&
(@*concat-indirectly-readable-impl*@<@*concat-reference-t*@<Rs...>,
@*concat-rvalue-reference-t*@<Rs...>,
iterator_t<Rs>> && ...);
:::
template <class... Rs>
concept @_concatable_@ = @*see below*@; // exposition only
:::bq
[2]{.pnum} []{#concatable-definition} The exposition-only @_concatable_@
concept is equivalent to:
template <class... Rs>
concept @_concatable_@ = requires { // exposition only
typename @*concat-reference-t*@<Rs...>;
typename @*concat-value-t*@<Rs...>;
typename @*concat-rvalue-reference-t*@<Rs...>;
} && @*concat-indirectly-readable*@<Rs...>;
:::
template <bool Const, class... Rs>
concept @_concat-is-random-access_@ = @*see below*@; // exposition only
:::bq
[3]{.pnum} Let Fs
be the pack that consists of all elements of Rs
except the last element, then @_concat-is-random-access_@
is equivalent to:
template <bool Const, class... Rs>
concept @_concat-is-random-access_@ = // exposition only
@*all-random-access*@<Const, Rs...> &&
(common_range<@*maybe-const*@<Const, Fs>> && ...);
:::
template <bool Const, class... Rs>
concept @_concat-is-bidirectional_@ = @*see below*@; // exposition only
:::bq
[4]{.pnum} Let Fs
be the pack that consists of all elements of Rs
except the last element, then @_concat-is-bidirectional_@
is equivalent to:
template <bool Const, class... Rs>
concept @_concat-is-bidirectional_@ = // exposition only
@*all-bidirectional*@<Const, Rs...> &&
(common_range<@*maybe-const*@<Const, Fs>> && ...);
:::
constexpr explicit concat_view(Views... views);
:::bq
[5]{.pnum} Effects: Initializes @*views_*@
with std::move(views)...
.
:::
constexpr @_iterator_@<false> begin() requires(!(@_simple-view_@<Views> && ...));
constexpr @_iterator_@<true> begin() const
requires((range<const Views> && ...) && @_concatable_@<const Views...>);
:::bq
[6]{.pnum} Effects: Let @*is-const*@
be true
for the const-qualified overload,
and false
otherwise. Equivalent to:
@_iterator_@<@_is-const_@> it(this, in_place_index<0>, ranges::begin(std::get<0>(@*views_*@)));
it.template @_satisfy_@<0>();
return it;
:::
constexpr auto end() requires(!(@_simple-view_@<Views> && ...));
constexpr auto end() const
requires((range<const Views> && ...) && @_concatable_@<const Views...>);
:::bq
[7]{.pnum} Effects: Let @*is-const*@
be true
for the const-qualified overload,
and false
otherwise. Equivalent to:
constexpr auto N = sizeof...(Views);
if constexpr (common_range<@_maybe-const_@<@*is-const*@, Views...[N-1]>>) {
return @_iterator_@<@_is-const_@>(this, in_place_index<N - 1>,
ranges::end(std::get<N - 1>(@*views_*@)));
} else {
return default_sentinel;
}
:::
constexpr auto size() requires(sized_range<Views>&&...);
constexpr auto size() const requires(sized_range<const Views>&&...);
:::bq
[8]{.pnum} Effects: Equivalent to:
return apply(
[](auto... sizes) {
using CT = @*make-unsigned-like-t*@<common_type_t<decltype(sizes)...>>;
return (CT(sizes) + ...);
},
@_tuple-transform_@(ranges::size, @*views_*@));
:::
namespace std::ranges{
template <input_range... Views>
requires (view<Views> && ...) && (sizeof...(Views) > 0) &&
@_concatable_@<Views...>
template <bool Const>
class concat_view<Views...>::@_iterator_@ {
public:
using iterator_category = @*see below*@; // not always present.
using iterator_concept = @*see below*@;
using value_type = @*concat-value-t*@<@_maybe-const_@<Const, Views>...>;
using difference_type = common_type_t<range_difference_t<@_maybe-const_@<Const, Views>>...>;
private:
using @*base-iter*@ = // exposition only
variant<iterator_t<@_maybe-const_@<Const, Views>>...>;
@_maybe-const_@<Const, concat_view>* @*parent_*@ = nullptr; // exposition only
@*base-iter*@ @*it_*@; // exposition only
template <size_t N>
constexpr void @_satisfy_@(); // exposition only
template <size_t N>
constexpr void @_prev_@(); // exposition only
template <size_t N>
constexpr void @_advance-fwd_@(difference_type offset, difference_type steps); // exposition only
template <size_t N>
constexpr void @_advance-bwd_@(difference_type offset, difference_type steps); // exposition only
template <class... Args>
explicit constexpr @_iterator_@(@_maybe-const_@<Const, concat_view>* parent, Args&&... args)
requires constructible_from<@*base-iter*@, Args&&...>; // exposition only
public:
@_iterator_@() = default;
constexpr @_iterator_@(@_iterator_@<!Const> i)
requires Const && (convertible_to<iterator_t<Views>, iterator_t<const Views>> && ...);
constexpr decltype(auto) operator*() const;
constexpr @_iterator_@& operator++();
constexpr void operator++(int);
constexpr @_iterator_@ operator++(int)
requires @*all-forward*@<Const, Views...>;
constexpr @_iterator_@& operator--()
requires @_concat-is-bidirectional_@<Const, Views...>;
constexpr @_iterator_@ operator--(int)
requires @_concat-is-bidirectional_@<Const, Views...>;
constexpr @_iterator_@& operator+=(difference_type n)
requires @_concat-is-random-access_@<Const, Views...>;
constexpr @_iterator_@& operator-=(difference_type n)
requires @_concat-is-random-access_@<Const, Views...>;
constexpr decltype(auto) operator[](difference_type n) const
requires @_concat-is-random-access_@<Const, Views...>;
friend constexpr bool operator==(const @_iterator_@& x, const @_iterator_@& y)
requires(equality_comparable<iterator_t<@_maybe-const_@<Const, Views>>>&&...);
friend constexpr bool operator==(const @_iterator_@& it, default_sentinel_t);
friend constexpr bool operator<(const @_iterator_@& x, const @_iterator_@& y)
requires @*all-random-access*@<Const, Views...>;
friend constexpr bool operator>(const @_iterator_@& x, const @_iterator_@& y)
requires @*all-random-access*@<Const, Views...>;
friend constexpr bool operator<=(const @_iterator_@& x, const @_iterator_@& y)
requires @*all-random-access*@<Const, Views...>;
friend constexpr bool operator>=(const @_iterator_@& x, const @_iterator_@& y)
requires @*all-random-access*@<Const, Views...>;
friend constexpr auto operator<=>(const @_iterator_@& x, const @_iterator_@& y)
requires (@*all-random-access*@<Const, Views...> &&
(three_way_comparable<iterator_t<@_maybe-const_@<Const, Views>>> && ...));
friend constexpr @_iterator_@ operator+(const @_iterator_@& it, difference_type n)
requires @_concat-is-random-access_@<Const, Views...>;
friend constexpr @_iterator_@ operator+(difference_type n, const @_iterator_@& it)
requires @_concat-is-random-access_@<Const, Views...>;
friend constexpr @_iterator_@ operator-(const @_iterator_@& it, difference_type n)
requires @_concat-is-random-access_@<Const, Views...>;
friend constexpr difference_type operator-(const @_iterator_@& x, const @_iterator_@& y)
requires @_concat-is-random-access_@<Const, Views...>;
friend constexpr difference_type operator-(const @_iterator_@& x, default_sentinel_t)
requires @*see below*@;
friend constexpr difference_type operator-(default_sentinel_t, const @_iterator_@& x)
requires @*see below*@;
friend constexpr decltype(auto) iter_move(const @_iterator_@& it) noexcept(@*see below*@);
friend constexpr void iter_swap(const @_iterator_@& x, const @_iterator_@& y) noexcept(@*see below*@)
requires @*see below*@;
};
}
[1]{.pnum} @_iterator_@::iterator_concept
is defined as follows:
- [1.1]{.pnum} If
@_concat-is-random-access_@<Const, Views...>
is modeled, theniterator_concept
denotesrandom_access_iterator_tag
. - [1.2]{.pnum} Otherwise, if
@_concat-is-bidirectional_@<Const, Views...>
is modeled, theniterator_concept
denotesbidirectional_iterator_tag
. - [1.3]{.pnum} Otherwise, if
@*all-forward*@<Const, Views...>
is modeled, theniterator_concept
denotesforward_iterator_tag
. - [1.4]{.pnum} Otherwise,
iterator_concept
denotesinput_iterator_tag
.
[2]{.pnum} The member typedef-name iterator_category
is defined if and only if
@*all-forward*@<Const, Views...>
is modeled. In that case,
iterator::iterator_category
is defined as follows:
- [2.1]{.pnum} If
is_reference_v<@*concat-reference-t*@<@_maybe-const_@<Const, Views>...>>
isfalse
, theniterator_category
denotesinput_iterator_tag
- [2.2]{.pnum} Otherwise, let
Cs
denote the pack of typesiterator_traits<iterator_t<@*maybe-const*@<Const, Views>>>::iterator_category...
.- [2.2.1]{.pnum} If
(derived_from<Cs, random_access_iterator_tag> && ...) && @_concat-is-random-access_@<Const, Views...>
is true,iterator_category
denotesrandom_access_iterator_tag
. - [2.2.2]{.pnum} Otherwise, if
(derived_from<Cs, bidirectional_iterator_tag> && ...) && @_concat-is-bidirectional_@<Const, Views...>
is true,iterator_category
denotesbidirectional_iterator_tag
. - [2.2.3]{.pnum} Otherwise, If
(derived_from<Cs, forward_iterator_tag> && ...)
is true,iterator_category
denotesforward_iterator_tag
. - [2.2.4]{.pnum} Otherwise,
iterator_category
denotesinput_iterator_tag
.
- [2.2.1]{.pnum} If
template <size_t N>
constexpr void @_satisfy_@(); // exposition only
:::bq
[3]{.pnum} Effects: Equivalent to:
if constexpr (N < (sizeof...(Views) - 1)) {
if (std::get<N>(@*it_*@) == ranges::end(std::get<N>(@*parent_*@->@*views_*@))) {
@*it_*@.template emplace<N + 1>(ranges::begin(std::get<N + 1>(@*parent_*@->@*views_*@)));
@*satisfy*@<N + 1>();
}
}
:::
template <size_t N>
constexpr void @_prev_@(); // exposition only
:::bq
[4]{.pnum} Effects: Equivalent to:
if constexpr (N == 0) {
--std::get<0>(@*it_*@);
} else {
if (std::get<N>(@*it_*@) == ranges::begin(std::get<N>(@*parent_*@->@*views_*@))) {
@*it_*@.template emplace<N - 1>(ranges::end(std::get<N - 1>(@*parent_*@->@*views_*@)));
@_prev_@<N - 1>();
} else {
--std::get<N>(@*it_*@);
}
}
:::
template <size_t N>
constexpr void @_advance-fwd_@(difference_type offset, difference_type steps); // exposition only
:::bq
[5]{.pnum} Effects: Equivalent to:
using underlying_diff_type = iter_difference_t<variant_alternative_t<N, @*base-iter*@>>;
if constexpr (N == sizeof...(Views) - 1) {
std::get<N>(@*it_*@) += static_cast<underlying_diff_type>(steps);
} else {
auto n_size = ranges::distance(std::get<N>(@*parent_*@->@*views_*@));
if (offset + steps < n_size) {
std::get<N>(@*it_*@) += static_cast<underlying_diff_type>(steps);
} else {
@*it_*@.template emplace<N + 1>(ranges::begin(std::get<N + 1>(@*parent_*@->@*views_*@)));
@*advance-fwd*@<N + 1>(0, offset + steps - n_size);
}
}
:::
template <size_t N>
constexpr void @_advance-bwd_@(difference_type offset, difference_type steps); // exposition only
:::bq
[6]{.pnum} Effects: Equivalent to:
using underlying_diff_type = iter_difference_t<variant_alternative_t<N, @*base-iter*@>>;
if constexpr (N == 0) {
std::get<N>(@*it_*@) -= static_cast<underlying_diff_type>(steps);
} else {
if (offset >= steps) {
std::get<N>(@*it_*@) -= static_cast<underlying_diff_type>(steps);
} else {
auto prev_size = ranges::distance(std::get<N - 1>(@*parent_*@->@*views_*@));
@*it_*@.template emplace<N - 1>(ranges::end(std::get<N - 1>(@*parent_*@->@*views_*@)));
@*advance-bwd*@<N - 1>(prev_size, steps - offset);
}
}
:::
template <class... Args>
explicit constexpr @_iterator_@(
@_maybe-const_@<Const, concat_view>* parent,
Args&&... args)
requires constructible_from<@*base-iter*@, Args&&...>; // exposition only
:::bq
[7]{.pnum} Effects: Initializes @*parent_*@
with parent
, and initializes
@*it_*@
with std::forward<Args>(args)...
.
:::
constexpr @_iterator_@(@_iterator_@<!Const> it)
requires Const &&
(convertible_to<iterator_t<Views>, iterator_t<const Views>> && ...);
:::bq
[8]{.pnum} Effects: Initializes @*parent_*@
with it.@*parent_*@
, and
let @*i*@
be it.@*it_*@.index()
, initializes @*it_*@
with
@*base-iter*@(in_place_index<@*i*@>, std::get<@*i*@>(std::move(it.@*it_*@)))
:::
constexpr decltype(auto) operator*() const;
:::bq
[9]{.pnum} Preconditions: @*it_*@.valueless_by_exception()
is false
.
[10]{.pnum} Effects: Equivalent to:
using reference = @*concat-reference-t*@<@_maybe-const_@<Const, Views>...>;
return std::visit([](auto&& it) -> reference {
return *it; }, @*it_*@);
:::
constexpr @_iterator_@& operator++();
:::bq
[11]{.pnum} Preconditions: @*it_*@.valueless_by_exception()
is false
.
[12]{.pnum} Effects: Let @*i*@
be @*it_*@.index()
. Equivalent to:
++std::get<@*i*@>(@*it_*@);
@*satisfy*@<@*i*@>();
return *this;
:::
constexpr void operator++(int);
:::bq
[13]{.pnum} Effects: Equivalent to:
++*this;
:::
constexpr @_iterator_@ operator++(int)
requires @*all-forward*@<Const, Views...>;
:::bq
[14]{.pnum} Effects: Equivalent to:
auto tmp = *this;
++*this;
return tmp;
:::
constexpr @_iterator_@& operator--()
requires @_concat-is-bidirectional_@<Const, Views...>;
:::bq
[15]{.pnum} Preconditions: @*it_*@.valueless_by_exception()
is false
.
[16]{.pnum} Effects: Let @*i*@
be @*it_*@.index()
. Equivalent to:
@*prev*@<@*i*@>();
return *this;
:::
constexpr @_iterator_@ operator--(int)
requires @_concat-is-bidirectional_@<Const, Views...>;
:::bq
[17]{.pnum} Effects: Equivalent to:
auto tmp = *this;
--*this;
return tmp;
:::
constexpr @_iterator_@& operator+=(difference_type n)
requires @_concat-is-random-access_@<Const, Views...>;
:::bq
[18]{.pnum} Preconditions: @*it_*@.valueless_by_exception()
is false
.
[19]{.pnum} Effects: Let @*i*@
be @*it_*@.index()
. Equivalent to:
if(n > 0) {
@*advance-fwd*@<@*i*@>(std::get<@*i*@>(@*it_*@) - ranges::begin(std::get<@*i*@>(@*parent_*@->@*views_*@)), n);
} else if (n < 0) {
@*advance-bwd*@<@*i*@>(std::get<@*i*@>(@*it_*@) - ranges::begin(std::get<@*i*@>(@*parent_*@->@*views_*@)), -n);
}
return *this;
:::
constexpr @_iterator_@& operator-=(difference_type n)
requires @_concat-is-random-access_@<Const, Views...>;
:::bq
[20]{.pnum} Effects: Equivalent to:
*this += -n;
return *this;
:::
constexpr decltype(auto) operator[](difference_type n) const
requires @_concat-is-random-access_@<Const, Views...>;
:::bq
[21]{.pnum} Effects: Equivalent to:
return *((*this) + n);
:::
friend constexpr bool operator==(const @_iterator_@& x, const @_iterator_@& y)
requires(equality_comparable<iterator_t<@_maybe-const_@<Const, Views>>>&&...);
:::bq
[22]{.pnum} Preconditions: x.@*it_*@.valueless_by_exception()
and
y.@*it_*@.valueless_by_exception()
are each false
.
[23]{.pnum} Effects: Equivalent to:
return x.@*it_*@ == y.@*it_*@;
:::
friend constexpr bool operator==(const @_iterator_@& it, default_sentinel_t);
:::bq
[24]{.pnum} Preconditions: it.@*it_*@.valueless_by_exception()
is false
.
[25]{.pnum} Effects: Equivalent to:
constexpr auto last_idx = sizeof...(Views) - 1;
return it.@*it_*@.index() == last_idx &&
std::get<last_idx>(it.@*it_*@) == ranges::end(std::get<last_idx>(it.@*parent_*@->@*views_*@));
:::
friend constexpr bool operator<(const @_iterator_@& x, const @_iterator_@& y)
requires @*all-random-access*@<Const, Views...>;
friend constexpr bool operator>(const @_iterator_@& x, const @_iterator_@& y)
requires @*all-random-access*@<Const, Views...>;
friend constexpr bool operator<=(const @_iterator_@& x, const @_iterator_@& y)
requires @*all-random-access*@<Const, Views...>;
friend constexpr bool operator>=(const @_iterator_@& x, const @_iterator_@& y)
requires @*all-random-access*@<Const, Views...>;
friend constexpr auto operator<=>(const @_iterator_@& x, const @_iterator_@& y)
requires (@*all-random-access*@<Const, Views...> &&
(three_way_comparable<iterator_t<@_maybe-const_@<Const, Views>>> && ...));
:::bq
[26]{.pnum} Preconditions: x.@*it_*@.valueless_by_exception()
and
y.@*it_*@.valueless_by_exception()
are each false
.
[27]{.pnum} Let @*op*@
be the operator.
[28]{.pnum} Effects: Equivalent to:
return x.@*it_*@ @*op*@ y.@*it_*@;
:::
friend constexpr @_iterator_@ operator+(const @_iterator_@& it, difference_type n)
requires @_concat-is-random-access_@<Const, Views...>;
:::bq
[29]{.pnum} Effects: Equivalent to:
auto temp = it;
temp += n;
return temp;
:::
friend constexpr @_iterator_@ operator+(difference_type n, const @_iterator_@& it)
requires @_concat-is-random-access_@<Const, Views...>;
:::bq
[30]{.pnum} Effects: Equivalent to:
return it + n;
:::
friend constexpr @_iterator_@ operator-(const @_iterator_@& it, difference_type n)
requires @_concat-is-random-access_@<Const, Views...>;
:::bq
[31]{.pnum} Effects: Equivalent to:
auto temp = it;
temp -= n;
return temp;
:::
friend constexpr difference_type operator-(const @_iterator_@& x, const @_iterator_@& y)
requires @_concat-is-random-access_@<Const, Views...>;
:::bq
[32]{.pnum} Preconditions: x.@*it_*@.valueless_by_exception()
and
y.@*it_*@.valueless_by_exception()
are each false
.
[33]{.pnum} Effects: Let @*i~x~*@
denote x.@*it_*@.index()
and @*i~y~*@
denote y.@*it_*@.index()
-
[33.1]{.pnum} if
@*i~x~*@ > @*i~y~*@
, let@*d~y~*@
beranges::distance(std::get<@*i~y~*@>(y.@*it_*@), ranges::end(std::get<@*i~y~*@>(y.@*parent_*@->@*views_*@)))
,@*d~x~*@
beranges::distance(ranges::begin(std::get<@*i~x~*@>(x.@*parent_*@->@*views_*@)), std::get<@*i~x~*@>(x.@*it_*@))
. Lets
denote the sum of the sizes of all the rangesstd::get<@*i*@>(x.@*parent_*@->@*views_*@)
for every integer@*i*@
in the range[@*i~y~*@ + 1, @*i~x~*@)
if there is any, and0
otherwise, of typedifference_type
, equivalent toreturn @*d~y~*@ + s + @*d~x~*@;
-
[33.2]{.pnum} otherwise, if
@*i~x~*@ < @*i~y~*@
, equivalent to:return -(y - x);
-
[33.3]{.pnum} otherwise, equivalent to:
return std::get<@*i~x~*@>(x.@*it_*@) - std::get<@*i~y~*@>(y.@*it_*@);
:::
friend constexpr difference_type operator-(const @_iterator_@& x, default_sentinel_t)
requires @*see below*@;
:::bq
[34]{.pnum} Preconditions: x.@*it_*@.valueless_by_exception()
is false
.
[35]{.pnum} Effects: Let @*i~x~*@
denote x.@*it_*@.index()
, @*d~x~*@
be ranges::distance(std::get<@*i~x~*@>(x.@*it_*@), ranges::end(std::get<@*i~x~*@>(x.@*parent_*@->@*views_*@)))
.
Let s
denote the sum of the sizes of all the ranges
std::get<@*i*@>(x.@*parent_*@->@*views_*@)
for every integer @*i*@
in the range
[@*i~x~*@ + 1, sizeof...(Views))
if there is any, and 0
otherwise, of type difference_type
,
equivalent to
return -(@*d~x~*@ + s);
[36]{.pnum} Remarks: Let Fs
be the pack that consists of all elements of Views
except the first element, the expression in the requires-clause is equivalent to:
(sized_sentinel_for<sentinel_t<@*maybe-const*@<Const, Views>>,
iterator_t<@*maybe-const*@<Const, Views>>> && ...)
&& (sized_range<@*maybe-const*@<Const, Fs>> && ...)
:::
friend constexpr difference_type operator-(default_sentinel_t, const @_iterator_@& x)
requires @*see below*@;
:::bq
[37]{.pnum} Effects: Equivalent to:
return -(x - default_sentinel);
[38]{.pnum} Remarks: Let Fs
be the pack that consists of all elements of Views
except the first element, the expression in the requires-clause is equivalent to:
(sized_sentinel_for<sentinel_t<@*maybe-const*@<Const, Views>>,
iterator_t<@*maybe-const*@<Const, Views>>> && ...)
&& (sized_range<@*maybe-const*@<Const, Fs>> && ...)
:::
friend constexpr decltype(auto) iter_move(const @_iterator_@& it) noexcept(@*see below*@);
:::bq
[39]{.pnum} Preconditions: it.@*it_*@.valueless_by_exception()
is false
.
[40]{.pnum} Effects: Equivalent to:
return std::visit(
[](const auto& i) ->
@*concat-rvalue-reference-t*@<@_maybe-const_@<Const, Views>...> {
return ranges::iter_move(i);
},
it.@*it_*@);
[41]{.pnum} Remarks: The exception specification is equivalent to:
((is_nothrow_invocable_v<decltype(ranges::iter_move),
const iterator_t<@_maybe-const_@<Const, Views>>&> &&
is_nothrow_convertible_v<range_rvalue_reference_t<@_maybe-const_@<Const, Views>>,
@*concat-rvalue-reference-t*@<@_maybe-const_@<Const, Views>...>>) && ...)
:::
friend constexpr void iter_swap(const @_iterator_@& x, const @_iterator_@& y) noexcept(@*see below*@)
requires @*see below*@;
:::bq
[42]{.pnum} Preconditions: x.@*it_*@.valueless_by_exception()
and
y.@*it_*@.valueless_by_exception()
are each false
.
[43]{.pnum} Effects: Equivalent to:
std::visit(
[&](const auto& it1, const auto& it2) {
if constexpr (is_same_v<decltype(it1), decltype(it2)>) {
ranges::iter_swap(it1, it2);
} else {
ranges::swap(*x, *y);
}
},
x.@*it_*@, y.@*it_*@);
[44]{.pnum} Remarks: The exception specification is equivalent to
(noexcept(ranges::swap(*x, *y)) && ... && noexcept(ranges::iter_swap(its, its)))
where its
is a pack of lvalues of type const iterator_t<@*maybe-const*@<Const, Views>>
respectively.
[45]{.pnum} Remarks: The expression in the requires-clause is equivalent to
swappable_with<iter_reference_t<@*iterator*@>, iter_reference_t<@*iterator*@>> &&
(... && indirectly_swappable<iterator_t<@*maybe-const*@<Const, Views>>>)
:::
Add the following macro definition to [version.syn]{.sref}, header <version>
synopsis, with the value selected by the editor to reflect the date of adoption
of this paper:
#define __cpp_lib_ranges_concat 20XXXXL // also in <ranges>
references:
-
id: rangev3 citation-label: range-v3 title: "range-v3 library" author:
- family: Niebler given: Eric URL: https://github.com/ericniebler/range-v3
-
id: ours citation-label: ours title: "A proof-of-concept implementation of views::concat" author:
- family: Xie given: Hui
- family: Yilmaz given: S. Levent URL: https://github.com/huixie90/cpp_papers/tree/main/impl/concat
<style> .bq{ display: block; margin-block-start: 1em; margin-block-end: 1em; margin-inline-start: 40px; margin-inline-end: 40px; } </style>