Created on 2026-06-16.00:00:00 last changed 1 week ago
Proposed resolution:
This wording is relative to N5046.
Modify [range.join.view], class template `join_view` synopsis, as indicated:
namespace std::ranges {
template<input_range V>
requires view<V> && input_range<range_reference_t<V>>
class join_view : public view_interface<join_view<V>> {
[…]
constexpr auto end() {
if constexpr (forward_range<V> &&
is_reference_v<InnerRng> && forward_range<InnerRng> &&
common_range<V> && common_range<InnerRng>)
return iterator<simple-view<V>>{*this, ranges::end(base_)};
else
return sentinel<simple-view<V>>{*this};
}
constexpr auto end() const
requires forward_range<const V> &&
is_reference_v<range_reference_t<const V>> &&
input_range<range_reference_t<const V>> {
if constexpr (forward_range<range_reference_t<const V>> &&
common_range<const V> &&
common_range<range_reference_t<const V>>)
return iterator<true>{*this, ranges::end(base_)};
else
return sentinel<true>{*this};
}
};
[…]
}
Modify [range.join.with.view] as indicated:
namespace std::ranges {
[…]
template<input_range V, forward_range Pattern>
requires view<V> && input_range<range_reference_t<V>>
&& view<Pattern>
&& concatable<range_reference_t<V>, Pattern>
class join_with_view : public view_interface<join_with_view<V, Pattern>> {
[…]
constexpr auto end() {
if constexpr (forward_range<V> &&
is_reference_v<InnerRng> && forward_range<InnerRng> &&
common_range<V> && common_range<InnerRng>)
return iterator<simple-view<V> && simple-view<Pattern>>{*this, ranges::end(base_)};
else
return sentinel<simple-view<V> && simple-view<Pattern>>{*this};
}
constexpr auto end() const
requires forward_range<const V> && forward_range<const Pattern> &&
is_reference_v<range_reference_t<const V>> &&
input_range<range_reference_t<const V>> &&
concatable<range_reference_t<const V>, const Pattern> {
using InnerConstRng = range_reference_t<const V>;
if constexpr (forward_range<InnerConstRng> &&
common_range<const V> && common_range<InnerConstRng>)
return iterator<true>{*this, ranges::end(base_)};
else
return sentinel<true>{*this};
}
};
[…]
}
Currently, `join_view` will only be `common_range` if the following five constraints are met:
constexpr auto end() {
if constexpr (forward_range<V> &&
is_reference_v<InnerRng> && forward_range<InnerRng> &&
common_range<V> && common_range<InnerRng>)
return iterator<simple-view<V>>{*this, ranges::end(base_)};
else
return sentinel<simple-view<V>>{*this};
}
The first three are reasonable, since those are necessary conditions for
join_view::iterator being a
`forward_iterator`: the iterator of both base and inner range should be
`forward_iterator`,
and the inner range should
be a reference type to guarantees the multi-pass during the iteration, so that
join_view::iterator can synthesize
the
following
`operator==`:
friend constexpr bool operator==(const iterator& x, const iterator& y)
requires ref-is-glvalue && forward_range<Base> &&
equality_comparable<iterator_t<range_reference_t<Base>>> {
return x.outer_ == y.outer_ && x.inner_ == y.inner_;
}
Where `outer_` is the iterator of base, and `inner_` is the iterator of inner range wrapped in `optional`. The constraints in the function signature match the corresponding first three constraints.
The fourth, requiring the base range to be `common_range`,
is also reasonable because we construct join_view::iterator
via ranges::end(base_),
which is stored in its `outer_` member,
which need to be the same type as ranges::begin(base_).
However, the last one, requiring the inner range to be a `common range`,
is not very meaningful.
Because we don't store the end iterator of the inner range in
join_view::iterator,
the inner_ member is just an `optional` with no value.
When we call satisfy() in the constructor
(to skip over empty inner ranges), such a constraint serves no purpose
if we examine satisfy() closely:
constexpr void satisfy() {
auto update_inner = [this](const iterator_t<Base>& x) -> auto&& {
if constexpr (ref-is-glvalue) // *x is a reference
return *x;
else
return parent_->inner_.emplace-deref(x);
};
for (; outer() != ranges::end(parent_->base_); ++outer()) {
auto&& inner = update_inner(outer());
inner_ = ranges::begin(inner);
if (*inner_ != ranges::end(inner))
return;
}
if constexpr (ref-is-glvalue)
inner_.reset();
}
The call of `ranges::end(inner)` is only used to compare whether
`inner_` has reached the end of the inner range,
which has nothing to do with whether the inner range is a `common_range`.
The only place where this condition is need is in the function body of
`operator--`,
which already imposes a constraint of
common_range<range_reference_t<Base>>.
As a result, this unnecessary constraint prevents some reasonable cases where `join_view` can be a `common_range`, for example:
vector<simd::vec<int>> v; auto j = v | views::join;
As above, `simd::vec` is `random_access_range`, but since it is not `common_range`, its role as the inner range makes the `join_view` no longer `common_range`. This means we cannot directly pass `j.begin()` and `j.end()` into the legacy algorithm which is unsatisfactory.
Note that `join_with_view` has the similar issue for being a `common_range`.
| History | |||
|---|---|---|---|
| Date | User | Action | Args |
| 2026-06-16 14:46:51 | admin | set | messages: + msg16481 |
| 2026-06-16 00:00:00 | admin | create | |