This page is a snapshot from the LWG issues list, see the Library Active Issues List for more information and the meaning of WP status.

### 3715. `view_interface::empty` is overconstrained

Section: 26.5.3.1 [view.interface.general] Status: WP Submitter: Hewill Kang Opened: 2022-06-12 Last modified: 2022-07-25

Priority: Not Prioritized

View all other issues in [view.interface.general].

View all issues with WP 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)`.

However, this prevents a more efficient way in some cases, i.e., when `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.

Also consider:

>     #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`.

Although we can still use `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.

1. Modify 26.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());
   >         }
   >         […]
   >       };
   >     }