Future-proof submdspan_mapping

Document #: P3663R1
Date: 2025-05-16
Project: Programming Language C++
LEWG
Reply-to: Mark Hoemmen
<>

1 Author

2 Revision history

3 Abstract

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.

We propose fixing this with a “canonicalization” approach. First, define a fixed set of “canonical” slice types that represent all valid slices, and a function submdspan_canonicalize_slices 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.

4 Motivation

4.1 Summary

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.

  1. A user defines a layout mapping my_layout::mapping with a C++26 - compatible submdspan_mapping customization.

  2. A later C++ version adds a new valid slice type new_slice.

  3. 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.

4.2 Four categories of slice types

The Working Draft has four categories of slice types. Let S be an input slice type of submdspan.

  1. “Full”: S is convertible to full_extent_t. This means “all of the indices in that extent.”

  2. “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.

  3. “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.)

  4. “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.

The current Working Draft uses the term unit-stride slice (defined in P3355) to include both full and contiguous subrange slices.

4.3 Why undefined behavior?

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.

  1. 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.

  2. 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
  submdspan_mapping(const mapping& src, Slices... slices)
{
  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);
  }
}

4.4 Set of contiguous subrange slices was expanded once and may be again

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. The pair-like concept depends in turn on the exposition-only concept tuple-like. 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.

It would be reasonable for submdspan to generalize pair-like into any type for which structured binding into two elements is well-formed. If the two elements are both convertible to index_type, then this has an unambiguous interpretation as a contiguous subrange. Thus, users should write their customizations generically to that interface. However, in terms of the Standard, there are two issues with that approach.

  1. 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.

  2. This approach would not help if other slice categories are expanded, or if a new slice category is added, as the following sections explain.

4.5 Other existing categories could be expanded

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> &&
  convertible_to<T::offset_type, IndexType> &&
  convertible_to<T::extent_type, IndexType> &&
  convertible_to<T::stride_type, IndexType> &&
  requires(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

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
  my_slice(extent_type ext) : offset(0), extent(ext), stride(1) {}

  // strided_slice is not allowed to have extra members
  std::string label = "my favorite slice label";
  std::vector<int, 3> member_function() const { return {1, 2, 3}; }

  // my_slice is not convertible to or from strided_slice
  template<class Offset, class Extent, class Stride>
  my_slice(strided_slice<Offset, Extent, Stride>) = delete;

  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 any type for which a structured binding into three elements is well-formed is a valid strided slice. 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.

4.6 WG21 may add a new slice category

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.

5 Solution: Make submdspan canonicalize slices

5.1 Summary

There 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.

We propose resolving this tension by changing 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.

  1. full_extent_t;

  2. index_type (that is, the layout mapping’s index_type);

  3. constant_wrapper<Value, index_type> for some Value; or

  4. strided_slice where each member is either index_type or constant_wrapper<Value, index_type> for some Value.

The constant_wrapper class template comes from P2781. LEWG design-approved P2781R7 (with design changes not relevant to this proposal, which are expressed as P2781R8) and forwarded it to LWG on 2025/03/11 for C++26. The only aspect of a type that can vary in the above list is constant_wrapper’s Value.

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.

5.2 Canonicalization rules

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.

  1. If S is convertible to full_extent_t, then full_extent_t;

  2. else, if S is convertible to index_type, then canonical-ice<index_type>(s);

  3. 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)};

  4. else, if s is destructurable into [first, last] that are both convertible to index_type, then strided_slice{.offset=canonical-ice<index_type>(first), .extent=subtract-ice<index_type>(last, first), .stride=cw<index_type(1)>}.

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. The canonical-ice function also has Mandates or Preconditions (depending on whether its argument is integral-constant-like) that its argument is representable as a value of type index_type. This lets implementations check for overflow. The cw variable template comes from P2781.

5.3 How to apply canonicalization

