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
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