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.

### 3569. `join_view` fails to support ranges of ranges with non-`default_initializable` iterators

Section: 24.7.12.3 [range.join.iterator] Status: New Submitter: Casey Carter Opened: 2021-06-16 Last modified: 2021-08-27

Priority: 3

View all other issues in [range.join.iterator].

View all issues with New status.

Discussion:

`join_view::iterator` has exposition-only members `outer_` — which holds an iterator into the adapted range — and `inner_` — which holds an iterator into the range denoted by `outer_`. After application of P2325R3 "Views should not be required to be default constructible" to the working draft, single-pass iterators can be `non-default_initializable`. P2325R3 constrains `join_view::iterator`'s default constructor to require that the types of both `outer_` and `inner_` are `default_initializable`, indicating an intent to support such iterator types. However, the effect of the non-default constructor specified in 24.7.12.3 [range.join.iterator] paragraph 6 is to default-initialize `inner_`, which is ill-formed if its type is not `default_initializable`.

[2021-06-23; Reflector poll]

Set priority to 3 after reflector poll.

Previous resolution [SUPERSEDED]:

> Wording relative to the post 2021-06 virtual plenary working draft. This PR is currently being implemented in MSVC.

> 1. Modify 24.7.12.3 [range.join.iterator] as indicated:

>    > >     namespace std::ranges {
>    > >       template<input_range V>
>    > >         requires view<V> && input_range<range_reference_t<V>> &&
>    > >                  (is_reference_v<range_reference_t<V>> ||
>    > >                   view<range_value_t<V>>)
>    > >       template<bool Const>
>    > >       struct join_view<V>::iterator {
>    > >         […]
>    > >         optional<InnerIter> inner_ = InnerIter();
>    > >         […]
>    > >         constexpr decltype(auto) operator*() const { return **inner_; }
>    > >         […]
>    > >         friend constexpr decltype(auto) iter_move(const iterator& i)
>    > >         noexcept(noexcept(ranges::iter_move(*i.inner_))) {
>    > >           return ranges::iter_move(*i.inner_);
>    > >         }

>    > >         friend constexpr void iter_swap(const iterator& x, const iterator& y)
>    > >           noexcept(noexcept(ranges::iter_swap(*x.inner_, *y.inner_)))
>    > >           requires indirectly_swappable<InnerIter>;
>    > >       };
>    > >     }

>    > […]

>    >     constexpr void satisfy();       // exposition only

>    > > -5- Effects: Equivalent to:

>    > > >     auto update_inner = [this](const iterator_t<Base>& x) -> auto&& {
>    > > >     […]
>    > > >     };
>    > > >     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() = InnerIter();

>    > […]

>    >     constexpr InnerIter operator->() const
>    >       requires has-arrow<InnerIter> && copyable<InnerIter>;

>    > > -8- Effects: Equivalent to: `return *inner_;`

>    >     constexpr iterator& operator++();

>    > > -9- Let inner-range be:

>    > > […]

>    > > -10- Effects: Equivalent to:

>    > > >     auto&& inner_rng = inner-range;
>    > > >     if (++*inner_ == ranges::end(inner_rng)) {
>    > > >       ++outer_;
>    > > >       satisfy();
>    > > >     }
>    > > >     return *this;

>    > […]

>    >     constexpr iterator& operator--()
>    >       requires ref-is-glvalue && bidirectional_range<Base> &&
>    >                bidirectional_range<range_reference_t<Base>> &&
>    >                common_range<range_reference_t<Base>>;

>    > > -13- Effects: Equivalent to:

>    > > >     if (outer_ == ranges::end(parent_->base_))
>    > > >       inner_ = ranges::end(*--outer_);
>    > > >     while (*inner_ == ranges::begin(*outer_))
>    > > >       *inner_ = ranges::end(*--outer_);
>    > > >     --*inner_;
>    > > >     return *this;

>    > […]

>    >     friend constexpr void iter_swap(const iterator& x, const iterator& y)
>    >       noexcept(noexcept(ranges::iter_swap(*x.inner_, *y.inner_)))
>    >       requires indirectly_swappable<InnerIter>;