The function submdspan_canonicalize_slices canonicalizes all the slices at once. It takes as arguments the input mapping’s extents and the input slices of submdspan_mapping. Canonicalization makes submdspan equivalent to the following code.

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),
              sub_map_result.mapping,
              AccessorPolicy::offset_policy(src.accessor()));

The statement starting with auto [...canonical_slices] 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.

  1. A slice’s canonical type depends on the extents’ index_type.

  2. Knowing the extents lets it enforce preconditions on the slices, so that the resulting canonical slices don’t need to be checked again.

5.4 Canonicalization is reusable

We expose submdspan_canonicalize_slices, instead of making it exposition-only, because users may want to use it to build their own functionality analogous to submdspan. For example, consider a C++ type small_matrix<T, M, N> representing a dense rank-2 array with element type T and compile-time extents M and N. (This could be a library exposure of a compiler extension, such as Clang’s matrix type extension.) This type would expose access to individual elements, but would not expose subviews. That is, getting an mdspan that views any subset of elements would be forbidden. However, small_matrix could expose copies of subsets of elements, via a function submatrix that takes the same slice specifiers as submdspan.

small_matrix<float, 5, 7> A = /* ... */;
auto A_sub = submatrix(A,
  strided_slice{.offset=1, .extent=4, .stride=3},  // 1, 4
  strided_slice{.offset=0, .extent=7, .stride=2}); // 0, 2, 4, 6 

If A were a layout_right mdspan, then result of submdspan with those slices would be layout_stride. However, A is not an mdspan. Its layout might be hidden from users. Even if small_matrix layouts are known, there’s no need for submatrix to return layouts in the same way as submdspan does. For example, it might “pack” the result into a contiguous layout_right small_matrix. Nevertheless, the slice specifiers have the same interpretation for submatrix as for submdspan; they are independent of the input’s and result’ layout mappings. Thus, users who want to implement submatrix could use submdspan_canonicalize_slices to canonicalize submatrix’s slice arguments.

This example explains why submdspan_mapping has a name with the word “submdspan” in it, even though it is purely a function of layout mappings and need not be tied to mdspan. Calling submdspan_mapping(mapping, slices...) returns the mapping that submdspan(input, slices...) would have for mapping = input.mapping().

5.5 What about precondition checking?

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:

  1. on the slice arguments of submdspan_extents ([mdspan.sub.extents] 3), and

  2. 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:

  1. 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

  2. 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. Users who implement a submdspan_mapping customization could thus assume that the input slices are all correct.

5.6 Why implementers should do this anyway

Implementers should canonicalize slices anyway, even if this proposal is not adopted. Suppose, for example, that P2769 (expanding the definition of tuple-like) 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, which would only work with Standard Library types that have overloaded std::get, instead of using structured binding to get the two elements. 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). This proposal merely anticipates the expected implementation approach.

6 Performance

6.1 Speculation on compile-time and run-time effects

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.

6.2 Hypothetical performance mitigations

Here are some options that might mitigate some performance concerns. All such concerns are hypothetical without actual performance measurements.

  1. 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.

  2. 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.

6.3 submdspan can skip canonicalization for Standard layout mappings

We believe that by the as-if rule, submdspan should be able to skip canonicalization for layout mappings provided by the Standard, even though we specify submdspan as “Effects: Equivalent to” code that always canonicalizes slices. If LWG disagrees, we offer alternate wording that enforces this permission.

7 Implementation

This Compiler Explorer link offers a brief and hasty implementation of this solution.

8 Acknowledgements

Thanks to Tomasz Kamiński for pointing out this issue and for suggesting improvements to the proposed wording.

9 Proposed wording

Text in blockquotes is not proposed wording, but rather instructions for generating proposed wording.

Assume that P2781R8 has been applied to the Working Draft. [Editorial note: This makes constant_wrapper and cw available to the rest of the Standard Library. – end note]

9.1 Increment __cpp_lib_submdspan feature test macro

