Date: Tue, 30 Jan 2024 11:22:13 -0500
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
>
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:22:28