Under what circumstances is ref_view{E} ill-formed and subrange{E} not?

Viewed 189

C++20 introduces views::all which is a range adaptor that returns a view that includes all elements of its range argument.

The expression views::all(E) is expression-equivalent (has the same effect) to:

  • decay-copy(E) if the decayed type of E models view.
  • Otherwise, ref_view{E} if that expression is well-formed
  • Otherwise, subrange{E}

The first case represents that a view's type is not changed after being piped with views::all (goldbot):

auto r = views::iota(0);
static_assert(std::same_as<decltype(r), decltype(r | views::all)>);

The second case is used to wrap a viewable_range with ref_view to facilitate range pipe operations:

int r[] = {0, 1, 2};
static_assert(std::same_as<ranges::ref_view<int[3]>, decltype(r | views::all)>);

But regarding the third case, I can't think of under what circumstances subrange{E} is well-formed and ref_view{E} is ill-formed.

What is its purpose? Can someone give an example of it?

3 Answers

But regarding the third case, I can't think of under what circumstances subrange{E} is well-formed and ref_view{E} is ill-formed.

ref_view{E} is only well-formed for lvalue ranges.

subrange{E} is only well-formed for borrowed ranges. You can find its deduction guide in [range.subrange.general]:

template<borrowed_­range R>
  subrange(R&&) ->
    subrange<iterator_t<R>, sentinel_t<R>,
             (sized_­range<R> || sized_­sentinel_­for<sentinel_t<R>, iterator_t<R>>)
               ? subrange_kind::sized : subrange_kind::unsized>;

Where a borrowed range is either an lvalue again or a range that opts into being borrowed from. Types like string_view and span are borrowed, for instance.

So if you have something like an rvalue vector<int>, then that's not a view (first bullet) nor can you construct a ref_view from it (because it's an rvalue), nor can you construct a subrange from it (because it's a non-borrowed range).


I realize this doesn't quite answer the question cause I flipped the polarity in my head while typing the answer. But T.C.'s got me covered.

Another purely hypothetical example is a non-view range that stores its contents in a shared_ptr, and then its iterators also share that data. Something like:

struct SharedVector {
    std::shared_ptr<std::vector<int>> data;

    struct Iterator {
        std::shared_ptr<std::vector<int>> data;
        std::vector<int>::iterator cur;

        // ...
    };

    auto begin() -> Iterator { return {data, data->begin()}; }
    auto end() -> Iterator { return {data, data->end()}; }
};

An rvalue SharedVector would not be a view (not O(1)-destructible), you could not ref_view{E} it (because it's an rvalue), but such a range could still be borrowed, so subrange{E} could work.

Borrowed rvalue non-view ranges (views would fall under the first bullet so that the question doesn't arise).

The canonical example in the current standard is span of nonzero static extent (like span<int, 42>). A hypothetical borrowed range type that doesn't support assignment would be another example. Synthetic examples can be constructed as well (since both borrowed_range and view are opt-in).

template<__NotSameAs<ref_view> T>
requires std::convertible_to<T, R&> && requires { _FUN(std::declval<T>()); }
constexpr ref_view(T&& t);

from this, rvalues seem iffy.

template<class R>
ref_view(R&) -> ref_view<R>;

also looks like it blocks rvalues.

Make an rvalue that you can subrange{E}. Like a subrange rvalue. Probably others, depending on deduction guides of subrange.

Related