---
title: "views::take_last and views::drop_last"
document: P4294R0
date: 2026-07-06
audience: SG9, LEWG, LWG
reply-to:
  - "Hewill Kang <hewillk@gmail.com>"
---



## Abstract

This paper proposes two new range adaptors, `views::take_last` and `views::drop_last`, that respectively produce the last *N* elements of a range and all-but-the-last *N* elements of a range. They mirror the shape of the existing `views::take` / `views::drop` adaptors and fill an obvious gap in the standard range adaptor set.

## Revision History

### R0

- Initial revision.

## Motivation

C++20 introduced `views::take` and `views::drop` to select or discard a *prefix* of a range. There is currently no direct adaptor that operates on the *suffix* of a range. Users have to fall back to composition:

```
r | views::reverse | views::take(n) | views::reverse   // last n elements
r | views::reverse | views::drop(n) | views::reverse   // drop last n
```

which:

1. Requires `bidirectional_range` (excludes forward-only sized ranges).
2. Reverses the traversal direction and may obscure the original access pattern, pessimizing algorithms or implementations that would otherwise operate directly on the original range.
3. Is verbose and obscures intent.

For an input that is a `sized_range` but not `bidirectional_range` (for example a user-defined sized forward range, or a sized forward range synthesized by an adaptor pipeline), the reverse-based workaround simply does not compile.

Both operations are well-established in range-v3 (`views::take_last`, `views::drop_last`), in Python (`r[-n:]`, `r[:-n]`), in Kotlin (`takeLast`, `dropLast`), etc.

## Prior Art

| Library / Language | `take_last` | `drop_last` |
| --- | --- | --- |
| range-v3 | `views::take_last` | `views::drop_last` |
| Python | `seq[-n:]` | `seq[:-n]` |
| Kotlin / Scala | `takeLast(n)` | `dropLast(n)` |
| D (Phobos) | `takeLast(n)` | `dropBackN(n)` |

## Design

### Concept requirements

Both adaptors require:

```
template<view V>
  requires forward_range<V> || sized_range<V>
```

Rationale:

- If `V` is `sized_range` we know the size *up front* and can jump to the correct offset (`take_last`) or compute the correct end (`drop_last`) directly.
- If `V` is only `forward_range` we can still make it work with a two-iterator "probe" technique that traverses the range once, provided we are allowed to re-visit elements (i.e. `forward_iterator`).
- An input-only, non-sized range provides neither, and cannot support the operation without buffering — we explicitly do not want that.

### take_last: what begin() and end() return

`take_last_view` never introduces a new iterator type; it always yields the underlying `iterator_t<V>` (or `iterator_t<const V>`). The interesting question is *how* `begin()` is computed. Five cases are handled, listed from cheapest to most expensive:

| # | Category of `V` | `begin()` | `end()` | Complexity of `begin()` |
| --- | --- | --- | --- | --- |
| 1 | `sized_range` && `random_access_range` | `begin(b) + (size − min(size,count))` | `end(b)` | O(1) |
| 2 | `sized_range` && `bidirectional_range` && `common_range` | pick the shorter of the two walks | `end(b)` | O(min(count, size−count)) |
| 3 | `sized_range` (not in 1 or 2) | `next(begin(b), size − count)` | `end(b)` | O(size − count) |
| 4 | `bidirectional_range` && `common_range` (not sized) | `prev(end(b), count, begin(b))` | `end(b)` | O(count) |
| 5 | Otherwise (forward, not sized, not common-bidi) | two-iterator probe | `end(b)` | O(size) |

Case 2 is a nice runtime optimisation: with `n = min(size, count)` and `from_begin = size − n`, we compare `n` and `from_begin` and walk from whichever end is closer. The cost becomes O(min(n, from_begin)).

This optimization is intentionally not reflected as a separate normative case in the wording. The wording specifies the iterator that `begin()` returns, not the particular path used to compute it. For a sized bidirectional common range, an implementation can compute the required iterator either by advancing from `begin(base_)` or by retreating from `end(base_)`, whichever is cheaper, while still returning the iterator denoted by `ranges::next(ranges::begin(base_), size - n)`.

Case 5 uses the classical "runner" trick:

```
auto it    = ranges::begin(base_);
auto probe = ranges::next(it, count_, ranges::end(base_));
while (probe != ranges::end(base_)) {
    ++it;
    ++probe;
}
return it;
```

### drop_last: what begin() and end() return

