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.

4597. Unnecessary constraint for join_view/join_with_view to be common_range

Section: 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.

  1. 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};
        }
      };
      […]
    }
    
  2. 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};
        }
      };
      […]
    }