Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Can I compare two view sentinels?

I am implementing type-erased iterators for code that uses std::views and have found a problem while trying to compare values that wrap sentinels. Basically, it seems that for some composition of views the std::ranges::end(view) == std::ranges::end(view) comparison does not compile.

This is a minimal example:

#include <ranges>
#include <vector>


int main()
{
    auto vec = std::vector{1, 2, 3, 4, 5, 6, 7, 8};

    auto view = vec
      | std::views::take_while([](auto i) { return i < 8; })
      | std::views::filter([past_value = false](auto i) mutable
        {
            if (past_value)
            {
                return true;
            }
            else
            {
                return false;
            }
        });

    auto end1 = std::ranges::end(view);
    auto end2 = std::ranges::end(view);

    return end1 == end2;
}

This fails to compile on both clang 21.1.0

<source>:26:17: error: invalid operands to binary expression ('_Sentinel' and '_Sentinel')
   26 |     return end1 == end2;
      |            ~~~~ ^  ~~~~
/opt/compiler-explorer/gcc-15.2.0/lib/gcc/x86_64-linux-gnu/15.2.0/../../../../include/c++/15.2.0/ranges:1801:2: note: candidate function (with reversed parameter order) not viable: no known conversion from '_Sentinel' to 'const _Iterator' for 2nd argument
 1801 |         operator==(const _Iterator& __x, const _Sentinel& __y)
      |         ^                                ~~~~~~~~~~~~~~~~~~~~
/opt/compiler-explorer/gcc-15.2.0/lib/gcc/x86_64-linux-gnu/15.2.0/../../../../include/c++/15.2.0/ranges:1801:2: note: candidate function not viable: no known conversion from '_Sentinel' to 'const _Iterator' for 1st argument
 1801 |         operator==(const _Iterator& __x, const _Sentinel& __y)
      |         ^          ~~~~~~~~~~~~~~~~~~~~
1 error generated.
Compiler returned: 1

and gcc 15.2

<source>: In function 'int main()':
<source>:26:17: error: no match for 'operator==' (operand types are 'std::ranges::filter_view<std::ranges::take_while_view<std::ranges::ref_view<std::vector<int, std::allocator<int> > >, main()::<lambda(auto:10)> >, main()::<lambda(auto:11)> >::_Sentinel' and 'std::ranges::filter_view<std::ranges::take_while_view<std::ranges::ref_view<std::vector<int, std::allocator<int> > >, main()::<lambda(auto:10)> >, main()::<lambda(auto:11)> >::_Sentinel')
   26 |     return end1 == end2;
      |            ~~~~ ^~ ~~~~
      |            |       |
      |            |       _Sentinel<[...],[...]>
      |            _Sentinel<[...],[...]>
<source>:26:17: note: there is 1 candidate
   26 |     return end1 == end2;
      |            ~~~~~^~~~~~~
In file included from <source>:1:
/cefs/22/22e6cdc013c8541ce3d1548e_consolidated/compilers_c++_x86_gcc_15.2.0/include/c++/15.2.0/ranges:1801:9: note: candidate 1: 'constexpr bool std::ranges::operator==(const filter_view<take_while_view<ref_view<std::vector<int, std::allocator<int> > >, main()::<lambda(auto:10)> >, main()::<lambda(auto:11)> >::_Iterator&, const filter_view<take_while_view<ref_view<std::vector<int, std::allocator<int> > >, main()::<lambda(auto:10)> >, main()::<lambda(auto:11)> >::_Sentinel&)' (reversed)
 1801 |         operator==(const _Iterator& __x, const _Sentinel& __y)
      |         ^~~~~~~~
/cefs/22/22e6cdc013c8541ce3d1548e_consolidated/compilers_c++_x86_gcc_15.2.0/include/c++/15.2.0/ranges:1801:37: note: no known conversion for argument 1 from 'std::ranges::filter_view<std::ranges::take_while_view<std::ranges::ref_view<std::vector<int, std::allocator<int> > >, main()::<lambda(auto:10)> >, main()::<lambda(auto:11)> >::_Sentinel' to 'const std::ranges::filter_view<std::ranges::take_while_view<std::ranges::ref_view<std::vector<int, std::allocator<int> > >, main()::<lambda(auto:10)> >, main()::<lambda(auto:11)> >::_Iterator&'
 1801 |         operator==(const _Iterator& __x, const _Sentinel& __y)
      |                    ~~~~~~~~~~~~~~~~~^~~
Compiler returned: 1

Curiously, if I comment out the | std::views::take_while([](auto i) { return i < 8; }) line (so I am left with just filter), the code compiles and works as expected.

See live demo.

Is comparing two sentinels ill-formed? Shouldn't std::ranges::end(view) == std::ranges::end(view) compile for any view?

like image 297
Krzysiek Karbowiak Avatar asked Nov 16 '25 13:11

Krzysiek Karbowiak


2 Answers

Sentinels are required to model the sentinel_for concept, which is defined as:

template<class S, class I>
concept sentinel_for =
    semiregular<S> &&
    input_or_output_iterator<I> &&
    weakly-equality-comparable-with<S, I>;

You can see that S needs to be equality comparable with the iterator type I, but it doesn't need to be comparable with itself.

like image 62
cpplearner Avatar answered Nov 18 '25 05:11

cpplearner


Sentinel types only need to be comparable to iterator types within range; they don't need to be comparable to themselves.

Forward iterators can serve as sentinels because they are syntactically comparable to themselves.

However, this does not apply to non-iterator sentinel types, such as default_sentinel and unreachable_sentinel; they are simply empty classes that cannot be compared to themselves.

like image 36
康桓瑋 Avatar answered Nov 18 '25 04:11

康桓瑋



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!