In [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>

[Editorial note: If this proposal is adopted for C++26, increasing the version macro is not strictly necessary, because submdspan (P2630) itself is a C++26 feature. We retain the version macro increase in case the proposal is adopted after C++26, possibly as a DR for C++26. – end note]

9.2 Change [mdspan.syn]

Change [mdspan.syn] (“Header <mdspan> synopsis”) as follows.

  struct full_extent_t { explicit full_extent_t() = default; };
  inline constexpr full_extent_t full_extent{};
  template<class IndexType, size_t... Extents, class... Slices>
    constexpr auto submdspan_canonicalize_slices(
      const extents<IndexType, Extents...>& src, Slices... slices);
  template<class IndexType, class... Extents, class... SliceSpecifiers>
    constexpr auto submdspan_extents(const extents<IndexType, Extents...>&, SliceSpecifiers...);

9.3 Change [mdspan.sub.helpers]

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, class S>
constexpr auto canonical-ice(S s);

1 Constraints: S is convertible to IndexType.

2 Mandates:

  • (2.1) IndexType is a signed or unsigned integer type.

  • (2.2) If S models integral-constant-like and if decltype(S::value) is a signed or unsigned integer type, then S::value is representable as a value of type IndexType.

3 Preconditions: If S is a signed or unsigned integer type, then s is representable as a value of type IndexType.

4 Returns:

  • (4.1) If S models integral-constant-like, then constant_wrapper<extents<IndexType>::index_cast(S::value), IndexType>{};

  • (4.2) otherwise, extents<IndexType>::index-cast(s).

template<class IndexType, class X, class Y>
constexpr auto subtract-ice(X x, Y y) {
  return canonical-ice<IndexType>(y) - canonical-ice<IndexType>(x);
}

[Editorial note: Even though submdspan only uses the difference between y and x to compute an extent, each of y and x must be a valid index in the slice’s extent. This is why subtract-ice retains canonical-ice’s precondition. – end note]

template<class IndexType, size_t k, class... SliceSpecifiers>
  constexpr IndexType first_(SliceSpecifiers... slices);

[Editorial note: Former paragraph 1 has been renumbered to 5. – end note]

5 Mandates: IndexType is a signed or unsigned integer type.

6 Let ϕk denote the following value:

7 Preconditions: ϕk is representable as a value of type IndexType.

8 Returns: extents<IndexType>::index-cast(ϕk).

template<size_t k, class Extents, class... SliceSpecifiers>
  constexpr auto last_(const Extents& src, SliceSpecifiers... slices);

9 Mandates: Extents is a specialization of extents.

10 Let index_type be typename Extents::index_type.

11 Let λk denote the following value:

12 Preconditions: λk is representable as a value of type index_type.

13 Returns: Extents​::​index-cast(λk).

enum class check-static-bounds-result {
  in-bounds,
  out-of-bounds,
  unknown
};

template<size_t k, class IndexType, size_t... Exts, class... SliceSpecifiers>
  constexpr check-static-bounds-result check-static-bounds(
    const extents<IndexType, Exts...>&, SliceSpecifiers... slices);

14 Returns:

  • (14.1) check-static-bounds-result::in-bounds if is_convertible_v<Sk, full_extent_t> is true;

  • (14.2) otherwise, if Sk is integral-constant-like and if is_convertible_v<Sk, IndexType> is true:

    • (14.2.1) check-static-bounds-result::out-of-bounds if de-ice(sk) < 0;

    • (14.2.2) otherwise, check-static-bounds-result::out-of-bounds if Exts...[k] does not equal dynamic_extent and if Exts...[k]de-ice(sk);

    • (14.2.2) otherwise, check-static-bounds-result::in-bounds if Exts...[k] does not equal dynamic_extent and if de-ice(sk) < Exts...[k];

    • (14.2.3) otherwise, check-static-bounds-result::unknown;

  • (14.3) otherwise, if Sk is a specialization of strided_slice and if Sk::offset_type models integral-constant-like:

    • (14.3.1) check-static-bounds-result::out-of-bounds if de-ice(sk.offset) < 0;

    • (14.3.2) otherwise, check-static-bounds-result::out-of-bounds if Exts...[k] does not equal dynamic_extent and if Exts...[k] < de-ice(sk.offset);

    • (14.3.3) otherwise, check-static-bounds-result::out-of-bounds if Sk::extent_type models integral-constant-like and if de-ice(sk.offset) + de-ice(sk.extent) < 0;

    • (14.3.4) otherwise, check-static-bounds-result::out-of-bounds if Exts...[k] does not equal dynamic_extent, if Sk::extent_type models integral-constant-like, and if Exts...[k] < de-ice(sk.offset) + de-ice(sk.extent);

    • (14.3.5) otherwise, check-static-bounds-result::in-bounds if Exts...[k] does not equal dynamic_extent, if Sk::extent_type models integral-constant-like, and if 0 ≤ de-ice(sk.offset)de-ice(sk.offset) + de-ice(sk.extent)Exts...[k];

    • (14.3.6) otherwise, check-static-bounds-result::unknown;

  • (14.4) otherwise, if the structured binding declaration auto [s_k0, s_k1] = declval<Sk>(); is well-formed and if decltype(s_k0) is integral-constant-like:

    • (14.4.1) check-static-bounds-result::out-of-bounds if de-ice(s_k0) < 0;

    • (14.4.2) otherwise, check-static-bounds-result::out-of-bounds if Exts...[k] does not equal dynamic_extent and if Exts...[k] < de-ice(s_k0);

    • (14.4.3) otherwise, check-static-bounds-result::out-of-bounds if decltype(s_k1) is integral-constant-like and if de-ice(s_k1) < de-ice(s_k0);

    • (14.4.4) otherwise, check-static-bounds-result::out-of-bounds if Exts...[k] does not equal dynamic_extent, if decltype(s_k1) is integral-constant-like, and if Exts...[k] < de-ice(s_k1);

    • (14.4.5) otherwise, check-static-bounds-result::in-bounds if Exts...[k] does not equal dynamic_extent, if decltype(s_k1) is integral-constant-like, and if 0 ≤ de-ice(s_k0)de-ice(s_k1)Exts...[k];

    • (14.4.6) otherwise, check-static-bounds-result::unknown;

  • (14.5) otherwise, check-static-bounds-result::unknown.

template<class IndexType, size_t N, class... SliceSpecifiers>
  constexpr array<IndexType, sizeof...(SliceSpecifiers)>
    src-indices(const array<IndexType, N>& indices, SliceSpecifiers... slices);

15 Mandates: IndexType is a signed or unsigned integer type.

16 Returns: An array<IndexType, sizeof...(SliceSpecifiers)> src_idx such that for each k in the range [0, sizeof...(SliceSpecifiers)), src_idx[k] equals

9.4 Add section [mdspan.sub.slices], “Slices definitions and canonicalization”

Add a new section [mdspan.sub.slices], “Slices definitions and canonicalization,” after [mdspan.sub.helpers] and before [mdspan.sub.extents]. The new section has the following contents.

1 Given a signed or unsigned integer IndexType, a type S is a canonical submdspan index type for IndexType if S is either IndexType or constant_wrapper<Value, IndexType> for some Value of type IndexType.

2 Given a signed or unsigned integer IndexType, a type S is a canonical submdspan slice type for IndexType if exactly one of the following is true:

  • (2.1) S is full_extent_t;

  • (2.2) S is a canonical submdspan index type for IndexType; or

  • (2.3) S is a specialization of strided_slice where all of the following are true:

    • (2.3.1) S::offset_type, S::extent_type, and S::stride_type are all canonical submdspan index types for IndexType; and

    • (2.3.2) if S::stride_type is constant_wrapper<Stride, IndexType> for some value Stride of type IndexType, and if S::extent_type is constant_wrapper<Extent, IndexType> for some value Extent of type IndexType, then either Extent equals zero, or Stride is greater than zero.

3 Given an extents specialization E and a rank index k of E, a type Sk is a canonical kth submdspan slice type for E if all of the following are true:

  • (3.1) Sk is a canonical submdspan slice type for E::index_type, and

  • (3.2) check-static-bounds<k>(E{}, slices...) does not equal check-static-bounds-result::out-of-bounds.

4 Given a signed or unsigned integer IndexType, a type S is a valid submdspan slice type for IndexType if exactly one of the following is true:

  • (4.1) is_convertible_v< S , full_extent_t> is true;

  • (4.2) is_convertible_v< S , IndexType> is true;

  • (4.3) S is a specialization of strided_slice; or

  • (4.4) if s is an object of type S, then the structured binding declaration auto [s0, s1] =s; is well-formed, and each of s0 and s1 are convertible to IndexType.

[Note 1: If S is a valid submdspan slice type for IndexType, then it is also a canonical slice type for IndexType. – end note]

5 Given an extents specialization E and a rank index k of E, a type Sk is a valid kth submdspan slice type for E if all of the following are true:

  • (5.1) Sk is a valid submdspan slice type for E::index_type, and

  • (5.2) check-static-bounds<k>(E{}, slices...) does not equal check-static-bounds-result::out-of-bounds.

[Note 2: If Sk is a valid kth submdspan slice type for E, then it is also a canonical kth submdspan slice type for E. – end note]

6 Given an extents specialization E, an object e of type E, and a rank index k of E, an object sk of type Sk is a valid kth submdspan slice for e if Sk is a valid kth submdspan slice type for E, and if all of the following are true:

  • (6.1) If Sk is a specialization of strided_slice, then either

  • (6.2) 0 ≤ first_<IndexType,k>(slices...)last_<k>(src, slices...)src.extent(k).

7 Given an extents specialization E, an object e of type E, and a rank index k of E, an object sk of type Sk is a canonical kth submdspan slice for e if sk is a valid kth submdspan slice for E, and if Sk is a canonical kth submdspan slice type for E.

template<class IndexType, size_t... Extents, class... Slices>
constexpr auto submdspan_canonicalize_slices(
  const extents<IndexType, Extents...>& src, Slices... slices);

8 Constraints: sizeof...(slices) equals sizeof...(Extents).

9 Mandates: For each rank index k of src, Sk is a valid kth submdspan slice type for extents<IndexType, Extents...>.

[Editorial Note: This cannot be a Constraint because it is impossible to constrain a template on whether a structured binding declaration is well-formed. This is because a compound requirement accepts an expression, not a declaration. – end note]

10 Preconditions: For each rank index k of src, sk is a valid kth submdspan slice for src.

11 Returns: a tuple of src.rank() elements, where for each rank index k of src, the following specifies element k of the return value:

  • (11.1) If is_convertible_v< Sk , full_extent_t> is true, then full_extent;

  • (11.2) else, if Sk is_convertible_v< Sk , IndexType> is true, then canonical-ice<IndexType>(s);

  • (11.3) 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)};

  • (11.4) else, if the structured binding declaration auto [s_k0, s_k1] =sk; is well-formed, and if each of s_k0 and s_k1 are convertible to IndexType, then strided_slice{.offset=canonical-ice<IndexType>(s_k0), .extent=subtract-ice<IndexType>(s_k0, s_k1), .stride=cw<IndexType(1)>}.

