Legacy iterator adaptors such as reverse_iterator
and move_iterator
, or C++20/23 newly introduced adaptors such as counted_iterator
, basic_const_iterator
, and move_sentinel
, all of them provide base()
member to allow us to access the underlying iterator/sentinel:
constexpr const I& base() const& noexcept { return current_; }
constexpr I base() && { return std::move(current_); }
However, for a series of iterators/sentinels of range adaptors in <ranges>
, I found that they do not all provide a base()
. For example, split_view
's "outer-iterator" provides a base()
, while lazy_split_view
does not (godbolt):
string_view s = "one two three four";
// ["one", "two", "three", "four"]
auto r1 = s | views::split(' ');
for (auto it = r1.begin(); it != r1.end(); ++it)
cout << *it.base() << " "; // prints 'o' 't' 't' 'f'
// ["one", "two", "three", "four"]
auto r2 = s | views::lazy_split(' ');
for (auto it = r2.begin(); it != r2.end(); ++it)
cout << *it.base() << " "; // error, no 'base' member
As another example, chunk_view::iterator
provides a base()
, but slide_view::iterator
, which also belongs to windowing range adaptors, does not (godbolt):
array v = {1, 2, 3, 4, 5};
// [[1, 2], [3, 4], [5]]
auto r1 = v | views::chunk(2);
for (auto it = r1.begin(); it != r1.end(); ++it)
cout << *it.base() << " "; // prints 1 3 5
// [[1, 2], [2, 3], [3, 4], [4, 5]]
auto r2 = v | views::slide(2);
for (auto it = r2.begin(); it != r2.end(); ++it)
cout << *it.base() << " "; // error, no 'base' member
And for sentinel adaptors, there also seem to be inconsistencies: both filter_view/transform_view::sentinel
provide base()
, but join_view/split_view::sentienl
with the same layout do not.
So, what is the rationale for these iterator/sentinel adaptors that provide the base()
? I could not find any considerations behind this from historical documents, so I'm wondering what design philosophy the standard is based on.