Document number P3216R4
Date 2026-7-26
Audience LEWG, SG9 (Ranges)
Reply-to Hewill Kang <hewillk@gmail.com>

views::slice

Abstract

This paper proposes the Tier 1 adaptor views::slice (as described in P2760) to enhance the C++29 ranges library. Notably, this is the first standard range adaptor that accepts two arguments — start and end — to specify the interval [start, end) for slicing a range.

Revision history

R0

Initial revision.

R1

Introduce new slice_view class based on feedback from SG9 St. Louis.

R2

Apply specialization for optional based on feedback from SG9 Kona.

R3

Provide const begin() member when the underlying range is not a forward_range.

R4

Remove slice_view and use assembly design instead.

Provides the existing practice on GitHub.

Discussion

Slicing — a means of extracting a contiguous subrange from a sequence by specifying a start and end index — is a fundamental operation in modern programming. Many mainstream languages, such as Python and Rust, offer built-in slice syntax, making it a familiar and expected feature for developers. In the C++ ecosystem, while the Ranges library has greatly enhanced composability and expressiveness, it currently lacks a direct, ergonomic, and standard way to perform slicing by index.

Why views::slice is needed

Given the above, the introduction of views::slice fills a clear gap in the C++ Ranges library, providing a direct, expressive, and interoperable way to extract subranges by index. It aligns C++ with industry standards, improves code readability, and empowers developers to write more concise and correct range-based code. For these reasons, views::slice is a valuable and timely addition to C++29:

  string_view text = "Hello, world!";
  auto sub1 = text | views::slice(7, 12);  // "world"

  vector v = {1, 2, 3, 4, 5};
  auto sub3 = v | views::slice(1, 10);     // [2, 3, 4, 5]
  auto sub2 = v | views::slice(2, 2);      // empty range
  auto sub4 = v | views::slice(5, 10);     // empty range

Note that a search for views::slice on GitHub already yields a huge number of use cases, not to mention different namespaces and aliases (thanks to Inbal Levi for providing the link).

Design

The second argument should be end instead of size

An alternative design for slice is to accept a starting index and a size. However, the author prefers to use the end index as the second parameter as this is intuitive and consistent with other language syntaxes:

Language Syntax Stride Support Negative Indices Out-of-Bounds Behavior Notes
Python a[start:end:step] ✅ Yes ✅ Yes ✅ Truncated
Rust &a[start..end] ❌ No ❌ No ❌ Panics Need to use .iter().step_by(n) to stride
Go a[start:end] ❌ No ❌ No ❌ Panics a[start:end:max] controls capacity, not stride
JavaScript a.slice(start, end) ❌ No ✅ Yes ✅ Truncated
Ruby a[start, length] or a[start..end] ❌ No ✅ Yes ✅ Truncated

This is the de facto standard for slicing.

No need a dedicated slice_view class

In R0, the author simply proposed that slice(M, N) is equivalent to views::drop(M) | views::take(N - M) instead of introducing a new slice_view.

The main reason is that views::slice involves advancing to the beginning of the slice and calculating the end, which perfectly matches what views::drop and views::take are currently doing, also as stated in P2214:

"slice(M, N) is equivalent to views::drop(M) | views::take(N - M), and you couldn't do much better as a first class view. range-v3 also supports a flavor that works as views::slice(M, end - N) for a special variable end, which likewise be equivalent to r | views::drop(M) | views::drop_last(N)."

