---
title: "views::slice"
document: P3216R4
date: 2026-07-27
audience: LEWG, SG9 (Ranges)
reply-to:
  - "Hewill Kang <hewillk@gmail.com>"
---

- Abstract
  - Revision history
  - Discussion
  - Design
  - Implementation experience
  - Proposed change
  - References

## Abstract

This paper proposes the Tier 1 adaptor `views::slice` (as described in [P2760](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2023/p2760r1.html)) 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

- Familiarity
- Readability
- Generic and Robust Semantics

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](https://github.com/search?q=views%3A%3Aslice+language%3AC%2B%2B&type=code&l=C%2B%2B) 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](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2024/p3216r0.html), 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](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2022/p2214r2.html#the-takedrop-family):

"`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](https://github.com/cplusplus/papers/issues/1865#issuecomment-2197500247), 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](https://github.com/ericniebler/range-v3/blob/108f93c279c8f9cec175dac361084983d0176e99/include/range/v3/view/slice.hpp#L120) does this.

In later versions such as [R3](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2026/p3216r3.html), 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](https://godbolt.org/z/nv4ejhE5n).

## Proposed change

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

## References

[P2760R1] Barry Revzin. A Plan for C++26 Ranges. URL: [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)