[Note 3: For each rank index k of src, element k of the tuple returned by submdspan_canonicalize_slices is a canonical kth submdspan slice for src. – end note]

9.5 Change section [mdspan.sub.extents]

Change section [mdspan.sub.extents] as follows.

template<class IndexType, class... Extents, class... SliceSpecifiers>
  constexpr auto submdspan_extents(const extents<IndexType, Extents...>& src,
                                   SliceSpecifiers... slices);

1 Constraints: sizeof...(Slices) equals Extents::rank().

2 Mandates: For each rank index k of src.extents(), exactly one of the following is true:Sk is a valid kth submdspan slice type for extents<IndexType, Extents...>.

3 Preconditions: For each rank index k of src, all of the following are true:sk is a valid kth submdspan slice for src.

4 Let SubExtents be a specialization of extents such that:

9.6 Requirements of all submdspan_mapping customizations

Right before [mdspan.sub.map.common] (“Specializations of submdspan_mapping), insert a new section [mdspan.sub.sliceable],”Sliceable layout mapping requirements,” with the following content.

template<layout-mapping-alike LayoutMapping>
constexpr auto
submdspan-mapping-with-full-extents(const LayoutMapping& mapping);

1 Constraints: LayoutMapping meets the layout mapping requirements ([mdspan.layout.reqmts]).

