This page is a snapshot from the LWG issues list, see the Library Active Issues List for more information and the meaning of C++23 status.
view_interface::empty
is overconstrainedSection: 25.5.3.1 [view.interface.general] Status: C++23 Submitter: Hewill Kang Opened: 2022-06-12 Last modified: 2023-11-22
Priority: Not Prioritized
View all other issues in [view.interface.general].
View all issues with C++23 status.
Discussion:
Currently, view_interface::empty
has the following constraints
constexpr bool empty() requires forward_range<D> { return ranges::begin(derived()) == ranges::end(derived()); }
which seems reasonable, since we need to guarantee the equality preservation of the expression
ranges::begin(r)
.
D
models
sized_range
, we only need to determine whether the value of ranges::size
is 0
.
Since sized_range
and forward_range
are orthogonal to each other, this also
prevents any range that models sized_range
but not forward_range
.
Consider:
#include <iostream>
#include <ranges>
int main() {
auto f = std::views::iota(0, 5)
| std::views::filter([](int) { return true; });
auto r = std::views::counted(f.begin(), 4)
| std::views::slide(2);
std::cout << (r.size() == 0) << "\n"; // #1
std::cout << r.empty() << "\n"; // #2, calls r.begin() == r.end()
}
Since r
models sized_range
, #1
will invoke slide_view::size
,
which mainly invokes ranges::distance
; However, #2
invokes view_interface::empty
and evaluates r.begin() == r.end()
, which constructs the iterator, invokes ranges::next
,
and caches the result, which is unnecessary.
#include <iostream>
#include <ranges>
int main() {
auto i = std::views::istream<int>(std::cin);
auto r = std::views::counted(i.begin(), 4)
| std::views::chunk(2);
std::cout << (r.size() == 0) << "\n"; // #1
std::cout << !r << "\n"; // #2, equivalent to r.size() == 0
std::cout << r.empty() << "\n"; // #3, ill-formed
}
Since r
is still sized_range
, #1
will invoke chunk_view::size
.
#2
is also well-formed since view_interface::operator bool
only requires the
expression ranges::empty(r)
to be well-formed, which first determines the validity of
r.empty()
, and ends up evaluating #1
; However, #3
is ill-formed since
r
is not a forward_range
.
ranges::empty
to determine whether r
is empty, this
inconsistency of the validity of !r
and r.empty()
is quite unsatisfactory.
I see no reason to prevent view_interface::empty
when D
is sized_range
,
since checking whether ranges::size(r) == 0
is an intuitive way to check for empty, as
ranges::empty
does.
[2022-06-21; Reflector poll]
Set status to Tentatively Ready after six votes in favour during reflector poll.
[2022-07-15; LWG telecon: move to Ready]
[2022-07-25 Approved at July 2022 virtual plenary. Status changed: Ready → WP.]
Proposed resolution:
This wording is relative to N4910.
Modify 25.5.3.1 [view.interface.general] as indicated:
namespace std::ranges { template<class D> requires is_class_v<D> && same_as<D, remove_cv_t<D>> class view_interface { private: constexpr D& derived() noexcept { // exposition only return static_cast<D&>(*this); } constexpr const D& derived() const noexcept { // exposition only return static_cast<const D&>(*this); } public: constexpr bool empty() requires sized_range<D> || forward_range<D> { if constexpr (sized_range<D>) return ranges::size(derived()) == 0; else return ranges::begin(derived()) == ranges::end(derived()); } constexpr bool empty() const requires sized_range<const D> || forward_range<const D> { if constexpr (sized_range<const D>) return ranges::size(derived()) == 0; else return ranges::begin(derived()) == ranges::end(derived()); } […] }; }