4

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.

康桓瑋
  • 33,481
  • 5
  • 40
  • 90

1 Answers1

3

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.

T.C.
  • 133,968
  • 17
  • 288
  • 421
  • Thanks for the enlightening explanation. For `adjacent_view::iterator`, is it reasonable to provide a `base()` that returns `array.front()`? Although compared to `spill_view`, I can't really think of a use case for `slide`/`adjacent`'s outer-iterator to provide such utility. – 康桓瑋 Jan 04 '23 at 09:50
  • That leaks an implementation detail for non-bidirectional common ranges (you can have `i == j` but `i.base() != j.base()`). – T.C. Jan 04 '23 at 20:14
  • That would be confusing. But does this mean that `slide_view` will have the same similar issue? According to its implementation details, if we provide `base()` for its iterator, for `list`, `r.end().base()` points to a valid element, and for `forward_list`, `r.end().base ()` refers to its actual `end()`, which seems confusing. It seems that only `split_view::sentinel/chunk_by_view::iterator` is worth adding `base()`, is that correct? – 康桓瑋 Jan 05 '23 at 03:07