| Document number | P3216R4 |
| Date | 2026-7-26 |
| Audience | LEWG, SG9 (Ranges) |
| Reply-to | Hewill Kang <hewillk@gmail.com> |
views::slice
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.
Initial revision.
Introduce new slice_view class based on feedback from SG9 St. Louis.
Apply specialization for optional based on feedback from SG9 Kona.
Provide const begin() member when the underlying range is not a forward_range.
Remove slice_view and use assembly design instead.
Provides the existing practice on GitHub.
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.
views::slice is neededDevelopers often need to access a specific segment of a range, whether for data processing, algorithmic
partitioning, or windowed computations. Introducing
views::slice aligns C++ with the expectations set by other languages, reducing the cognitive gap for
new and
experienced programmers alike.
While it is possible to achieve slicing today with views::drop(start) |
views::take(end - start), this composition, while valid, may obscure the programmer's intent due to its
verbosity. A single views::slice(start,
end) call is both concise and self-documenting,
improving code clarity and maintainability.
Unlike subrange and counted, which have limitations when working with only-input or
non-sized range types,
views::slice can be designed to work generically with any range, making it broadly applicable. Its
semantics — returning the interval [start, end) and gracefully handling
out-of-bounds indices — are both predictable and robust.
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).
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.
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:
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.
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.
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 asend 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.
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.
The author implemented views::slice based on libstdc++, see here.
This wording is relative to N5014.
Add a new feature-test macro to 17.3.2 [version.syn]:
#define __cpp_lib_ranges_slice 2026XXL // freestanding, also in <ranges>
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; } […] }
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]