Unlike `take_last`, `drop_last` genuinely needs a new iterator type in the worst case, because the "logical end" of the range is *count* steps before the physical end and we may not be able to represent that with the underlying sentinel. Four cases:

| # | Category of `V` | `begin()` | `end()` | Iterator returned |
| --- | --- | --- | --- | --- |
| 1 | `sized_range` && `random_access_range` | `begin(b)` | `begin(b) + (size − n)` | `iterator_t<V>` |
| 2 | `sized_range` (not random-access) | `counted_iterator(begin(b), size − n)` | `default_sentinel` | `counted_iterator<iterator_t<V>>` |
| 3 | `bidirectional_range` && `common_range` (not sized) | `begin(b)` | `prev(end(b), count, begin(b))` | `iterator_t<V>` |
| 4 | Otherwise (forward, not sized, not common-bidi) | *iterator*`(begin(b), next(begin(b), count, end(b)))` | `end(b)` (a `sentinel_t<V>`) | *iterator* |

Cases 1 and 3 reuse `iterator_t<V>` verbatim. Case 2 uses `counted_iterator` so that `end()` can be `default_sentinel`, avoiding an O(size) walk in `end()`. Only case 4 requires a bespoke iterator; see the next section.

### Iterator design for drop_last (case 4)

The iterator carries two copies of `iterator_t<V>`:

```
iterator_t<V> current_;    // logical position
iterator_t<V> probe_;      // current_ advanced by count_ (clamped to end)
```

Both advance together on every operation. When `probe_` reaches the underlying sentinel, iteration is complete:

```
friend constexpr bool
operator==(const iterator& x, const sentinel_t<V>& y)  { return x.probe_ == y; }
```

The bespoke iterator intentionally does not provide subtraction with `sentinel_t<V>`. This iterator is only used in the fallback case where `V` is forward but not sized, and where the logical end cannot be represented directly by an iterator into the underlying range. If `sentinel_t<V>` and `iterator_t<V>` were sized sentinels for each other, then the underlying forward range would already be a `sized_range`, and the adaptor would use the sized-range case instead of this iterator.

When the underlying iterator supports stronger iterator concepts, the wrapper preserves those operations by applying the same movement to both `current_` and `probe_`. This maintains the invariant that `probe_` denotes the position corresponding to `current_` advanced by `count_`, and allows the fallback iterator to preserve bidirectional, random-access, and contiguous capabilities of the underlying iterator where available.

### Caching for amortized O(1) begin()

Per *[range.range]*, `ranges::begin(r)` must be *amortized* O(1). This matters for the cases where the naive `begin()` is not O(1):

- `take_last` cases 3, 4, 5.
- `drop_last` case 4.
- `drop_last` case 3 has an expensive `end()` (not `begin()`), which we cache symmetrically so that repeated iteration does not repeatedly walk backwards.

The specification uses the standard "cache the value on the first call and return it unchanged on subsequent calls" formulation.

### Why end() does not reuse begin()'s work

An attractive optimisation for `take_last` case 5 is to observe that at the end of the two-iterator loop, `probe_` *equals* `ranges::end(base_)` already, and could conceivably be returned by `end()` to avoid recomputing it. We deliberately reject this for two reasons:

1. `end()` may be called *before* `begin()`. Reusing the cache would then force `end()` to run the O(size) walk, which is exactly the cost we are trying to avoid, and would silently pessimise algorithms that only ever query `end()` (`ranges::empty`, comparisons against a stored sentinel, etc.).
2. `ranges::end(base_)` is itself O(1); nothing is gained by caching it.

The same reasoning applies to `drop_last` case 4: even though `begin()`'s computation produces the correct sentinel-equivalent iterator, `end()` still returns the underlying `sentinel_t<V>` directly, and the comparison uses `probe_`.

### Precondition on count

Following `take_view` / `drop_view`, both constructors add:

> Preconditions:
> 
> count >= 0
> 
> is
> 
> true
> 
> .

### size()

- `take_last`: `min(size(base_), count_)` — always ≤ `count_`.
- `drop_last`: `size(base_) > count_ ? size(base_) - count_ : 0` — never negative.

Both are provided on the `const` overload iff `sized_range<const V>`.

### reserve_hint()

`take_last_view` always provides `reserve_hint()`, because its cardinality is bounded above by `count_`. `drop_last_view` provides `reserve_hint()` only when the base range is approximately-sized. This lets `ranges::to<C>` allocate the right capacity even when the exact size cannot be obtained in O(1). The rules generalise `size()`:

