Hi folks,

I recently plan to write proposals for views::drop_last(n) (and views::take_last(n)), which are labeled Tier 1 on P2760. The trickiest part is implementing drop_last_view::end() when dealing with only-forward-non-sized ranges.

In such a case, we need to calculate the end position. range/v3 uses two iterators to probe the end, similar to this:

template<forward_range R>
auto probe_last(R& r, range_difference_t<V> n) { 
  auto it = ranges::begin(r); 
  auto probe = ranges::next(it, n, ranges::end(r)); 
  for (; probe != ranges::end(r); ++probe) 
    ++it; 
  return it; 
}

We probe the end by advancing both iterators simultaneously. If the latter iterator reaches the end of the original range, the previous one is the end position of drop_last_view.

Therefore, the drop_last_view::begin()/end() can be implemented as follows (let's ignore other more optimized cases):

template<view V>
struct drop_last {
  V base_;
  range_difference_t<V> count_;

  auto begin() { return ranges::begin(base_); }

  auto end() {
    return probe_last(base_, count_); // O(N)
  }
};

However, this makes the first call to end() take O(N), where N is the size of the original range (because the probe iterator must reach the end). The advantage is that we can reuse this probe_last() help function when implementing take_last_view::begin().

Another way to implement drop_last_view::begin()/end() in the worst-case is to wrap the current iterator as well as the probe iterator with a custom iterator, then specialize its operator==:

template<view V>
struct drop_last {
  V base_;
  range_difference_t<V> count_;

  struct iterator {
    iterator_t<V> current_, probe_;

    // lots of iterator operations...
    iterator& operatpr++() {
      ++current_;
      ++probe_;
      return *this;
    }

    bool operatpr==(sentinel_t<V> end) const {
      return probe_ == end;
    }
  };

  auto begin() {
    auto it = ranges::begin(base_);
    auto probe = ranges::next(it, count_, ranges::end(base_)); // O(count_)
    return iterator{it, probe};
  }

  auto end() { return ranges::end(base_); }
};

This way seems to be more efficient than the previous one because the begin() call only takes O(count_) instead of O(N). The downside is that we basically introduce an iterator that behaves exactly the same as the original iterator, except for the operator== operator. This brings lots of repetition.

(Range/v3 adopts the second method, but thanks to its internal helper classes such as adapter_base, which can easily create such iterators with such custom behavior. However, there is no such thing in the standard currently, the closest one is P2727 std::iterator_interface).

I'm not sure if there is a better solution for this. I would like to know which method you think is more likely to be accepted by the standard?

Reference:

SO: What's the best way to `take_last(n)` a non-sized range?SO: What is the best way to drop last element using c++20 ranges

range/v3 drop_last_viewhttps://github.com/ericniebler/range-v3/blob/master/include/range/v3/view/drop_last.hpp