submdspan_mapping
?Document #: | P3663R0 |
Date: | 2025-03-14 |
Project: | Programming Language C++ LEWG |
Reply-to: |
Mark Hoemmen <mhoemmen@nvidia.com> |
submdspan
canonicalize slices
This proposal apprises WG21 of an issue with
submdspan_mapping
. We do not think the issue warrants a
change to C++26. However, we present a solution to be applied to C++26,
in case WG21 thinks a change is needed.
Currently, the submdspan
function can call a
submdspan_mapping
customization with any valid slice type.
This means that submdspan_mapping
customizations may be
ill-formed, possibly even without a diagnosic, if future C++ versions
expand the set of valid slice types. This will happen if P2769R3 is merged into the Working
Draft for C++29, as that would generalize
tuple-like
from a concrete list of types to a
concept. It may also happen in the future for other categories of slice
types.
The natural way to fix this is with a “canonicalization” approach.
First, define a fixed set of “canonical” slice types that represent all
valid slices, and a function to map valid slices to their canonical
versions. Then, change submdspan
so it always canonicalizes
its input slices, and so it only ever calls
submdspan_mapping
customizations with canonical slices.
We do not propose this as a change for C++26 because we believe that
this is something implementations would want to do anyway. The
possibility of later generalization of tuple-like
would naturally lead high-quality C++26 implementations to canonicalize
such slice inputs. That would minimize code changes both for users, and
for implementations (as the Standard Library currently has five
submdspan_mapping
customizations of its own). Nevertheless,
we present a draft of a canonicalization design as an option for WG21 to
consider.
The current submdspan
wording has the following
constraint ([mdspan.sub.sub]
3.2).
… the expression
submdspan_mapping(src.mapping(), slices...)
is well-formed when treated as an unevaluated operand.
However, nothing currently requires submdspan_mapping
to
be ill-formed when any of its slices is not a valid slice type.
Thus, the submdspan
function can call a
submdspan_mapping
customization with any slice type that is
valid for submdspan
. This means that the following scenario
invokes undefined behavior.
A user defines a layout mapping my_layout::mapping
with a C++26 - compatible submdspan_mapping
customization.
A later C++ version adds a new valid slice type
new_slice
.
submdspan
is called with
my_layout::mapping
and a new_slice
slice.
This is true whether the new slice type is in one of the existing four categories of slice types or is in a new category. It is a scenario that has already occurred and may occur again, as we will explain below.
The Working Draft has four categories of slice types. Let
S
be an input slice type of submdspan
.
“Full”: S
is convertible to
full_extent_t
. This means “all of the indices in that
extent.”
“Contiguous subrange”: S
models
index-pair-like
<index_type>
, or
S
is a strided_slice
specialization whose
stride_type
is integral-constant-like
with value 1. A contiguous subrange slice represents a contiguous range
of indices in that extent. The fact that the indices are contiguous (in
other words, that they have stride 1) is known at compile time as a
function of the slice’s type alone. The special case of
strided_slice
with compile-time stride 1 is the only way to
represent a contiguous index range with run-time offset but compile-time
extent. (See P3355r2, which was
voted into the Working Draft for C++26 at the 2024 Wrocław
meeting.)
“Strided”: S
is a specialization of
strided_slice
, but is not a contiguous subrange. It may
represent a noncontiguous set of indices in that extent.
“Integer”: S
is convertible to (the layout
mapping’s) index_type
. This means “fix the slice to view
that extent at only that index.” Each integer slice reduces the result’s
rank by one.
The Draft uses the term unit-stride slice to include both full and contiguous subrange slices.
Why would calling a C++26 - compatible submdspan_mapping
customization with a post - C++26 slice type be undefined behavior? Why
wouldn’t it just be ill-formed? The problem is that the customization
might actually be well-formed for new slice types, but it might do
something that submdspan
doesn’t expect. For example, it
might violate preconditions of the submdspan_mapping
customization, or it might produce a
submdspan_mapping_result
that violates postconditions of
submdspan
(e.g., views the wrong elements). This could
happen for two reasons.
The user has overloaded the customization incorrectly. It works correctly for all C++26 slice types, but incorrectly maps the new slice type to one of the four C++26 cases.
The user intends for the customization to be well-formed with
invalid slice types. It might have behavior that makes sense for the
user, but not for submdspan
, as in the following
example.
struct my_tag_slice_t {
template<size_t k>
friend constexpr size_t get() const {
if constexpr (k == 0) {
return 42;
}
else if constexpr (k == 1) {
return 100;
}
else {
static_assert(false);
}
}
};
namespace std {
struct tuple_size<my_tag_slice_t>
: integral_constant<size_t, 2> {};
template<size_t I>
struct tuple_element<I, my_tag_slice_t> {
using type = size_t;
};
} // namespace std
template<class... Slices>
friend constexpr auto
(const mapping& src, Slices... slices)
submdspan_mapping{
if constexpr ((valid_cpp26_slice<Slices> && ...)) {
// ... all slices are valid C++26 slices,
// so do the normal C++26 thing ...
}
else if constexpr (is_same_v<Slices, my_tag_slice_t> && ...) {
// my_tag_slice_t is not a valid C++26 slice,
// but it has an unambiguous interpretation
// as a pair of indices.
return std::string("Hello, I am doing something weird");
}
else {
static_assert(false);
}
}
WG21 has already expanded the set of contiguous subrange slice types
once, and is likely to do so again. This is because it depends on the
exposition-only concept pair-like
. Adoption of P2819R2 into the Working Draft for
C++26 at the 2024 Tokyo meeting made complex
a
pair-like
type, and therefore a valid slice type
for submdspan
. P2769R3, currently in LEWG review,
proposes generalizing tuple-like
from a concrete
list of types to a concept. That would make user-defined pair types
valid slice types.
Before adoption of P2819R2, a
user-defined layout mapping’s submdspan_mapping
customization could determine whether a type is T
is
pair-like
by testing exhaustively whether it is
array<X, 2>
for some X
,
tuple<X, Y>
or pair<X, Y>
for some
X
and Y
, or ranges::subrange
with
tuple_size
2. P2819 would break a customization that uses
this approach. Similarly, adoption of P2769R3 would break customizations
that add complex<R>
(for some R
) to this
list of types to test exhaustively.
One could argue that any type T
with get
findable by argument-dependent lookup (ADL),
tuple_size<T>
of 2, and
tuple_element<I, T>
convertible to
index_type
for all I
has an unambiguous
interpretation as a contiguous subrange. Thus, users should write their
customizations generically to that interface. A reasonable way to do
that would be to imitate the current wording that uses
get<0>
([mdspan.sub.helpers]
2.2) and get<1>
([mdspan.sub.helpers]
7.2). This is a reasonable thing to do. However, in terms of the
Standard, there are three issues with that approach.
Standard Library implementers need to assume that users read the
Standard as narrowly as possible. C++26 does not forbid calling a
submdspan_mapping
customization with a user-defined pair
type, but it also does not restrict its behavior in that case.
What if a future C++ version calls any type destructurable into
two elements a “pair-like type,” even if it does not have ADL-findable
get
?
This approach would not help if other slice categories are expanded, or if a new slice category is added, as the following sections explain.
The only types currently in the “strided” category are
specializations of strided_slice
. Future versions of C++
might expand this category. In our view, this would have little benefit.
Nevertheless, nothing stops WG21 from doing so. For example, C++29 might
(hypothetically) make a strided slice any type that meets the following
exposition-only strided-slice<index_type>
concept.
template<class T>
concept integral_not_bool = std::is_integral_v<T> &&
! std::is_same_v<T, bool>;
template<class IndexType, class T>
concept strided-slice = semiregular<T> &&
<T::offset_type, IndexType> &&
convertible_to<T::extent_type, IndexType> &&
convertible_to<T::stride_type, IndexType> &&
convertible_torequires(T t) {
{ t.offset } -> convertible_to<IndexType>;
{ t.extent } -> convertible_to<IndexType>;
{ t.stride } -> convertible_to<IndexType>;
};
The following user-defined type my_slice
would meet
strided-slice
<size_t>
, even
though
it is not convertible to or from
strided_slice<size_t, size_t, size_t>
;
it has a base class, which strided_slice
specializations cannot, per [mdspan.sub.strided.slice] 2; and
it has members other than offset
,
extent
, and stride
, which
strided_slice
cannot, per [mdspan.sub.strided.slice]
2.
struct my_base {};
struct my_slice : public my_base {
using offset_type = size_t;
using extent_type = size_t;
using stride_type = size_t;
offset_type offset;
extent_type extent;
stride_type stride;
// User-declared constructor makes this not an aggregate
(extent_type ext) : offset(0), extent(ext), stride(1) {}
my_slice
// strided_slice is not allowed to have extra members
::string label = "my favorite slice label";
std::vector<int, 3> member_function() const { return {1, 2, 3}; }
std
// my_slice is not convertible to or from strided_slice
template<class Offset, class Extent, class Stride>
(strided_slice<Offset, Extent, Stride>) = delete;
my_slice
template<class Offset, class Extent, class Stride>
operator strided_slice<Offset, Extent, Stride>() const = delete;
};
Now suppose that a user has defined a submdspan_mapping
customization in a C++26 - compatible way for their custom layout
mapping custom_layout::mapping
. Then, my_slice
likely would not work for this layout mapping in C++29, even though the
hypothetical _strided-slice
_ concept makes it possible to
interpret my_slice
unambiguously as a strided slice, and to
write generic code that does so.
Unlike with contiguous subrange slices, users have no way to
anticipate all possible ways that future Standard versions might expand
the set of valid strided slice types. For example, C++29 could decide
that strided slices must be aggregate types (which would exclude the
above my_base
), or that they also include any
tuple-like
type with 3 members all convertible to
index_type
(not what we prefer, but WG21 has the freedom to
do this). Furthermore, my_slice
with
stride = cw<1, size_t>
should reasonably count as a
contiguous subrange slice type; expanding one set risks conflicts
between sets.
The same problem would arise if future versions of C++ introduce a
new category of slices. For example, suppose that C++29 hypothetically
adds an “indirect slice” category that takes an arbitrary tuple of
indices to include in that extent of the slice. The Mandates of
submdspan
([mdspan.sub.sub] 4.3) make it ill-formed to call
with a new kind of slice, so this hypothetical C++29 addition would need
to add a new clause there. That would not be a breaking change in
itself. The problem is that nothing currently requires
submdspan_mapping
to be ill-formed when any of its slices
is not a valid slice type.
submdspan
canonicalize slicesThere is a tension between submdspan
users and custom
mapping authors. Users of submdspan
want it to work with
all kinds of slice types. For example, they might want to write their
own tuple types that are standard-layout if all their template
parameters are (unlike std::tuple
), and would expect
submdspan
to work with them. In contrast, users who
customize submdspan_mapping
for their own layout mappings
want a “future-proof” interface. They only want submdspan
to call submdspan_mapping
for a known and unchanging set of
types.
One solution to this tension is to change submdspan
so
that it “canonicalizes” its slice inputs before passing them to
submdspan_mapping
. The submdspan
function
still takes any valid slice types, but it maps each input slice type to
a “canonical” slice type according to rules that we will explain below.
As a result, a submdspan_mapping
customization would ever
be called with the following concrete list of slice types.
index_type
(that is, the layout mapping’s
index_type
)
constant_wrapper<Value, index_type>
for some
Value
strided_slice
where each member is either
index_type
or
constant_wrapper<Value, index_type>
for some
Value
full_extent_t
The constant_wrapper
class template comes from P2187R7, which
LEWG design-approved (with design changes not relevant to this proposal)
and forwarded to LWG on 2025/03/11 for C++26.
This solution would make it always ill-formed to call an existing
submdspan_mapping
customization with a new kind of slice.
With the status quo, calling an existing submdspan_mapping
customization with a new kind of slice would be ill-formed at
best, and could be a precondition violation at worst, because
nothing prevents submdspan
from ever calling existing
submdspan_mapping
customizations with new slice types.
Here are the canonicalization rules for an input slice s
of type S
and a input mapping whose extents type has index
type index_type
. The result depends only on the input
mapping’s extents type and the slices.
If S
is convertible to index_type
, then
canonical-ice
<index_type>(s)
;
else, if S
models
index-pair-like
<index_type>
,
then
strided_slice{.offset=
canonical-ice
<index_type>(get<0>(s)), .extent=
subtract-ice
<index_type>(get<1>(s), get<0>(s)), .stride=cw<index_type(1)>}
.
else, if S
is convertible to
full_extent_t
, then full_extent_t
;
else, if S
is a specialization of
strided_slice
, then
strided_slice{.offset=
canonical-ice
<index_type>(s.offset), .extent=
canonical-ice
<index_type>(s.extent), .stride=
canonical-ice
<index_type>(s.stride)}
.
The two exposition-only functions canonical-ice
and subtract-ice
preserve “compile-time-ness” of
any integral-constant-like
arguments, and force
all inputs to either index_type
or
constant_wrapper<Value, index_type>
for some value
Value
. The cw
variable template comes from P2187R7. Rule
(1) mimics the behavior of the exposition-only function
first_
([mdspan.sub.helpers] 1 - 4).
The function submdspan_canonicalize_slices
would
canonicalize all the slices at once. It takes as arguments the input
mapping’s extents and the input slices of
submdspan_mapping
. Canonicalization would make
submdspan
equivalent to the following code.
auto [...canonicalize_slices] =
(src.extents(), slices...);
submdspan_canonicalize_slicesauto sub_map_result =
(src.mapping(), canonicalize_slices...);
submdspan_mappingreturn mdspan(src.accessor().offset(src.data(), sub_map_result.offset),
.mapping,
sub_map_result::offset_policy(src.accessor())); AccessorPolicy
The auto [...canonical_slices]
bit of code above uses
the “structured bindings can introduce a pack” feature introduced by P1061R10 into C++26. The
submdspan_canonicalize_slices
function takes the extents
for two reasons.
A slice’s canonical type depends on the extents’
index_type
.
Knowing the extents lets it enforce preconditions on the slices, so that the resulting canonical slices don’t need to be checked again. This is why canonicalization is not just a function to be called on each slice without considering its corresponding input extent.
The submdspan
function imposes preconditions on both its
slice arguments and on the result of submdspan_mapping
([mdspan.sub.sub]
5). The preconditions on submdspan_mapping
’s result
require that the user’s customization give a mapping that views exactly
those elements of the input mdspan
that are specified by
the slices. That is, the customization has to be correct, given valid
input slices. Enforcement of this is the user’s responsibility.
Enforcement of preconditions on the input slices is the
implementation’s responsibility. This depends only on the input slices
and the input mapping’s extents. Just as mdspan
can check
bounds for the input of the at
function using the input and
its extents, submdspan
can enforce all preconditions on the
slices using the input mapping’s extents. The current wording expresses
these preconditions identically in two places:
on the slice arguments of submdspan_extents
([mdspan.sub.extents]
3), and
on the slice arguments of submdspan
([mdspan.sub.sub]
5).
Passing the slices through submdspan_extents
is not
enough to ensure that submdspan_mapping
’s slice arguments
do not violate its (unstated) preconditions. This is because deciding
the result mapping’s type and constructing it may depend on the slices,
not just the result’s extents. Another way to say this is that different
slices...
arguments might result in the same extents, but a
different layout mapping. For example, if src
is a rank-1
layout_left
mapping with extents
dims<1>{10}
, both full_extent
and
strided_slice{.offset=0, .extent=src.extent(0), .stride=1}
would result in extents dims<1>{10}
, but different
layouts (layout_left
and layout_stride
,
respectively). This means that slice canonicalization is also an
opportunity to avoid duplication of precondition-checking code.
There are two kinds of preconditions on slices:
that all indices represented by the slice can be represented as
values of type extents::index_type
, that is, that casting
them to extents::index_type
does not cause integer
overflow; and
that all indices represented by slice k
are in [0, src.extent(k)
).
The current wording expresses both preconditions via the
exposition-only functions first_
and
last_
. The specification of each of these uses
extents::
index-cast
to preprocess
their return values (see [mdspan.sub.helpers
4] and [mdspan.sub.helpers
9]). The index-cast
exposition-only function
[mdspan.extents.expo]
9 is a hook to enforce the “no overflow” precondition. The “in the
extent’s range” precondition is currently up to submdspan
implementations to check.
Slice canonicalization needs to have at least the “no overflow”
precondition, because any integer results are either
index_type
or
constant_wrapper<Value, index_type>
for some
Value
. That is, it always casts input to
index_type
. We propose applying the “in the extent’s range”
precondition to slice canonicalization as well. This would let
submdspan
implementations put all their precondition
checking (if any) in the canonicalization function.
We have not measured the performance effects of this approach. Everything we write here is speculation.
In terms of compile-time performance, canonicalization would minimize
the set of instantiations of both submdspan_mapping
and
submdspan_extents
(which customizations of
submdspan_mapping
may call).
In terms of run-time performance, customizations would likely need to
copy slices into their canonical forms. This would introduce new local
variables. Slices tend to be simple, and are made of combinations of
zero to three integers and empty types (like
constant_wrapper
or full_extent_t
). Thus, it
should be easy for compilers to optimize away all the extra types. On
the other hand, if they can’t, then the compiler may need to reserve
extra hardware resources (like registers and stack space) for the extra
local variables. This may affect performance, especially in
tight loops. Mitigations generally entail creating subview layout
mappings by hand.
The proposed set of canonicaliation rules would require turning every
pair-like
type into a strided_slice
.
Pair-like slices are common enough in practice that we may want to
optimize specially for them; see below.
Here are some options that might mitigate some performance concerns. All such concerns are hypothetical without actual performance measurements.
Let Standard layout mappings accept arbitrary slice types.
Restrict the proposed changes to user-defined layout mappings. This
would optimize for the common case of Standard layout mappings. On the
other hand, performing canonicalization for all
submdspan_mapping
customizations (including the Standard
ones) would simplify implementations, especially for checking
preconditions.
Expand the canonicalization rules so that pair
and
tuple
slices are passed through, instead of being
transformed into strided_slice
. This would optimize a
common case, at the cost of making submdspan_mapping
customizations handle more slice types. That would complicate the code
and possibly increase compile-time cost, thus taking away at least some
of the benefits of canonicalization.
This Compiler Explorer link offers a brief and hasty implementation of this solution.
Text in blockquotes is not proposed wording, but rather instructions for generating proposed wording.
Assume that P2187R7 plus fixes from LEWG on its 2025/03/11 review have been applied to the Working Draft. (EDITORIAL NOTE: This makes
constant_wrapper
andcw
available to the rest of the Standard Library.)
__cpp_lib_submdspan
feature test macroIn [version.syn], increase the value of the
__cpp_lib_submdspan
macro by replacing YYYMML below with
the integer literal encoding the appropriate year (YYYY) and month
(MM).
#define __cpp_lib_submdspan YYYYMML // also in <mdspan>
Change [mdspan.sub.helpers] as follows.
template<class T>
constexpr T de-ice(T val) { return val; }
template<integral-constant-like T>
constexpr auto de-ice(T) { return T::value; }
template<class IndexType, integral-constant-like S>
constexpr constant_wrapper<S::value, IndexType> canonical-ice(S s) {
return {};
}
template<class IndexType, class S>
constexpr IndexType canonical-ice(S s) {
return s;
}
template<class IndexType, class X, class Y>
constepxr auto subtract-ice(X x, Y y) {
if constexpr (integral-constant-like<X> && integral-constant-like<Y>) {
return cw<IndexType(de-ice(y) - de-ice(x))>;
}
else {
return IndexType(y - x);
} }
template<class IndexType, size_t k, class... SliceSpecifiers>
constexpr IndexType first_(SliceSpecifiers... slices);
1
Mandates: IndexType
is a signed or unsigned
integer type.
[Editorial note: Skip down to the end of the section – end note]
template<class IndexType, size_t... Extents, class... Slices>
constexpr auto submdspan_canonicalize_slices( const extents<IndexType, Extents...>& src, Slices... slices);
12
Constraints: sizeof...(slices)
equals
sizeof...(Extents)
.
13
Mandates: For each rank index k of src
, exactly one
of the following is true
:
(13.1)
Sk models
convertible_to<IndexType>
,
(13.2)
Sk models
index-pair-like<IndexType>
,
(13.3)
is_convertible_v<
Sk
, full_extent_t>
is true
, or
(13.4)
Sk is a
specialization of strided_slice
.
14
Preconditions: For each rank index k of src
, all of the
following are true
.
(14.1) if
Sk is a
specialization of strided_slice
(14.2) 0 ≤
first_
<IndexType,
k>(slices...)
≤
last_
<
k>(src, slices...)
≤
src.extent(
k)
[Editorial note: These are the same as the preconditions on
the slice inputs of submdspan_extents
and
submdspan
. They must be imposed here, before conversion to
IndexType
or
constant_wrapper<IndexType, Value>
, in order to
include the precondition that forbids integer overflow. – end
note]
15
Returns: a tuple
of src.rank()
elements, where for each rank index k of src
, the following
specifies element k of the
return value:
(15.1) If
Sk models
convertible_to<IndexType>
, then
canonical-ice
<IndexType>(s)
;
(15.2)
else, if Sk models
index-pair-like
<IndexType>
,
then
strided_slice{.offset=
canonical-ice
<IndexType>(get<0>(s)), .extent=
subtract-ice
<IndexType>(get<1>(s), get<0>(s)), .stride=cw<IndexType(1)>}
;
(15.3)
else, if is_convertible_v<
Sk
, full_extent_t>
is true
, then
full_extent_t
;
(15.4)
else, if Sk is a
specialization of strided_slice
, then
strided_slice{.offset=
canonical-ice
<IndexType>(s.offset), .extent=
canonical-ice
<IndexType>(s.extent), .stride=
canonical-ice
<IndexType>(s.stride)}
.
submdspan_mapping
customizationsChange [mdspan.sub.map.common] as follows.
1
The following elements apply to all functions in [mdspan.sub.map]named
submdspan_mapping
that would be found by
overload resolution when called with a layout mapping
m
as their first argument and a parameter pack
slices
as their remaining
arguments(s).
2
Constraints: sizeof...(slices)
equals
extents_type::rank()
.
3
Mandates: For each rank index k of extents()
, exactly
one of the following is true
:
(3.1)
Sk models
is
convertible_to<index_type>
index_type
,
(3.2)
Sk models
is
index-pair-like
<index_type>
constant_wrapper<Value, index_type>
for
some value Value
of type
index_type
such that 0 ≤
Value
<
extents().extent(0)
,
(3.3)
Sk is
is_convertible_v<
Sk
, full_extent_t>
is
true
full_extent_t
, or
(3.4)
Sk is a
specialization of strided_slice
where each member is either
index_type
or
constant_wrapper<Value, index_type>
for
some value Value
of type
extents_type::index_type
such that 0 ≤
Value
<
extents().extent(0)
.
4
Preconditions: For each rank index k
of
extents()
, all of the following are true
:
submdspan
function templateChange [mdspan.sub.sub] (“
submdspan
function template”) as follows.
1
Let index_type
be
typename Extents::index_type
.
2
Let canonical_slices
be the pack resulting from the
following statement.
auto [...canonical_slices] = submdspan_canonicalize_slices(src.extents(), slices...).
3
Let sub_map_offset
be the result of
submdspan_mapping(src.mapping(),
slices
canonical_slices
...)
.
[Note 1: This invocation of submdspan_mapping
selects a function call via overload resolution on a candidate set that
includes the lookup set found by argument-dependent lookup
([basic.lookup.argdep]). — end note]
4 Constraints:
(4.1)
sizeof...(slices)
equals Extents::rank()
,
and
(4.2) the
expression submdspan_mapping(src.mapping(),
slices
canonical_slices
...)
is well-formed when treated as an unevaluated
operand.
5 Mandates:
(5.1)
decltype(submdspan_mapping(src.mapping(),
slices
canonical_slices
...))
is a specialization of
submdspan_mapping_result
.
(5.2)
is_same_v<remove_cvref_t<decltype(sub_map_offset.mapping.extents())>, decltype(submdspan_extents(src.mapping(),
slices
canonical_slices
...))>
is true
.
(5.3) For
each rank index k
of src.extents()
, exactly
one of the following is true
:
6 Preconditions:
(6.1) For
each rank index k of
src.extents()
, all of the following are
true
:
(6.2)
sub_map_offset.mapping.extents() == submdspan_extents(src.mapping(),
slices
canonical_slices
...)
is true
; and
(6.3) for
each integer pack I
which is a multidimensional index in
sub_map_offset.mapping.extents()
,
sub_map_offset.mapping(I...) + sub_map_offset.offset == src.mapping()(src-indices(array{I...}, slices...))
sub_map_offset.mapping(I...) + sub_map_offset.offset == src.mapping()(src-indices(array{I...}, canonical_slices...))
is true
.
7 Effects: Equivalent to:
auto sub_map_result = submdspan_mapping(src.mapping(), slices...);
auto [...canonicalize_slices] =
submdspan_canonicalize_slices(src.extents(), slices...);
auto sub_map_result = submdspan_mapping(src.mapping(), canonicalize_slices...);
return mdspan(src.accessor().offset(src.data(), sub_map_result.offset),
.mapping,
sub_map_result::offset_policy(src.accessor())); AccessorPolicy
We think that canonicalization is the expected implementation
approach anyway, so there is no need to standardize it. Suppose that
P2769 is adopted into some future C++ version. Implementations might
thus expose users to an intermediate period in which they implement
C++26 but not C++29. During this period, users might write
submdspan_mapping
customizations to the narrower definition
of pair-like
. For example, users might write
std::get
explicitly instead of using ADL-findable
get
, which would only work with Standard Library types that
have overloaded std::get
. This would, in turn, encourage
implementers of submdspan
to “canonicalize”
pair-like
slice arguments (e.g., into
pair
or tuple
) before calling
submdspan_mapping
customizations, as that would maximize
backwards compatibility.
In summary, the possibility of later generalization of
pair-like
would naturally lead high-quality
implementations to the desired outcome. That would minimize code changes
both for users, and for implementations (as the Standard Library
currently has five submdspan_mapping
customizations of its
own). Forcing canonicalization for C++26 would merely anticipate the
expected implementation approach.
submdspan
changes a DR for C++26If P2769 is on track to be adopted into some future C++ version, then
we could lobby WG21 to treat P2769’s proposed changes to
submdspan
’s wording as a Defect Report (DR) for C++26. This
would require no effort prior to acceptance of C++26. However,
implementations might expose users to an intermediate period in which
they implement C++26 but not the DR. This would have the same practical
effect on users as if we had applied the change to C++29 but not as a
DR.
Thanks to Tomasz Kamiński for pointing out this issue.