- `take_last_view::reserve_hint()` returns `min(ranges::reserve_hint(base_), count_)` when the base range is approximately-sized, and `count_` otherwise.
- `drop_last_view::reserve_hint()` returns `max(ranges::reserve_hint(base_) - count_, 0)`.

When `V` also models `sized_range` (which refines `approximately_sized_range`), these coincide with `size()` and yield the exact count. When `V` is only approximately-sized (e.g. an `istream_view` with a hint attached), the returned value is a best-effort estimate, exactly as intended by `ranges::reserve_hint`.

### Reducing to views::drop / views::take

For a wide range of well-known random-access-sized view types, both adaptors have simple closed-form reductions:

- `views::take_last(E, F)` ≡ `E | views::drop(distance(E) - min(distance(E), F))`
- `views::drop_last(E, F)` ≡ `E | views::take(max(distance(E) - F, 0))`

For types where `views::drop` / `views::take` are themselves specialised to return the *same type* (i.e. `empty_view`, `span`, `basic_string_view`, `optional`, `repeat_view`, `iota_view`, `subrange`), this reduction produces the same-typed result *without* introducing a wrapper `take_last_view` / `drop_last_view`.

### Borrowed-range propagation

Both views are borrowed iff `V` is:

```
template<class T>
constexpr bool enable_borrowed_range<take_last_view<T>> = enable_borrowed_range<T>;
template<class T>
constexpr bool enable_borrowed_range<drop_last_view<T>> = enable_borrowed_range<T>;
```

### Unbounded ranges

These adaptors are suffix operations and therefore are meaningful for ranges with a reachable end. The constraints cannot, in general, express finiteness. For a non-sized forward range, `take_last_view::begin()` must reach the physical end of the base range, and therefore might not terminate for an unbounded range. Similarly, `drop_last_view` on an unbounded range has no mathematical “last N elements” to remove.

This is also why the closed-form reductions for well-known view types are restricted to cases where the relevant distance is known. For example, `repeat_view` can be either bounded or unbounded; only the bounded form models `sized_range`. Therefore the reductions involving `repeat_view` are intentionally limited to the sized case.

## Proposed Wording

This wording is relative to [N5046](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2026/n5046.pdf).

### Change to 24.2 [ranges.syn]

```
namespace std::ranges {
  […]
  namespace views { inline constexpr unspecified drop_while = unspecified; }

  // [range.take.last], take last view
  template<view V>
    requires forward_range<V> || sized_range<V>
  class take_last_view;

  template<class T>
    constexpr bool enable_borrowed_range<take_last_view<T>> =
      enable_borrowed_range<T>;

  namespace views { inline constexpr unspecified take_last = unspecified; }

  // [range.drop.last], drop last view
  template<view V>
    requires forward_range<V> || sized_range<V>
  class drop_last_view;

  template<class T>
    constexpr bool enable_borrowed_range<drop_last_view<T>> =
      enable_borrowed_range<T>;

  namespace views { inline constexpr unspecified drop_last = unspecified; }
  […]
}
```

### Add 25.7.? Take last view [range.take.last] after 25.7.13 [range.drop.while] as indicated:

#### Overview [range.take.last.overview]

> `take_last_view` produces a view of the last *N* elements of another view.
> 
> The name `views::take_last` denotes a range adaptor object (*[range.adaptor.object]*). Let `E` and `F` be expressions, let `T` be `remove_cvref_t<decltype((E))>`, and let `D` be `range_difference_t<decltype((E))>`. If `decltype((F))` does not model `convertible_to<D>`, `views::take_last(E, F)` is ill-formed. Otherwise, the expression `views::take_last(E, F)` is expression-equivalent to:
> 
> - *Preconditions:* `static_cast<D>(F) >= 0` is `true`.
> - If `T` is a specialization of `empty_view`, `span` or `basic_string_view`, or `T` models `view` and is a specialization of `optional`, or `T` models `random_access_range` and `sized_range` and is a specialization of `repeat_view`, `iota_view`, or `subrange`, then `E | views::drop(ranges::distance(E) - std::min(ranges::distance(E), static_cast<D>(F)))`, except that `E` is evaluated only once.
> - Otherwise, `take_last_view(E, F)`.
> 
> *[Example:*
> 
> ```
> for (int i : views::iota(0, 10) | views::take_last(3))
>   print("{} ", i);        // prints 7 8 9
> ```
> 
> *— end example]*