2 Returns: submdspan_mapping(mapping, P ), where P is a pack of typename LayoutMapping::extents_type::rank() object(s) of type full_extent_t.

[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]

template<class T>
constexpr bool is-submdspan-mapping-result = false;

template<class LayoutMapping>
constexpr bool is-submdspan-mapping-result<
  submdspan_mapping_result<LayoutMapping>> = true;

template<class LayoutMapping>
concept submdspan-mapping-result =
  is-submdspan-mapping-result<LayoutMapping>;

template<class LayoutMapping>
concept mapping-sliceable-with-full-extents =
  requires(const LayoutMapping& mapping) {
    {
      submdspan-mapping-with-full-extents(mapping)
    } -> submdspan-mapping-result;
  };

3 A type T models mapping-sliceable-with-full-extents only if T is an object type that meets the layout mapping requirements ([mdspan.layout.reqmts]).

[Editorial Note: The exposition-only concept mapping-sliceable-with-full-extents exists because it is impossible to constrain submdspan ([mdspan.sub.sub]) on whether submdspan_mapping(src.mapping(), slices...) is well-formed. This is because it is impossible to constrain a function template on whether one of its arguments (a member of the SliceSpecifiers pack, in this case) can be on the right-hand side of a structured binding declaration with two elements. This, in turn, is because a structured binding declaration is a declaration, but constraints cannot use declarations, only expressions. — end note]

