C++ Logo

std-proposals

Advanced search

Re: [std-proposals] <ranges>: Provide member empty() for ranges adaptors (whenever possible)

From: Hewill Kang <hewillk_at_[hidden]>
Date: Wed, 31 Jan 2024 00:45:41 +0800
>
> I support the general direction of making the Ranges view types more
> consistent; but why is your proposal not simply to change
> https://eel.is/c++draft/view.interface
> like this?


I believe this does not solve the original issue.
Let’s leave aside the issue of constraints relying on themselves this may
cause.
 In my original example, the type of s | std::views::as_rvalue is
as_rvalue_view<subrange<istream_iterator<int>,
istream_iterator<int>>>, so derived() returns as_rvalue_view, and
ranges::empty cannot be applied to this as_rvalue_view since it is neither
forward_range nor sized_range.

Hewill

Arthur O'Dwyer <arthur.j.odwyer_at_[hidden]> 於 2024年1月31日 週三 上午12:22寫道:

> I support the general direction of making the Ranges view types more
> consistent; but why is your proposal not simply to change
> https://eel.is/c++draft/view.interface
> like this?
>
> constexpr bool empty() requires sized_range<D> || forward_range<D> *requires
> { ranges::empty(derived()); }* {
> if constexpr (sized_range<D>)
> return ranges::size(derived()) == 0;
> else
> return ranges::begin(derived()) == ranges::end(derived());
> *return ranges::empty(derived());*
> }
> constexpr bool empty() const requires sized_range<const D> ||
> forward_range<const D> *requires { ranges::empty(derived()); }* {
> if constexpr (sized_range<const D>)
> return ranges::size(derived()) == 0;
> else
> return ranges::begin(derived()) == ranges::end(derived());
> *return ranges::empty(derived());*
> }
>
> –Arthur
>
>
> On Tue, Jan 30, 2024 at 10:07 AM Hewill Kang via Std-Proposals <
> std-proposals_at_[hidden]> wrote:
>
>> Hi C++ experts,
>>
>> Currently, except for ref_view and owning_view, which explicitly provide
>> the constrained empty() member, other range adaptors do not provide
>> empty().
>>
>> This may be because the original design expected these range adaptors to
>> get free empty() from the view_interface.
>>
>> However, view_interface::empty() requires derived classes to satisfy
>> forward_range, which ultimately results in input_ranges with an empty()
>> member losing the functionality to query emptiness when applied to these
>> adaptors. A real-life example could be <https://godbolt.org/z/T8ejaM8jP>:
>>
>> #include <iostream>
>> #include <ranges>
>> #include <sstream>
>>
>> int main() {
>> std::istringstream ints("1 2 3 4 5");
>> auto s = std::ranges::subrange(std::istream_iterator<int>(ints),
>> std::istream_iterator<int>());
>> std::cout << s.empty() << "\n";
>> auto r = s | std::views::as_rvalue;
>> std::cout << r.empty() << "\n"; // failed
>> }
>>
>> views::as_rvalue does *not* change the number of elements of the
>> underlying range, so it seems worthwhile to provide it with a constrained
>> empty() that simply checks whether ranges::empty(*base_*) is valid.
>>
>> I believe there are other adaptors such as views::transform or
>> views::take, etc. can also do this to preserve the empty() of the
>> original range. For example, the as_rvalue_view::empty() member can be
>> simply implemented as:
>>
>> namespace std::ranges {
>> template<view V>
>> requires input_range<V>
>> class as_rvalue_view : public view_interface<as_rvalue_view<V>> {
>> […]
>>
>> constexpr bool empty() requires requires { ranges::empty(*base_*); }
>> { return ranges::empty(*base_*); }
>> constexpr bool empty() const requires requires { ranges::empty(*base_*); }
>> { return ranges::empty(*base_*); }
>>
>> constexpr auto size() requires sized_range<V> { return ranges::size(*base_*); }
>> constexpr auto size() const requires sized_range<const V> { return ranges::size(*base_*); }
>> };
>> […]
>> }
>>
>> And for take_view, its empty() member can be implemented as:
>>
>> namespace std::ranges {
>> template<view V>
>> class take_view : public view_interface<take_view<V> {
>> […]
>>
>> constexpr bool empty() requires requires { ranges::empty(*base_*); } {
>> return ranges::empty(*base_*) || *count_* == 0;
>> }
>> constexpr bool empty() const requires requires { ranges::empty(*base_*); } {
>> return ranges::empty(*base_*) || *count_* == 0;
>> }
>>
>> constexpr auto size() requires sized_range<V> {
>> auto n = ranges::size(*base_*);
>> return ranges::min(n, static_cast<decltype(n)>(*count_*));
>> }
>> constexpr auto size() const requires sized_range<V> {
>> auto n = ranges::size(*base_*);
>> return ranges::min(n, static_cast<decltype(n)>(*count_*));
>> }
>> };
>> […]
>> }
>>
>> Since split_view always returns a non-empty range, its empty() can just be:
>>
>> namespace std::ranges {
>> template<forward_range V, forward_range Pattern>
>> requires view<V> && view<Pattern> &&
>> indirectly_comparable<iterator_t<V>, iterator_t<Pattern>, ranges::equal_to> &&
>> class split_view<V, Pattern> : public view_interface<split_view<V, Pattern>> {
>> […]
>> constexpr bool empty() const noexcept { return false; }
>> };
>> […]
>> }
>>
>> For zip_view and cartesian_product_view, its empty() is a little more complicated:
>>
>> namespace std::ranges {
>> […]
>> template<input_range... Views>
>> requires (view<Views> && ...) && (sizeof...(Views) > 0)
>> class zip_view : public view_interface<zip_view<Views...>> {
>> […]
>>
>> constexpr bool empty() requires
>> (requires { ranges::empty(declval<Views&>()); } && ...);
>> constexpr bool empty() requires
>> (requires { ranges::empty(declval<const Views&>()); } && ...);
>>
>> constexpr auto size() requires (sized_range<Views> && ...);
>> constexpr auto size() const requires (sized_range<const Views> && ...);
>>
>> […]
>> };
>> […]
>>
>> }
>>
>> constexpr bool empty() requires
>> (requires { ranges::empty(declval<Views&>()); } && ...);
>> constexpr bool empty() requires
>> (requires { ranges::empty(declval<const Views&>()); } && ...);
>>
>> -?- *Effects*: Equivalent to:
>>
>> return apply([](auto... is_empty) { return (is_empty || ...); },
>> *tuple-transform*(ranges::empty, *views_*));
>>
>>
>> For adoptors that always satisfy forward_range such as reverse_view and slide_view, there is no need to provide empty(),
>> because they can always get free empty() from view_interface.
>>
>> I noticed that the current standard does *not* clearly specify what the return value of ranges::empty() means and what time complexity it has,
>> so I may need to introduce an extra concept similar to sized_range to provide semantic requirements for ranges::empty(t), somthing like:
>>
>> template<class T>
>> concept *empty-checkable-range* = range<T> && requires(T& t) { ranges::empty(t); };
>>
>> *-*1- Given an lvalue t of type remove_reference_t<T>, T models *empty-checkable-range* only if: *some semantic requirements omit*
>>
>> Maybe this is also the time to add [[nodiscard]] enhancements to member empty() throughout <ranges>, perharps?
>>
>> So the above wording can be refined as:
>>
>> namespace std::ranges {
>> template<view V>
>> requires input_range<V>
>> class as_rvalue_view : public view_interface<as_rvalue_view<V>> {
>> […]
>>
>> [*[nodiscard]] *constexpr bool empty() requires *empty-checkable-range*<V>
>> { return ranges::empty(*base_*); }
>> [[nodiscard]] constexpr bool empty() const requires *empty-checkable-range*<const V>
>> { return ranges::empty(*base_*); }
>>
>> constexpr auto size() requires sized_range<V> { return ranges::size(*base_*); }
>> constexpr auto size() const requires sized_range<const V> { return ranges::size(*base_*); }
>> };
>> […]
>> }
>>
>> Not sure if *empty-checkable-range* has a better name.
>>
>> What do you guys think of the above? Can this be considered a worthwhile enhancement? Comments at any level are welcome.
>>
>> Hewill
>>
>> --
>> Std-Proposals mailing list
>> Std-Proposals_at_[hidden]
>> https://lists.isocpp.org/mailman/listinfo.cgi/std-proposals
>>
>

Received on 2024-01-30 16:45:53