#### Class template take_last_view [range.take.last.view]

```
namespace std::ranges {
  template<view V>
    requires forward_range<V> || sized_range<V>
  class take_last_view : public view_interface<take_last_view<V>> {
    V base_ = V();                     // exposition only
    range_difference_t<V> count_ = 0;  // exposition only

  public:
    take_last_view() requires default_initializable<V> = default;
    constexpr explicit take_last_view(V base, range_difference_t<V> count);

    constexpr V base() const & requires copy_constructible<V> { return base_; }
    constexpr V base() && { return std::move(base_); }

    constexpr auto begin()
      requires (!(simple-view<V> &&
                  random_access_range<const V> && sized_range<const V>));

    constexpr auto begin() const
      requires random_access_range<const V> && sized_range<const V> {
      const auto sz = ranges::distance(base_);
      const auto n = std::min(sz, count_);
      return ranges::next(ranges::begin(base_), sz - n);
    }

    constexpr auto end() { return ranges::end(base_); }
    constexpr auto end() const requires range<const V> { return ranges::end(base_); }

    constexpr auto size() requires sized_range<V> {
      const auto sz = ranges::size(base_);
      return std::min(sz, static_cast<decltype(sz)>(count_));
    }

    constexpr auto size() const requires sized_range<const V> {
      const auto sz = ranges::size(base_);
      return std::min(sz, static_cast<decltype(sz)>(count_));
    }

    constexpr auto reserve_hint() {
      if constexpr (approximately_sized_range<V>) {
        auto sz = static_cast<range_difference_t<V>>(ranges::reserve_hint(base_));
        return to-unsigned-like(std::min(sz, count_));
      }
      return to-unsigned-like(count_);
    }

    constexpr auto reserve_hint() const {
      if constexpr (approximately_sized_range<const V>) {
        auto sz = static_cast<range_difference_t<const V>>(ranges::reserve_hint(base_));
        return to-unsigned-like(std::min(sz, count_));
      }
      return to-unsigned-like(count_);
    }
  };

  template<class R>
  take_last_view(R&&, range_difference_t<R>) -> take_last_view<views::all_t<R>>;
}
```

```
constexpr explicit take_last_view(V base, range_difference_t<V> count);
```

> *Preconditions:* `count >= 0` is `true`.
> 
> *Effects:* Initializes `*base_*` with `std::move(base)` and `*count_*` with `count`.

```
constexpr auto begin()
  requires (!(simple-view<V> &&
              random_access_range<const V> && sized_range<const V>));
```

> *Returns:*
> 
> - If `V` models `sized_range`, let `sz` be `ranges::distance(*base_*)` and `n` be `std::min(sz, *count_*)`, `ranges::next(ranges::begin(*base_*), sz - n)`.
> - Otherwise, if `V` models `bidirectional_range` and `common_range`, `ranges::prev(ranges::end(*base_*), *count_*, ranges::begin(*base_*))`.
> - Otherwise, the first iterator `i` reachable from `ranges::begin(*base_*)` such that `bool(ranges::next(i, *count_*, ranges::end(*base_*)) == ranges::end(*base_*))` is `true`.
> 
> *Remarks:* In order to provide the amortized constant-time complexity required by the `range` concept when `take_last_view` models `forward_range`, this function caches the result within the `take_last_view` for use on subsequent calls.

### Add 25.7.? Drop last view [range.drop.last] after [range.take.last] as indicated:

#### Overview [range.drop.last.overview]

> `drop_last_view` produces a view of a range with its last *N* elements removed.
> 
> The name `views::drop_last` denotes a range adaptor object (*[range.adaptor.object]*). Let `E` and `F` be expressions, let `T` be `remove_cvref_t<decltype((E))>`, and let `D` be `range_difference_t<decltype((E))>`. If `decltype((F))` does not model `convertible_to<D>`, `views::drop_last(E, F)` is ill-formed. Otherwise, the expression `views::drop_last(E, F)` is expression-equivalent to:
> 
> - *Preconditions:* `static_cast<D>(F) >= 0` is `true`.
> - If `T` is a specialization of `empty_view`, `span` or `basic_string_view`, or `T` models `view` and is a specialization of `optional`, or `T` models `random_access_range` and `sized_range` and is a specialization of `repeat_view`, `iota_view`, or `subrange`, then `E | views::take(std::max(ranges::distance(E) - static_cast<D>(F), D(0)))`, except that `E` is evaluated only once.
> - Otherwise, `drop_last_view(E, F)`.
> 
> *[Example:*
> 
> ```
> for (int i : views::iota(0, 10) | views::drop_last(3))
>   print("{} ", i);        // prints 0 1 2 3 4 5 6
> ```
> 
> *— end example]*

