Search code examples
c++c++20std-rangesc++23

What is the rationale behind the iterator/sentinel of range adaptors providing the base() accessor?


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.


Solution

  • In some cases, I deliberately did not provide base() when doing so could leak implementation details (zip and adjacent - we don't want to mandate any particular form of iterator storage) and/or be confusing (chunk_view::outer-iterator - base() will change while you iterate over the chunk, because that's the only way to implement this for input iterators).

    lazy_split's outer-iterator - at least for input ranges - would have the same problem as chunk.

    I probably didn't provide base() for slide because I didn't provide it for adjacent, but it should be fine to add it.

    There's nothing meaningful you can do with a join_view's sentinel's wrapped sentinel (the iterator doesn't provide base() - and for good reasons), so there's no motivation to provide access to it. It's probably reasonable to provide base() for split's sentinel though, since we provided one for its iterator.