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?
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With