#### Class template drop_last_view [range.drop.last.view]

```
namespace std::ranges {
  template<view V>
    requires forward_range<V> || sized_range<V>
  class drop_last_view : public view_interface<drop_last_view<V>> {
    V base_ = V();                     // exposition only
    range_difference_t<V> count_ = 0;  // exposition only

    // 24.7.?.3, class drop_last_view::iterator
    class iterator;                    // exposition only

  public:
    drop_last_view() requires default_initializable<V> = default;
    constexpr explicit drop_last_view(V base, range_difference_t<V> count);

    constexpr V base() const & requires copy_constructible<V> { return base_; }
    constexpr V base() && { return std::move(base_); }

    constexpr auto begin()
      requires (!(simple-view<V> && sized_range<const V>));

    constexpr auto begin() const requires sized_range<const V> {
      if constexpr (random_access_range<const V>)
        return ranges::begin(base_);
      else {
        const auto sz = ranges::distance(base_);
        const auto n = std::min(sz, count_);
        return counted_iterator(ranges::begin(base_), sz - n);
      }
    }

    constexpr auto end()
      requires (!(simple-view<V> && sized_range<const V>));

    constexpr auto end() const requires sized_range<const V> {
      if constexpr (random_access_range<const V>) {
        const auto sz = ranges::distance(base_);
        const auto n = std::min(sz, count_);
        return ranges::next(ranges::begin(base_), sz - n);
      } else
        return default_sentinel;
    }

    constexpr auto size() requires sized_range<V> {
      const auto s = ranges::size(base_);
      const auto c = static_cast<decltype(s)>(count_);
      return s < c ? 0 : s - c;
    }
  
    constexpr auto size() const requires sized_range<const V> {
      const auto s = ranges::size(base_);
      const auto c = static_cast<decltype(s)>(count_);
      return s < c ? 0 : s - c;
    }

    constexpr auto reserve_hint() requires approximately_sized_range<V> {
      const auto s = static_cast<range_difference_t<V>>(ranges::reserve_hint(base_));
      return to-unsigned-like(s < count_ ? 0 : s - count_);
    }

    constexpr auto reserve_hint() const requires approximately_sized_range<const V> {
      const auto s = static_cast<range_difference_t<const V>>(ranges::reserve_hint(base_));
      return to-unsigned-like(s < count_ ? 0 : s - count_);
    }
  };

  template<class R>
  drop_last_view(R&&, range_difference_t<R>) -> drop_last_view<views::all_t<R>>;
}
```

```
constexpr explicit drop_last_view(V base, range_difference_t<V> count);
```

> *Preconditions:* `count >= 0` is `true`.
> 
> *Effects:* Initializes `*base_*` with `std::move(base)` and `*count_*` with `count`.

```
constexpr auto begin()
  requires (!(simple-view<V> && sized_range<const V>));
```

> *Returns:*
> 
> - If `V` models `sized_range`, let `sz = ranges::distance(*base_*)` and `n = std::min(sz, *count_*)`.
>   - If `V` models `random_access_range`, `ranges::begin(*base_*)`.
>   - Otherwise, `counted_iterator(ranges::begin(*base_*), sz - n)`.
> - Otherwise, if `V` models `bidirectional_range` and `common_range`, `ranges::begin(*base_*)`.
> - Otherwise, `*iterator*(ranges::begin(*base_*), ranges::next(ranges::begin(*base_*), *count_*, ranges::end(*base_*)))`.
> 
> *Remarks:* In order to provide the amortized constant-time complexity required by the `range` concept when `drop_last_view` models `forward_range`, this function caches the result within the `drop_last_view` for use on subsequent calls.

```
constexpr auto end()
  requires (!(simple-view<V> && sized_range<const V>));
```