template<layout-mapping-alike LayoutMapping, class... Slices>
constexpr auto
invoke-submdspan-mapping(const LayoutMapping& mapping, Slices... slices) {
  return submdspan_mapping(mapping, slices...);
}

4 Let M be a type, and let m be an object of type M. M meets the sliceable layout mapping requirements if all of the following are true.

  • (4.1) M models mapping-sliceable-with-full-extents.

  • (4.2) the expression invoke-submdspan-mapping(mapping, slices...) is well-formed only if sizeof...(slices) equals LayoutMapping::extent_type::rank().

[Editorial Note: This forms the Constraints on any submdspan_mapping customization. — end note]

  • (4.3) Let sizeof...(Slices) equal LayoutMapping::extents_type::rank(), and for every rank index k of LayoutMapping::extents_type, let Slices...[k] be a canonical kth submdspan slice type for LayoutMapping::extents_type. Then,

    • (4.3.1) the expression invoke-submdspan-mapping(mapping, slices...) is well-formed; and

    • (4.3.2) decltype(invoke-submdspan-mapping(mapping, slices...)) is a specialization of submdspan_mapping_result.

  • (4.4) Let sizeof...(Slices) equal LayoutMapping::extents_type::rank(). If there exists a k in [0, sizeof...(Slices)) such that Slices...[k] is not a canonical kth submdspan slice type for LayoutMapping::extents_type, then the expression invoke-submdspan-mapping(mapping, slices...) is ill-formed.

[Note 2: Restricting submdspan_mapping customizations to accept only canonical slice types makes it possible for the set of valid slice types to grow in the future. — end note]

