C++ Logo

std-proposals

Advanced search

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

From: Hewill Kang <hewillk_at_[hidden]>
Date: Tue, 30 Jan 2024 23:07:07 +0800
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

Received on 2024-01-30 15:07:20