> *Returns:*
> 
> - If `V` models `sized_range`, let `sz = ranges::distance(*base_*)` and `n = std::min(sz, *count_*)`.
>   - If `V` models `random_access_range`, `ranges::next(ranges::begin(*base_*), sz - n)`.
>   - Otherwise, `default_sentinel`.
> - Otherwise, if `V` models `bidirectional_range` and `common_range`, `ranges::prev(ranges::end(*base_*), *count_*, ranges::begin(*base_*))`.
> - Otherwise, `ranges::end(*base_*)`.
> 
> *Remarks:* In order to provide the amortized constant-time complexity required by the `range` concept when `drop_last_view` models `forward_range`, this function caches the result within the `drop_last_view` for use on subsequent calls.

#### Class drop_last_view::*iterator* [range.drop.last.iterator]

```
namespace std::ranges {

template<view V>
  requires forward_range<V> || sized_range<V>
class drop_last_view<V>::iterator {
  iterator_t<V> current_ = iterator_t<V>();                       // exposition only
  iterator_t<V> probe_   = iterator_t<V>();                       // exposition only

  constexpr iterator(iterator_t<V> current, iterator_t<V> probe);   // exposition only
public:
  using iterator_concept  = see below;
  using iterator_category = see below;
  using value_type        = range_value_t<V>;
  using difference_type   = range_difference_t<V>;

  iterator() = default;
  constexpr iterator_t<V> base() const;

  constexpr range_reference_t<V> operator*() const;
  constexpr auto operator->() const noexcept requires contiguous_range<V>;

  constexpr iterator& operator++();
  constexpr iterator operator++(int) = default;

  constexpr iterator& operator--() requires bidirectional_range<V>;
  constexpr iterator operator--(int) requires bidirectional_range<V> = default;

  constexpr iterator& operator+=(difference_type n) requires random_access_range<V>;
  constexpr iterator& operator-=(difference_type n) requires random_access_range<V>;

  constexpr range_reference_t<V> operator[](difference_type n) const
    requires random_access_range<V>;

  friend constexpr bool operator==(const iterator& x, const iterator& y);
  friend constexpr bool operator==(const iterator& x, const sentinel_t<V>& y);

  friend constexpr bool operator<(const iterator& x, const iterator& y)
    requires random_access_range<V>;
  friend constexpr bool operator>(const iterator& x, const iterator& y)
    requires random_access_range<V>;
  friend constexpr bool operator<=(const iterator& x, const iterator& y)
    requires random_access_range<V>;
  friend constexpr bool operator>=(const iterator& x, const iterator& y)
    requires random_access_range<V>;
  friend constexpr auto operator<=>(const iterator& x, const iterator& y)
    requires random_access_range<V> && three_way_comparable<iterator_t<V>>;

  friend constexpr iterator operator+(const iterator& x, difference_type n)
    requires random_access_range<V>;
  friend constexpr iterator operator+(difference_type n, const iterator& x)
    requires random_access_range<V>;
  friend constexpr iterator operator-(const iterator& x, difference_type n)
    requires random_access_range<V>;
  friend constexpr difference_type operator-(const iterator& x, const iterator& y)
    requires sized_sentinel_for<iterator_t<V>, iterator_t<V>>;

  friend constexpr decltype(auto) iter_move(const iterator& i)
    noexcept(noexcept(ranges::iter_move(i.current_)));

  friend constexpr void iter_swap(const iterator& x, const iterator& y)
    noexcept(noexcept(ranges::iter_swap(x.current_, y.current_)))
    requires indirectly_swappable<iterator_t<V>>;
};

}
```

The member *typedef-name* `iterator_concept` is defined as follows:

- If `V` models `contiguous_range`, then `iterator_concept` denotes `contiguous_iterator_tag`.
- Otherwise if `V` models `random_access_range`, then `iterator_concept` denotes `random_access_iterator_tag`.
- Otherwise, if `V` models `bidirectional_range`, then `iterator_concept` denotes `bidirectional_iterator_tag`.
- Otherwise, `iterator_concept` denotes `forward_iterator_tag`.

The member *typedef-name* `iterator_category` is defined as follows:

- Let `C` be `iterator_traits<iterator_t<V>>::iterator_category`.
- If `C` models `derived_from<random_access_iterator_tag>`, then `iterator_category` denotes `random_access_iterator_tag`.
- Otherwise, if `C` models `derived_from<bidirectional_iterator_tag>`, then `iterator_category` denotes `bidirectional_iterator_tag`.
- Otherwise, `iterator_category` denotes `C`.

```
constexpr iterator(iterator_t<V> current, iterator_t<V> probe);
```

