views::take_last and views::drop_last

Document #: P4294R0
Date: 2026-07-06
Project: Programming Language C++
Audience: SG9, LEWG, LWG
Reply-to: Hewill Kang <hewillk@gmail.com>

Contents

  1. Abstract
  2. Revision History
  3. Motivation
  4. Prior Art
  5. Design
    1. Concept requirements
    2. take_last: what begin() and end() return
    3. drop_last: what begin() and end() return
    4. Iterator design for drop_last (case 4)
    5. Caching for amortized O(1) begin()
    6. Why end() does not reuse begin()'s work
    7. Precondition on count
    8. size()
    9. reserve_hint()
    10. Reducing to views::drop / views::take
    11. Borrowed-range propagation
    12. Unbounded ranges
  6. Proposed Wording
  7. Implementation experience
  8. Feature-test macro
  9. References

1  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.

2  Revision History

R0

3  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.

4  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)

5  Design

5.1  Concept requirements

Both adaptors require:

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

Rationale:

5.2  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;

5.3  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.

5.4  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.

5.5  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):

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

5.6  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_.

5.7  Precondition on count

Following take_view / drop_view, both constructors add:

Preconditions: count >= 0 is true.

5.8  size()

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

5.9  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():

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.

5.10  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:

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.

5.11  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>;

5.12  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.

6  Proposed Wording

This wording is relative to N5046.

6.1  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; }
  […]
}

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

6.2.1  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:

[Example:

for (int i : views::iota(0, 10) | views::take_last(3))
  print("{} ", i);        // prints 7 8 9

— end example]

6.2.2  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:

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.

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

6.3.1  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:

[Example:

for (int i : views::iota(0, 10) | views::drop_last(3))
  print("{} ", i);        // prints 0 1 2 3 4 5 6

— end example]

6.3.2  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:

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:

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.

6.3.3  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:

The member typedef-name iterator_category is defined as follows:

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_).

7  Implementation experience

The author implemented views::take_last and views::drop_last based on libstdc++, see here.

8  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>

9  References