[Editorial Note: It is our belief that this wording and the as-if rule together permit Standard layout mappings to accept any valid submdspan slice types, and thus permit submdspan not to canonicalize for Standard layout mappings. Here is alternate wording if we need a special call-out for Standard layout mappings.

  • (4.4) Let sizeof...(Slices) equal LayoutMapping::extents_type::rank().

    • (4.4.1) If LayoutMapping::layout_type is a layout defined in this Standard, then if there exists a rank index k of LayoutMapping::extents_type such that Slices...[k] is not a valid kth submdspan slice type for LayoutMapping::extents_type, then the expression invoke-submdspan-mapping(mapping, slices...) is ill-formed.

    • (4.4.2) Otherwise, if there exists a rank index k of LayoutMapping::extents_type such that Slices...[k] is not a canonical kth submdspan slice type for LayoutMapping::extents_type, then the expression invoke-submdspan-mapping(mapping, slices...) is ill-formed.

end note]

[Editorial Note: The above two points form the Mandates on any submdspan_mapping customization. — end note]

  • (4.5) Let sizeof...(Slices) equal LayoutMapping::extents_type::rank(), and for every rank index k of LayoutMapping::extents_type, let slices...[k] be a canonical kth submdspan slice for mapping.extents(). Let sub_map_offset be the result of invoke-submdspan-mapping(mapping, slices...). Then,

    • (4.5.1) sub_map_offset.mapping.extents() == submdspan_extents(src.mapping(), slices...) is true; and

    • (4.5.2) 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...))

is true.

[Editorial Note: This forms the Effects of submdspan_mapping, and the Preconditions on using any submdspan_mapping customization in submdspan ([mdspan.sub.sub]). — end note]

9.7 Change [mdspan.sub.map.common]

Change [mdspan.sub.map.common] as follows.

1 The following elements apply to all functions in [mdspan.sub.map].

2 Constraints: sizeof...(slices) equals extents_type::rank().

3 Mandates: For each rank index k of extents(), Sk is a valid kth submdspan slice type for extents_type.exactly one of the following is true:

4 Preconditions: For each rank index k of extents(), sk is a valid kth submdspan slice for extents().all of the following are true:

5 Let sub_ext be the result of submdspan_extents(extents(), slices...) and let SubExtents be decltype(sub_ext).

9.8 submdspan function template

Change [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(), slicescanonical_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 M shall meet the sliceable layout mapping requirements.

[Editorial Note: (4) cannot be fully implemented as a Constraint. The syntactic requirements have the effect of Mandates. The semantic requirements have the effect of Preconditions. — end note]

5 Constraints:

6 Mandates:

  • (6.1) decltype(submdspan_mapping(src.mapping(), slicescanonical_slices ...)) is a specialization of submdspan_mapping_result.

  • (6.2) is_same_v<remove_cvref_t<decltype(sub_map_offset.mapping.extents())>, decltype(submdspan_extents(src.mapping(), slices...))> is true.

7 Preconditions:

  • (7.2) sub_map_offset.mapping.extents() == submdspan_extents(src.mapping(), slices...) is true; and

  • (7.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...))

is true.

8 Effects: Equivalent to:

auto [...canonicalize_slices] =
  submdspan_canonicalize_slices(src.extents(), slices...);
auto sub_map_result = submdspan_mapping(src.mapping(), [canonical_]slices...);
return mdspan(src.accessor().offset(src.data(), sub_map_result.offset),
              sub_map_result.mapping,
              AccessorPolicy::offset_policy(src.accessor()));

[Editorial note: We believe that the above wording permits, but does not require canonicalization, for Standard layout mappings. If LWG does not agree, we offer the following alternative wording.

  • (8.1) If Layout is one of the layouts defined in this Standard and Layout::mapping<Extents> meets mapping-sliceable-with-full-extents, then equivalent to:
auto sub_map_result = submdspan_mapping(src.mapping(), slices...);
return mdspan(src.accessor().offset(src.data(), sub_map_result.offset),
              sub_map_result.mapping,
              AccessorPolicy::offset_policy(src.accessor()));
auto [...canonical_slices] =
  submdspan_canonicalize_slices(src.extents(), slices...);
auto sub_map_result = submdspan_mapping(src.mapping(), canonical_slices...);
return mdspan(src.accessor().offset(src.data(), sub_map_result.offset),
              sub_map_result.mapping,
              AccessorPolicy::offset_policy(src.accessor()));

end note]