> *Effects:* Initializes `*current_*` with `current` and `*probe_*` with `probe`.

```
constexpr iterator_t<V> base() const;
```

> *Effects:* Equivalent to: `return *current_*;`

```
constexpr range_reference_t<V> operator*() const;
```

> *Effects:* Equivalent to: `return **current_*;`

```
constexpr auto operator->() const noexcept requires contiguous_range<V>;
```

> *Effects:* Equivalent to: `return to_address(*current_*);`

```
constexpr iterator& operator++();
```

> *Effects:* Equivalent to:
> 
> ```
> ++current_;
> ++probe_;
> return *this;
> ```

```
constexpr iterator& operator--() requires bidirectional_range<V>;
```

> *Effects:* Equivalent to:
> 
> ```
> --current_;
> --probe_;
> return *this;
> ```

```
constexpr iterator& operator+=(difference_type n) requires random_access_range<V>;
```

> *Effects:* Equivalent to:
> 
> ```
> current_ += n;
> probe_   += n;
> return *this;
> ```

```
constexpr iterator& operator-=(difference_type n) requires random_access_range<V>;
```

> *Effects:* Equivalent to:
> 
> ```
> current_ -= n;
> probe_   -= n;
> return *this;
> ```

```
constexpr range_reference_t<V> operator[](difference_type n) const
  requires random_access_range<V>;
```

> *Effects:* Equivalent to: `return *current_*[n];`

```
friend constexpr bool operator==(const iterator& x, const iterator& y);
```

> *Returns:* `x.*current_* == y.*current_*`.

```
friend constexpr bool operator==(const iterator& x, const sentinel_t<V>& y);
```

> *Returns:* `x.*probe_* == y`.

```
friend constexpr bool operator<(const iterator& x, const iterator& y)
  requires random_access_range<V>;
friend constexpr bool operator>(const iterator& x, const iterator& y)
  requires random_access_range<V>;
friend constexpr bool operator<=(const iterator& x, const iterator& y)
  requires random_access_range<V>;
friend constexpr bool operator>=(const iterator& x, const iterator& y)
  requires random_access_range<V>;
friend constexpr auto operator<=>(const iterator& x, const iterator& y)
  requires random_access_range<V> && three_way_comparable<iterator_t<V>>;
```

> Let *op* be the operator.
> 
> *Effects:* Equivalent to: `return x.*current_* *op* y.*current_*;`

```
friend constexpr iterator operator+(const iterator& x, difference_type n)
  requires random_access_range<V>;
friend constexpr iterator operator+(difference_type n, const iterator& x)
  requires random_access_range<V>;
```

> *Effects:* Equivalent to:
> 
> ```
> auto tmp = x;
> tmp += n;
> return tmp;
> ```

```
friend constexpr iterator operator-(const iterator& x, difference_type n)
  requires random_access_range<V>;
```

> *Effects:* Equivalent to:
> 
> ```
> auto tmp = x;
> tmp -= n;
> return tmp;
> ```

```
friend constexpr difference_type operator-(const iterator& x, const iterator& y)
  requires sized_sentinel_for<iterator_t<V>, iterator_t<V>>;
```

> *Returns:* `x.*current_* - y.*current_*`.

```
friend constexpr decltype(auto) iter_move(const iterator& i)
  noexcept(noexcept(ranges::iter_move(i.current_)));
```

> *Effects:* Equivalent to: `return ranges::iter_move(i.*current_*);`

```
friend constexpr void iter_swap(const iterator& x, const iterator& y)
  noexcept(noexcept(ranges::iter_swap(x.current_, y.current_)))
  requires indirectly_swappable<iterator_t<V>>;
```

> *Effects:* Equivalent to `ranges::iter_swap(x.*current_*, y.*current_*)`.

## Implementation experience

The author implemented `views::take_last` and `views::drop_last` based on libstdc++, see [here](https://godbolt.org/z/5aPn1GMnx).

## Feature-test macro

Add to *[version.syn]*:

```
#define __cpp_lib_ranges_take_last 20XXXXL  // freestanding, also in <ranges>
#define __cpp_lib_ranges_drop_last 20XXXXL  // freestanding, also in <ranges>
```

## References

- [ A Plan for C++26 Ranges] Barry Revzin, [https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2023/p2760r1.html](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2023/p2760r1.html)
- [range-v3] Eric Niebler, *range-v3*, [https://github.com/ericniebler/range-v3](https://github.com/ericniebler/range-v3).
