This page is a snapshot from the LWG issues list, see the Library Active Issues List for more information and the meaning of New status.
join_view/join_with_view to be common_rangeSection: 25.7.14.2 [range.join.view], 25.7.15.2 [range.join.with.view] Status: New Submitter: Hewill Kang Opened: 2026-06-16 Last modified: 2026-06-16
Priority: Not Prioritized
View other active issues in [range.join.view].
View all other issues in [range.join.view].
View all issues with New status.
Discussion:
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.
Proposed resolution:
This wording is relative to N5032.
Modify 25.7.14.2 [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 25.7.15.2 [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};
}
};
[…]
}