However, at St. Louis 2024, SG9 preferred a new, dedicated view type, slice_view, primarily for two reasons:

  • 1. Most of the room favored a new view because it is so fundamental and common in other languages.
  • 2. range/v3 does this.
  • In later versions such as R3, the author introduced a new slice_view class. However, when implementing the new class, the author didn't find any noteworthy optimizations to mention. All the logic is the same as with drop_view and take_view: find the starting point first, then locate the ending point. And the new slice_view::sentinel class looks exactly the same as take_view::sentinel.

    Tim also pointed out that the discussion on "Motivation for a dedicated slice_view class" section was incorrect. For example, assembling views::drop/take also unconditionally provides reserve_hint(), because take_view unconditionally provides reserve_hint() even in the case of non-approximately-sized.

    Since the author doesn't see any real, decisive benefit in using first-class design, we believe that using simple assembly design is a better option after exploring first-class design.

    It's worth noting that the revert to assembly design leads views::slice(r, M, N).base() to return take_view instead of original range, but the authors believe the impact is still insufficient to justify introducing the new class.

    Handling of out-of-bounds

    In range/v3, views::slice is implemented with a dedicated view class, but it does not perform any boundary checking. It always assumes that the provided start and end indices are within valid bounds of the underlying range:

      auto ints  = {1, 2, 3, 4, 5};
      auto slice = ints | ranges::v3::views::slice(3, 9);
      std::println("{}", slice); // prints [4, 5, 0, 0, 0, 2147483647]

    This design makes its views::slice effectively an unchecked version of slicing. If the indices are out of range, the behavior is undefined, potentially leading to runtime errors or undefined behavior. From a naming perspective, the range/v3 version would be more accurately described as views::unchecked_slice or views::slice_exactly, reflecting its lack of safety checks.

    In contrast, the proposed views::slice includes comprehensive boundary checking just like views::take and views::drop as it is an assembly of the latter two; it will safely adjust or clamp the specified indices as appropriate, ensuring well-defined and predictable behavior.

    Special variable end is not support

    In range-v3, the special variable end is supported in views::slice, allowing users to write expressions like views::slice(M, end - N) to indicate slicing from index M up to N elements before the end of the range.

    While this can be expressive in certain scenarios, the author believes it is unnecessary and potentially problematic for several reasons.

    First, introducing a special variable such as end can make the syntax less clear and more confusing, especially for users who expect a straightforward two-index slicing interface similar to what is found in other mainstream languages. This added complexity may hinder readability and increase the learning curve for new users.

    Second, and more importantly, range-v3's implementation does not perform boundary checking for the end. It assumes that the indices provided are always valid, which is fundamentally different from our proposed design. Supporting end-based expressions in a boundary-checked implementation introduces challenges, particularly for input ranges, since evaluating something like end - N would require traversing the entire range, which is infeasible for single-pass input ranges.

    In summary, while the end variable enables some expressive patterns, it complicates the interface and is incompatible with a robust, boundary-checked design. For these reasons, the author does not adopting this feature in the proposal.

    Stride overload is not provided

    As described in the table above, Python also allows an optional stride (step) parameter, its slice syntax [start:end:step] enables users to select every nth element or even reverse the sequence by specifying a negative stride.

    However, JavaScript, Go, and many other languages with slicing capabilities (such as Ruby, Swift, or Kotlin) do not include stride as part of their native slice syntax; instead, users must use separate functions or methods to achieve similar effects. Rust's standard slice syntax does not support stride directly; users must use iterators like .iter().step_by(n) to achieve striding.

    This supports the case against overloading views::slice with a stride parameter in C++, which, already provides a clear and composable way to achieve striding via views::stride. Chaining adaptors like views::slice(M, N) | views::stride(P) makes the intent and order of operations clear, whereas adding a stride overload to views::slice could blur the distinction between slicing and stepping, making the API less intuitive.

    For these reasons, the author does not support stride.

    Implementation experience

    The author implemented views::slice based on libstdc++, see here.

    Proposed change

    This wording is relative to N5014.

      1. Add a new feature-test macro to 17.3.2 [version.syn]:

        #define __cpp_lib_ranges_slice 2026XXL // freestanding, also in <ranges>
      2. Modify 25.2 [ranges.syn], Header <ranges> synopsis, as indicated:

        // mostly freestanding
        #include <compare>              // see [compare.syn]
        #include <initializer_list>     // see [initializer.list.syn]
        #include <iterator>             // see [iterator.synopsis]
        
        namespace std::ranges {
          […]
          namespace views { inline constexpr unspecified drop_while = unspecified; }
        
        // [range.slice], slice view
          namespace views { inline constexpr unspecified slice = unspecified; }
          […]
        }
                
      3. Add 25.7.? Slice view [range.slice] after 25.7.13 [range.drop.while] as indicated:

        -1- A slice view presents elements from index N (inclusive) up to index M (exclusive) of another view, or all elements from N to the end if M exceeds the range, or an empty view if there are fewer than N elements.

        -2- The name views::slice denotes a range adaptor object ([range.adaptor.object]). Let E, F and G be expressions, let T be remove_cvref_t<decltype((E))> and D be range_difference_t<decltype((E))>. If decltype((F)) or decltype((G)) does not model convertible_to<D>, views::slice(E, F, G) is ill-formed. Otherwise, the expression views::slice(E, F, G) is expression-equivalent to E | views::drop(F) | views::take(static_cast<D>(G) - static_cast<D>(F)), except that F is evaluated only once.

        -3- [Example 1:

          auto ints = views::iota(0);
          auto fifties = ints | views::slice(50, 60);
          println("{} ", fifties); // prints [50, 51, 52, 53, 54, 55, 56, 57, 58, 59]
        end example]

    References

    [P2760R1]
    Barry Revzin. A Plan for C++26 Ranges. URL: https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2023/p2760r1.html