| Document number: | P3982R2 | |
|---|---|---|
| Date: | 2026-03-24 | |
| Audience: | Library Evolution Working Group | |
| Reply-to: | Tomasz Kamiński <tomaszkam at gmail dot com> | |
| Mark Hoemmen <mark dot hoemmen at gmail dot com> |
strided_slice into extent_slice and range_slice for C++26Addresses PL007: Define the extent member of the strided_slice
This paper proposes two changes:
strided_slice to extent_slice and
adjust the meaning of its extent member, to designate the desired
number of elements in the range produced by submdspan.
This change would be breaking after C++26 is shipped.range_slice slice type,
that expresses the (first, last, stride) interface provided for range
slicing in other programming languages.
While this is an extension that can be added in a later standard,
this change preserve more ergonomic and familiar interface for submdspan.| Before | After |
|---|---|
std::strided_slice{0, 2, 3};
|
std::extent_slice{0, 1, 3};
std::range_slice{0, 2, 3};
|
std::strided_slice{2, 10, 3};
|
std::extent_slice{2, 4, 3};
std::range_slice{2, 12, 3};
|
Apllied wording fixes from LWG review in Croydon.
[first, last, stride],
as slice type. This change will be proposed for C++29.strided_slice
per Charles Hussong suggestions.Initial revision.
For the invocation of in the form smd = submdspan(md, strided_slice{offset, extent, stride}),
there are two ranges of indices of elements to which we refer:
md, that can be accessed by smd; andsmd.Given the above, there are two possible interpretations of the extent for the above example:
1 + (extent - 1) / stride; orextent * stride.In most cases, this two meanings are functionally equivalent and they can be transformed into each other. However, due use of the division in the input span interpretation does not support the following:
stride whose value is zero, that could be used to produce non-unique layouts; orAs strided_slice is used as one of the canonical forms of the slices,
we propose to strided_slice::extent member should represent the output extent.
range_sliceOne argument for using the input span as the value of stride_slice::extent
was consistency with other programming languages' range slicing interface.
However, surveying the slicing interface in common languages shows that they all use first, last
instead of offset, length.
array(first:last) and array(first:last:step)array[first:last] and array[first:last:step]array(first:last) and array(first:step:last)array[first..last], array[first..], array[..last] and array[..].submdspan(md, strided_slice{2, 5, 1}),
should select elements [2, 5), instead of [2, 7) as currently specified.
To provide a interface consistent with existing practice in many languages, we propose to introduce a new vocabulary type for "range" slice:
template<typename FirstType, typename LastType, typename StrideType = constant_wrapper<1zu>>
struct range_slice
{
[[no_unique_addresss]] FirstType first{};
[[no_unique_addresss]] LastType last{};
[[no_unique_addresss]] StrideType stride{};
};
Creating a subset of a multidimensional index space (submdspan) requires
the output extent to be known. In the model where user provides an input span,
the output extent computation is performed, and thus duplicated in each layout.
In contrast, with this paper's proposed changes, the members of strided_slice
directly represent values used by submdspan creation. This gives programmers
more direct control over the process. In particular, in cases when the value of output extent
is known, or can be reused between invocations, passing it directly can lead to measurable
speed-up. We illustrate this by including benchmark results below.
submdspan(md, prefix_slice{0, span, stride}) | ||
|---|---|---|
|
Before | After |
stride \ prefix:span |
strided:10 |
extent:1 + 9 / stride |
3 |
3.03 ns | 1.52 ns |
1 |
3.03 ns | 1.51 ns |
std::cw<3> |
1.01 ns | 1.01 ns |
std::cw<1> |
1.02 ns | 1.01 ns |
As previously mentioned, range_slice can be used if passing
an input span is preferred. Results from a benchmark similar to one
used above show no significant performance difference.
submdspan(md, range_slice{0, 10, stride}) | ||
|---|---|---|
stride |
Before | After |
3 |
3.03 ns | 3.03 ns |
1 |
3.03 ns | 1.52 ns |
std::cw<3> |
1.01 ns | 1.01 ns |
std::cw<1> |
1.01 ns | 1.01 ns |
More details about above results may be found here.
As mentioned before, the current input span specification does not give users a way to
select a statically sized subset of elements with dynamic stride. For example,
imagine that we want to select 5 elements, evenly spaced from the mdspan md.
With the proposed change, we can simply express that as:
auto smd = sumdspan(md, strided_slice{cw<0>, cw<5>, md.extents()[0] / 5})
Note that the number of number of elements in the smd is always known
statically, regardless if the source span had static extents.
Using a zero as the value of the stride leads to a non-unique mappings,
because incrementing the index does not change the referenced element. Thus, they are not
accepted by the standard mappings.
However, submdspan can be also used with mdspan with
user-defined mappings that are not required to be unique. Any mapping can
be queried (via is_always_unique or is_unique) for this property.
One could imagine a layout_stride_relaxed1 layout that is equivalent
to layout_strided, except that it does not require that the provided strides
result in a non-unique mapping. In case of mdspan with such mapping, a zero stride
may be used to create a layout that "broadcasts" a single element over the given extent.
auto smd = submdspan(md, strided_slice{3, 5, 0});
For the above example, each of smd[0], smd[1], ..., smd[4] results in a reference
to md[3].
As mentioned before, such a slice specification is not representable currently. While the paper does not lift the current requirements on the stride value being non-zero, it permits zero strides in the future for a subset of mappings.
1 A version of layout_stride_relaxed was
recently proposed for inclusion
in NVIDIA's CCCL library.
strided_sliceAfter expanding the set of accepted slice types, the strided_slice name
does not capture the difference well, as range_slice is also strided. Thus,
we propose to rename the class to extent_slice.
The extent_slice name was selected to focus on the fact that its members
define the size (extent) of the produced (output) multidimensional index space. That is, it directly reflects
the value of smd.extent(k), where smd is the mdspan produced by submdspan,
and k is the index of the extent to which the slice is applied.
We also avoid using words commonly used to refer to the range like "size,"
"length" (as in offset + length), or "span".
The word "size" is particularly overloaded,
for example in the std::ranges::sized_range concept.
This paper proposes two changes:
strided_slice to extent_stride and
adjust the meaning of extent member, to designate the desired
number of elements in the produced range.range_slice,
that expresses the (first, last, stride) interface for range
slicing provided by other programming languages.From the above only the first change need to be applied in the C++26 timeframe. The others are extensions that could be applied to future standards.
1. Foward P3982R1 as resolution for PL007 NB comment to LWG for inclusion to C++26.
2. Foward P3982R1 without range_slice adition as resolution for PL007 NB comment to LWG for inclusion to C++26.
extent_slice needs to target C++26strided_slice is one of the canonical slice types that define the
interface between submdspan (and potentially other components providing
such facility) and custom layouts. Thus, it is important that the interface is both mininal
(reducing the burden on layouts implementers) and able to represent a wide range of
input without loss of information. As this document explains, the current specification
of strided_slice fails in both accounts (it incurs cost of division, and cannot
be used for non-unique layouts).
We currently reserve rights to introduce additional canonical stride types.
We could imagine introducing a separate canonical_strided_slice type
in a later standard. However, in contrast to amending strided_slice,
this would essentially duplicate the number of types that layouts would need to handle,
as pre-existing code depending on sliceable layout requirements may still produce
strided_slice objects.
range_slice should target C++26While range_slice could be added later as an extension, the authors strongly
believe that the ergonomics of submdspan would be severly degraded, without
the ability to specify a slice using an input span.
During the work on this paper, one of the authors (Tomasz) made the following mistakes,
when transforming the input span span to an output extent:
span / stride,(span - 1) / stride (missing +1),span equal to zero.This paper only impacts the behavior of the std::submdspan library function
that was introduced in C++26.
Here is a patch series
implementing the proposed wording changes to submdspan in libstdc++.
The proposed wording changes refer to N5032 (C++ Working Draft, 2025-12-15).
Apply following changes to section [mdspan.syn] Header <mdspan> synopsis:
// [mdspan.sub], submdspan creation
template<class OffsetType, class LengthType, class StrideType>
struct strided_slice;
template<class FirstType, class LastType,
class StrideType = std::constant_wrapper<1zu>>
struct range_slice;
template<class LayoutMapping>
struct submdspan_mapping_result;
Apply following changes to section [mdspan.sub.overview] Overview :
-1- The
submdspanfacilities create a newmdspanviewing a subset of elements of an existing inputmdspan. The subset viewed by the created mdspan is determined by theSliceSpecifierarguments.-2- Given a signed or unsigned integer type
IndexType, a typeSis a submdspan slice type forIndexTypeif at least one of the following holds:
- -2.1-
is_convertible_v<S, full_extent_t>istrue;- -2.2-
is_convertible_v<S, IndexType>istrue;- -2.3-
Sis a specialization ofstrided_sliceandis_convertible_v<X, IndexType>istrueforXdenotingS::offset_type,S::extent_type, andS::stride_type;or- -2.?-
Sis a specialization ofrange_sliceandis_convertible_v<X, IndexType>istrueforXdenoting type ofS::first,S::last, andS::stridemembers; or- -2.4- all of the following hold:
- -2.4.1- the declaration
auto [...ls] = std::move(s);is well-formed for some objectsof typeS,- -2.4.2-
sizeof...(ls)is equal to2, and- -2.4.3-
(is_convertible_v<decltype(std::move(ls)), IndexType> && ...)istrue.-3- Given a signed or unsigned integer type
IndexType, a typeSis a canonicalsubmdspanindex type forIndexTypeifSis eitherIndexTypeorconstant_wrapper<v>for some valuevof typeIndexType, such thatvis greater than or equal to zero.-4- Given a signed or unsigned integer type
IndexType, a typeSis a canonicalsubmdspanslice type forIndexTypeif exactly one of the following istrue:
- -4.1-
Sisfull_extent_t;- -4.2-
Sis a canonical submdspan index type forIndexType; or- -4.3-
Sa specialization ofstrided_slicewhere all of the following hold:
- -4.3.1-
S::offset_type,S::extent_type, andS::stride_typeare all canonical submdspan index types forIndexType; and- -4.2.2- if
S::stride_typeandS::extent_typeare both specializations ofconstant_wrapper, thenS::stride_type::valueis greater than zero.-5- A type
Sis a collapsing slice type if […]-6- A type
Sis a unit-stride slice type if […]-7- Given an object
eof typeEthat is a specialization ofextents, and an objectsof typeSthat is a canonical submdspan slice type forE::index_type, thesubmdspanslice range ofsfor the kth extent ofeis:
- -7.1-
[0, e.extent(k)), ifSisfull_extent_t;- -7.?-
[E::index_type(s.offset), E::index_type(s.offset)), ifSis a specialization ofstrided_sliceandE::index_type(s.extent)is zero; otherwise- -7.2-
[E::index_type(s.offset), E::index_type(s.offset + 1 + (s.extent - 1) * s.stride)), ifSis a specialization ofstrided_slice; otherwise- -7.3-
[E::index_type(s), E::index_type(s)+1)-8- Given a type
Ethat is a specialization ofextents, a typeSis a validsubmdspanslice type for the kth extent ofEifSis a canonical slice type forE::index_type, and forxequal toE::static_extent(k), either x is equal todynamic_extent; orwhere
- -8.1- if
Sis a specialization ofstrided_slice:
- -8.1.1-
ifo is less than or equal to x;S::offset_typeis a specialization ofconstant_wrapper, thenS::offset_type::value- -8.1.2-
ife is less than or equal to x;S::extent_typeis a specialization ofconstant_wrapperthenS::extent_type::value- -8.1.?- if e is greater than one, then t is greater than zero,
- -8.1.3-
if bothS::offset_typeandS::extent_typeare specializationsofconstant_wrapperthen,S::offset_type::value + S::extent_type::value
if e is greater than zero, then o + 1 + (e - 1) * t is less than or equal to x
- -8.1.4- o is value of
S::offset_type::valueifS::offset_typeis a specialization ofconstant_wrapperand 0 otherwise,- -8.1.5- e is value of
S::extent_type::valueifS::extent_typeis a specialization ofconstant_wrapperand 0 otherwise,- -8.1.6- t is value of
S::stride_type::valueifS::stride_typeis a specialization ofconstant_wrapperand 1 otherwise.- -8.2- if
Sis a specialization ofconstant_wrapper, thenS::valueis less thanx-9- Given an object
eof typeEthat is a specialization ofextentsand an objectsof typeS,sis a validsubmdspanslice for the kth extent ofeif:
- -9.1-
Sis a validsubmdspanslice type for kth extent ofE;- -9.2- the kth interval of
econtains thesubmdspanslice range ofsfor the kth extent ofe; and- -9.3- if
Sa specialization ofstrided_slicethen:
- -9.3.1-
s.extentis greater than or equal to zero, and- -9.3.2- either
s.extentequals zerois less than two ors.strideis greater than zero.
Apply following changes to section [mdspan.sub.strided.slice] strided_slice:
23.7.3.7.2
Range slices [mdspan.sub.strided_slicestridedrange.slices]
- -1-
strided_sliceandrange_slicerepresentsa set of extent regularly spaced integer indices. The indices start atoffsetandfirstrespectively, and increase by increments ofstride.namespace std { template<class OffsetType, class ExtentType, class StrideType> struct strided_slice { using offset_type = OffsetType; using extent_type = ExtentType; using stride_type = StrideType; [[no_unique_address]] offset_type offset{}; [[no_unique_address]] extent_type extent{}; [[no_unique_address]] stride_type stride{}; }; template<class FirstType, class LastType, class StrideType = std::constant_wrapper<1zu>> struct range_slice { [[no_unique_address]] FirstType first{}; [[no_unique_address]] LastType last{}; [[no_unique_address]] StrideType stride{}; }; }
- -2-
strided_sliceandrange_slicehashave the data members and special members specified above.It hasThey have no base classes or members other than those specified.- -3- Mandates:
OffsetType,ExtentType,FirstType,LastType, andStrideTypeare signed or unsigned integer types, or modelintegral-constant-like.- [ Note: Both
strided_slice{ .offset = 1, .extent =and104, .stride = 3}range_slice{ .first = 1, .last = 11, .stride = 3}indicatesthe indices1,4,7, and10. Indices are selected from the half-open interval[1, 1 + 10). — end note]
Apply following changes to section [mdspan.sub.helpers] Exposition-only helpers:
template<class T> concept is-strided-slice = see below;
- -2- The concept
is-strided-slice<T>is satisfied and modeled if and only ifTis a specialization ofstrided_slice.template<class T> concept is-range-slice = see below;
- -?- The concept
is-range-slice<T>is satisfied and modeled if and only ifTis a specialization ofrange_slice.template<class IndexType, class S> constexpr auto canonical-index(S s);
- -3- Mandates:
- […];
- -4- Preconditions:
- […];
- -5- Effects:
- […];
template<class IndexType, class OffsetType, class SpanType, class... StrideTypes> constexpr auto canonical-range-slice(OffsetType offset, SpanType span, StrideTypes... strides);
- -?- Let:
StrideTypedenote
constant_wrapper<IndexType(1)>ifStrideTypesis an empty pack orSpanTypedenotesconstant_wrapper<IndexType(0)>, otherwiseStrideTypes...[0];stridebe
StrideType()ifStrideTypeis a specialization ofconstant_wrapper, otherwiseIndexType(1)ifspan == 0istrue, otherwisestrides...[0];extent-valuebe1 + (span - 1) / strideifspan != 0istrue, and0otherwise;extentbecw<IndexType(extent-value)>if bothSpanTypeandStrideTypeare specializations ofconstant_wrapper, andIndexType(extent-value)otherwise.- -?- Mandates:
sizeof...(StrideTypes) <= 1istrue, and- if
StrideTypeis a specialization ofconstant_wrapper, thenStrideType::value > 0istrue.- -?- Preconditions:
stride > 0istrue.- -?- Returns:
strided_slice{ .offset = offset, .extent = extent, .stride = stride };template<class IndexType, class S> constexpr auto canonical-slice(S s);
- -6- Mandates:
Sis asubmdspanslice type forIndexTye.- -7- Effects:
- Equivalent to:
if constexpr (is_convertible_v<S, full_extent_t>) { return static_cast<full_extent_t>(std::move(s)); } else if constexpr (is_convertible_v<S, IndexType>) { return canonical-index<IndexType>(std::move(s)); } else if constexpr (is-strided-slice<S>) { return strided_slice{ .offset = canonical-index<IndexType>(std::move(s.extent)), .extent = canonical-index<IndexType>(std::move(s.offset)), .stride = canonical-index<IndexType>(std::move(s.stride)) };auto c_extent = canonical-index<IndexType>(std::move(s.extent)); auto c_offset = canonical-index<IndexType>(std::move(s.offset)); if constexpr (is_same_v<decltype(c_extent), constant_wrapper<IndexType(0)>>) { return strided_slice{ .offset = c_offset, .extent = c_extent, .stride = cw<IndexType(1)> }; } else { return strided_slice{ .offset = c_offset, .extent = c_extent, .stride = canonical-index<IndexType>(std::move(s.stride)) }; }} else if constexpr (is-range-slice<S>) { auto c_first = canonical-index<IndexType>(std::move(s.first)); auto c_last = canonical-index<IndexType>(std::move(s.last)); return canonical-slice-range<IndexType>( c_first, canonical-index<IndexType>(c_last - c_first), canonical-index<IndexType>(std::move(s.stride)); } else { auto [s_first, s_last] = std::move(s); auto c_first = canonical-index<IndexType>(std::move(s_first)); auto c_last = canonical-index<IndexType>(std::move(s_last));return strided_slice{ .offset = c_first, .extent = canonical-index<IndexType>(c_last - c_first), .stride = cw<IndexType(1)> };return canonical-slice-range<IndexType>( c_first, canonical-index<IndexType>(c_last - c_first)); }
Apply following changes to section [mdspan.sub.extents] submdspan_extents function:
template<class IndexType, size_t... Extents, class... SliceSpecifiers> constexpr auto submdspan_extents(const extents<IndexType, Extents...>& src, SliceSpecifiers... raw_slices);
- -1- Let
slicesbe […];- -2- Constraints:
- […];
- -3- Mandates:
- […];
- -4- Preconditions:
- […];
-5- Let
SubExtentsbe a specialization ofextentssuch that:
- -5.1-
SubExtents::rank()equalsMAP_RANK(slices, Extents::rank());and;- -5.2- for each rank index
kofExtentsextents<IndexType, Extents...>such that the type ofslices...[k]is not a collapsing slice type,SubExtents::static_extent(MAP_RANK(slices, k))equals the following, whereΣkdenotes the type ofslices...[k]:
- -5.2.1-
Extents::static_extent(k)ifΣkdenotesthefull_extent_t; otherwise-5.2.2-0, ifΣkis a specialization ofstrided_sliceandΣk::extent_typedenotesconstant_wrapper<IndexType(0)>; otherwise- -5.2.3-
1 + ((Σk::extent_type::value - 1) / Σk::stride_type::value)Σk::extent_type::valueifΣkis a specialization ofstrided_slicewhoseextent_typeanddenotes a specializationstride_typesofconstant_wrapper;- -5.2.4- otherwise,
dynamic_extent.- -6- Returns:
A value
extof typeSubExtentssuch that for each indexkofextents<IndexType, Extents...>, where the type ofslices...[k]is not a collapsing slice type,ext.extent(MAP_RANK(slices, k))equals the following whereσkdenotesslices...[k]:
- -6.1-
σk.extentif the type of σk is a specialization of== 0 ? 0 : 1 + (σk.extent - 1) / σk.stridestrided_slice,- -6.2- otherwise,
U−L, where[L, U)is thesubmdspanslice range ofσkfor thekthextent ofsrc.
Apply following changes to section [mdspan.sub.map.common] Common:
-6- Let
sub_stridesbe anarray<SubExtents::index_type, SubExtents::rank()>such that for each rank indexkofextents()for which the type ofslices...[k]is not a collapsing slice type,sub_strides[MAP_RANK(slices,k)]equals:
- -6.1-
stride(k) * s.strideif type ofsis a specialization ofstrided_sliceands.stride < s.extents.extent > 1istrue, wheresisslices...[k];- -6.2- otherwise,
stride(k).
Replace all occurrences of strided_slice and is-strided-slice
in [mdspan.syn] and [mdspan.sub] with extent_slice and is-extent-slice respectively.
Update the value of the __cpp_lib_submdspan in [version.syn]
Header <version> synopsis to reflect the date of approval of this proposal.
Christian Trott and Charles Hussong offered many useful suggestions and corrections to the proposal.