>    > > -16- Effects: Equivalent to: return `ranges::iter_swap(*x.inner_, *y.inner_);`

[2021-08-23; Louis Dionne comments and provides improved wording]

I believe the currently proposed resolution is missing the removal of the `default_initializable<InnerIter>` constraint on `join_view::iterator`'s default constructor in 24.7.12.3 [range.join.iterator]. Indeed, after the currently-proposed resolution, `join_view::iterator` reads like:

>     template<input_range V>
>       requires […]
>     struct join_view<V>::iterator {
>     private:
>       optional<InnerIter> inner_; // exposition only
>       […]
>     public:
>       iterator() requires default_initializable<OuterIter> &&
>                           default_initializable<InnerIter> = default;
>         […]
>     };

I believe we should drop the `default_initializable<InnerIter>` constraint from the default constructor (that seems like an oversight unless I missed something):

>     template<input_range V>
>       requires […]
>     struct join_view<V>::iterator {
>     private:
>       optional<InnerIter> inner_; // exposition only
>       […]
>     public:
>       iterator() requires default_initializable<OuterIter> = default;
>       […]
>     };

Proposed resolution:

This wording is relative to N4892.

1. Modify 24.7.12.3 [range.join.iterator] as indicated:

   > >     namespace std::ranges {
   > >       template<input_range V>
   > >         requires view<V> && input_range<range_reference_t<V>> &&
   > >                  (is_reference_v<range_reference_t<V>> ||
   > >                   view<range_value_t<V>>)
   > >       template<bool Const>
   > >       struct join_view<V>::iterator {
   > >         […]
   > >         optional<InnerIter> inner_ = InnerIter();
   > >         […]
   > >         iterator() requires default_initializable<OuterIter> &&
   > >                             default_initializable<InnerIter> = default;
   > >         […]
   > >         constexpr decltype(auto) operator*() const { return **inner_; }
   > >         […]
   > >         friend constexpr decltype(auto) iter_move(const iterator& i)
   > >         noexcept(noexcept(ranges::iter_move(*i.inner_))) {
   > >           return ranges::iter_move(*i.inner_);
   > >         }

   > >         friend constexpr void iter_swap(const iterator& x, const iterator& y)
   > >           noexcept(noexcept(ranges::iter_swap(*x.inner_, *y.inner_)))
   > >           requires indirectly_swappable<InnerIter>;
   > >       };
   > >     }

   > […]

   >     constexpr void satisfy();       // exposition only

   > > -5- Effects: Equivalent to:

   > > >     auto update_inner = [this](const iterator_t<Base>& x) -> auto&& {
   > > >     […]
   > > >     };
   > > >     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() = InnerIter();

   > […]

   >     constexpr InnerIter operator->() const
   >       requires has-arrow<InnerIter> && copyable<InnerIter>;

   > > -8- Effects: Equivalent to: `return *inner_;`

   >     constexpr iterator& operator++();

   > > -9- Let inner-range be:

   > > […]

   > > -10- Effects: Equivalent to:

   > > >     auto&& inner_rng = inner-range;
   > > >     if (++*inner_ == ranges::end(inner_rng)) {
   > > >       ++outer_;
   > > >       satisfy();
   > > >     }
   > > >     return *this;

   > […]

   >     constexpr iterator& operator--()
   >       requires ref-is-glvalue && bidirectional_range<Base> &&
   >                bidirectional_range<range_reference_t<Base>> &&
   >                common_range<range_reference_t<Base>>;

   > > -13- Effects: Equivalent to:

   > > >     if (outer_ == ranges::end(parent_->base_))
   > > >       inner_ = ranges::end(*--outer_);
   > > >     while (*inner_ == ranges::begin(*outer_))
   > > >       *inner_ = ranges::end(*--outer_);
   > > >     --*inner_;
   > > >     return *this;

   > […]

   >     friend constexpr void iter_swap(const iterator& x, const iterator& y)
   >       noexcept(noexcept(ranges::iter_swap(*x.inner_, *y.inner_)))
   >       requires indirectly_swappable<InnerIter>;

   > > -16- Effects: Equivalent to: return `ranges::iter_swap(*x.inner_, *y.inner_);`