Issue 3715: view_interface::empty is overconstrained (original) (raw)


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.

3715. view_interface::empty is overconstrained

Section: 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 { 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 #include

int main() { auto f = std::views::iota(0, 5) | std::views::filter( { 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::emptyand evaluates r.begin() == r.end(), which constructs the iterator, invokes ranges::next, and caches the result, which is unnecessary.

Also consider:

#include #include

int main() { auto i = std::views::istream(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 25.5.3.1 [view.interface.general] as indicated:

    namespace std::ranges {
    template
    requires is_class_v && same_as<D, remove_cv_t>
    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 || forward_range {
if constexpr (sized_range)
return ranges::size(derived()) == 0;
else
return ranges::begin(derived()) == ranges::end(derived());
}
constexpr bool empty() const requires sized_range || forward_range {
if constexpr (sized_range)
return ranges::size(derived()) == 0;
else
return ranges::begin(derived()) == ranges::end(derived());
}
[…]
};
}