Document number P4173R0
Date 2026-04-04
Audience LEWG
Reply-to Hewill Kang <hewillk@gmail.com>

iterator_accessor for mdspan

Abstract

Standard mdspan accessors, such as default_accessor, are specialized for use with raw pointers. While mdspan's design allows for custom accessors, providing a standard iterator-based accessor would simplify integration with non-contiguous C++ Ranges such as views and containers.

We propose iterator_accessor, which leverages random_access_iterator to decouple multi-dimensional views from physical memory continuity.

This utility enables seamless, zero-copy mdspan support for random-access ranges, further enhancing the composability between the Ranges and multi-dimensional data structures.

Revision history

R0

Initial revision.

Discussion

By default, mdspan provides accessors optimized for contiguous memory via raw pointers. Although designed as a general-purpose view, the standard default_accessor and aligned_accessor are strictly coupled with pointer-based data handles.

If we examine the requirements of the accessor, specifically the access and offset operations, a striking similarity emerges. Take default_accessor as an example:

  template<class ElementType>
  struct default_accessor {
    using offset_policy = default_accessor;
    using element_type = ElementType;
    using reference = ElementType&;
    using data_handle_type = ElementType*;

    constexpr default_accessor() noexcept = default;
    template<class OtherElementType>
      constexpr default_accessor(default_accessor<OtherElementType>) noexcept;
    constexpr reference access(data_handle_type p, size_t i) const noexcept;
    constexpr data_handle_type offset(data_handle_type p, size_t i) const noexcept;
  };

which performs two core operations

  • access(p, i): Returns p[i].
  • offset(p, i): Returns p + i.
  • These are exactly the fundamental operations defined by the random_access_iterator. A random-access iterator is the standard abstraction for any sequence that supports constant-time indexing and arbitrary offsets. The fact that default_accessor mirrors these operations suggests that mdspan is conceptually ready to support iterators, making a standard iterator-based accessor a natural and highly anticipated addition to the mdspan ecosystem.

    By introducing an empty iterator_accessor class with the following:

      template<random_access_iterator I>
      struct iterator_accessor {
        using offset_policy    = iterator_accessor;
        using data_handle_type = I;
        using reference        = iter_reference_t<I>;
        using element_type     = remove_reference_t<reference>;
    
        constexpr iterator_accessor() noexcept = default;
        constexpr iterator_accessor(I) noexcept {}                         // CTAD-convenient
    
        constexpr reference access(data_handle_type p, iter_difference_t<I> i) const 
          { return p[i]; }
        constexpr data_handle_type offset(data_handle_type p, iter_difference_t<I> i) const 
          { return p + i; }
      };

    we align the implementation of mdspan with its underlying mathematical logic. Instead of forcing data to be represented as a raw pointer, we allow any type that satisfies the random_access_iterator to serve as the data handle.

    A key observation is that the standard default_accessor<T> is conceptually and functionally equivalent to iterator_accessor<T*>, which suggests that default_accessor is merely a specialized instance of a more general iterator-based model, rather than a fundamentally distinct pointer-based design.

    This utility is meaningful because it provides a standardized mechanism to bridge abstract data sequences with multi-dimensional indexing, eliminating the need to reinvent access logic.

    Crucially, iterator_accessor enables mdspan to support ranges yielding proxy references, a capability fundamentally absent in pointer-based accessors.

    Benefiting from the following existing CTAD:

      template<class MappingType, class AccessorType>
        mdspan(const typename AccessorType::data_handle_type&, const MappingType&,
               const AccessorType&)
          -> mdspan<typename AccessorType::element_type, typename MappingType::extents_type,
                    typename MappingType::layout_type, AccessorType>;

    we can instantiate mdspan across various scenarios with minimal boilerplate:

    Example 1: Integration with Contiguous Ranges

      auto _3x3 = layout_right::mapping(extents(3, 3));
      auto l = {1, 2, 3, 4, 5, 6, 7, 8, 9};
      auto ms = mdspan(l.begin(), _3x3, iterator_accessor(l.begin())); // equivalent to mdspan(l.begin(), 3, 3)

    Example 2: Integration with Non-contiguous Ranges

      auto r = views::iota(0, 9);
      auto ms = mdspan(r.begin(), _3x3, iterator_accessor(r.begin()));

    Example 3: Handling Proxy References

      auto r = vector{true, false, true, false};
      auto ms = mdspan(r.begin(), _2x2, iterator_accessor(r.begin()));

    Example 4: Integration with Range adaptors

      vector<int> v1{1, 2, 3}, v2{4, 5}, v3{};
      array a{6, 7, 8};
      auto s = views::single(9);
      auto r = views::concat(v1, v2, v3, a, s);
      auto ms = mdspan(r.begin(), _3x3, iterator_accessor(r.begin()));

    Example 5: Enabling Multi-dimensional Move Semantics (with P3242)

      auto r = v | views::as_rvalue;
      auto ms = mdspan(r.begin(), _3x3, iterator_accessor(r.begin()));
      linalg::copy(ms, dst);

    Example 6: SoA-to-AoS mapping via views::zip

      vector posX = {0.0, 1.0, 2.0, 3.0};
      vector posY = {0.0, 0.5, 1.0, 1.5};
      vector mask = {1, 0, 1, 0};
    
      auto r = views::zip(posX, posY, mask);
      auto ms = mdspan(r.begin(), _2x2, iterator_accessor(r.begin()));
    
      auto [x, _, active] = ms[1, 0]; 
      if (active) {
        x += 10.0f;
      }

    Example 7: Abstracting Hardware Strides via views::stride

      // Physical buffer: Data is interleaved or padded (e.g., 128-bit alignment)
      vector<float> hardware_buffer = { /* large padded data */ };
    
      auto r = hardware_buffer | views::stride(4);
      auto ms = mdspan(r.begin(), _4x4, iterator_accessor(r.begin()));

    Design

    Explicit Constructor iterator_accessor(I)

    The inclusion of the constructor:

      constexpr iterator_accessor(I) noexcept { /* empty */ }

    is primarily intended to facilitate CTAD.

    While the constructor's body is empty, its signature allows the compiler to deduce the template parameter I directly from an iterator instance. Without this constructor, a user would be forced to explicitly specify the iterator type (e.g., iterator_accessor<decltype(r.begin())>{}), which is verbose.

    By providing this constructor, we enable a much cleaner syntax with iterator_accessor(r.begin()), which is more convenient for users.

    Deduction of element_type and reference

    The design of iterator_accessor must ensure that mdspan correctly deduces its template arguments through CTAD while maintaining compatibility with various iterators. A critical requirement of mdspan is that its first template argument, ElementType, must match the accessor's element_type. Furthermore, mdspan::value_type is strictly defined as remove_cv_t<element_type>.

    Earlier iterations of this design attempted to define element_type as remove_reference_t<iter_reference_t<I>>. While this correctly preserves const-qualification for contiguous iterators (e.g., const float* results in const float), it fails for proxy iterators as pointed out by Mark Hoemmen. For instance, with vector<bool> or views::zip, this would force element_type to be the vector<bool>::reference or tuple<double&, int&>, which unnecessarily couples the mdspan type to the internal implementation of the accessor and breaks the expectation that value_type represents the underlying data.

    The element_type should be defined as a cv-qualified version of iter_value_t<I>, while the accessor::reference alias is mapped directly to iter_reference_t<I>. This allows the accessor to return a proxy object through operator[] while letting mdspan maintain a clean, readable element_type such as plain bool.

    The determination of const-qualification is performed by inspecting the state of the iterator within the C++23 constant-iterator framework from P2278. By utilizing the constant-iterator concept checking, the accessor can determine if the iterator is read-only, regardless of whether it returns a true reference or a proxy. If the iterator is a constant-iterator, element_type is deduced as const iter_value_t<I>; otherwise, it is simply iter_value_t<I>.

    An alternative approach would be to use indirectly_writable<I, iter_value_t<I>> to detect mutability, though it remains an open question whether a capability-based check is more appropriate for the mdspan design philosophy than the previous one.

    Support for Conversions

    A key design principle for accessor is the strict mirroring of conversion semantics from the underlying data handle. The relationship between iterator_accessor<I> and iterator_accessor<I2> must faithfully reflect the relationship between the iterators I and I2.

    If an iterator I is constructible from I2, but I2 is not implicitly convertible to I, the accessor must maintain this exact distinction. To align with the design intent of mdspan, the conversion constructor can be defined as:

      template<typename I2>
        requires is_constructible_v<I, I2>
          explicit(!is_convertible_v<I2, I>)
            constexpr iterator_accessor(iterator_accessor<I2>) noexcept {}

    By using explicit(!is_convertible_v<I2, I>), the accessor correctly inherits the implicitness of the underlying iterator. This ensures that mdspan conversion works exactly as the user expects based on the behavior of their chosen iterator type. It should be noted that these conversions must also be constrained to prevent unsafe pointer-to-derived to pointer-to-base transitions that would invalidate multidimensional indexing.

    Furthermore, the accessor need include direct conversion paths to and from default_accessor<cv-element_type>, for example:

      default_accessor<      int> _ = iterator_accessor<      int*>(); // ok
      default_accessor<const int> _ = iterator_accessor<      int*>(); // ok
      default_accessor<      int> _ = iterator_accessor<const int*>(); // invalid
      default_accessor<const int> _ = iterator_accessor<const int*>(); // ok
    
      iterator_accessor<      int*> _ = default_accessor<      int>(); // ok
      iterator_accessor<      int*> _ = default_accessor<const int>(); // invalid
      iterator_accessor<const int*> _ = default_accessor<      int>(); // ok
      iterator_accessor<const int*> _ = default_accessor<const int>(); // ok

    which allows it to drop into standard mdspan contexts whenever the underlying iterator behaves like a raw pointer.

    Offset Type in access/offset

    According to the [mdspan.accessor] requirements, those two functions take a size_t offset, as the accessor is intended to be decoupled from and unaware of the specific index_type used by the layout mapping.

    The design adheres to this by using size_t at the interface and performing an internal static_cast<iter_difference_t<I>>(i).

    It should be noted that this is necessary as iterator models are only guaranteed to support arithmetic with their difference_type. Furthermore, we require that i be representable in difference_type as a Preconditions, guaranteeing that the mapping remains well-defined.

    Implementation experience

    The author implemented iterator_accessor based on libstdc++ along with the above example; see godbolt link for details.

    Proposed change

    This wording is relative to Latest Working Draft.

    1. Add a new feature-test macro to 17.3.2 [version.syn]:

      #define __cpp_lib_iterator_accessor YYYYMML // freestanding, also in <mdspan>
    2. Modify 21.4.1 [mdspan.syn] as indicated:

      // mostly freestanding
      namespace std {
        […]
        // [mdspan.accessor.aligned], class template aligned_accessor
        template<class ElementType, size_t ByteAlignment>
          class aligned_accessor;
      
        // [mdspan.accessor.iter], class template iterator_accessor
        template<random_access_iterator I>
          class iterator_accessor;
        […]
      }
      
    3. Add [mdspan.accessor.iter] after [mdspan.accessor.aligned] as indicated:

      23.? Overview [mdspan.accessor.iter.overview]

      namespace std {
        template<random_access_iterator I>
        struct iterator_accessor {
          using offset_policy    = iterator_accessor;
          using data_handle_type = I;
          using element_type     = see below;
          using reference        = iter_reference_t<I>;
      
          constexpr iterator_accessor() noexcept = default;
          constexpr iterator_accessor(I) noexcept {}
      
          template<class I2>
            constexpr explicit(see below) iterator_accessor(iterator_accessor<I2>) noexcept;
      
          constexpr iterator_accessor(default_accessor<element_type>) noexcept;
          constexpr iterator_accessor(default_accessor<remove_const_t<element_type>>) noexcept;
          constexpr iterator_accessor(default_accessor<remove_volatile_t<element_type>>) noexcept;
          constexpr iterator_accessor(default_accessor<remove_cv_t<element_type>>) noexcept;
      
          constexpr operator default_accessor<element_type>() noexcept;
          constexpr operator default_accessor<const element_type>() noexcept;
          constexpr operator default_accessor<volatile element_type>() noexcept;
          constexpr operator default_accessor<const volatile element_type>() noexcept;
      
          constexpr reference access(data_handle_type p, size_t i) const;
          constexpr data_handle_type offset(data_handle_type p, size_t i) const;
        };
      }

      -1- iterator_accessor meets the accessor policy requirements.

      -2- element_type is required to be a complete object type that is neither an abstract class type nor an array type.

      -3- Each specialization of iterator_accessor is a trivially copyable type that models semiregular.

      -4- [0, n) is an accessible range for an object p of type data_handle_type and an object of type iterator_accessor if and only if [p, p + n) is a valid range.

      -5- The member typedef-name element_type is defined as follows:

        (5.1) - If I models contiguous_iterator, then remove_reference_t<iter_reference_t<I>>.

        (5.2) - Otherwise, if I models constant-iterator, then const iter_value_t<I>.

        (5.3) - Otherwise, element_type denotes iter_value_t<I>.

      23.? Members [mdspan.accessor.iter.members]

      template<class I2>
        constexpr explicit(see below) iterator_accessor(iterator_accessor<I2>) noexcept;

      -1- Constraints: Let E2 be typename iterator_accessor<I2>::element_type.

        (1.1) - is_constructible_v<I, I2> is true.

        (1.2) - contiguous_iterator<I> && contiguous_iterator<I2> && !is_convertible_v<E2(*)[], element_type(*)[]> is false.

      -2- Effects: None.

      -3- Remarks: The expression inside explicit is equivalent to:

        !is_convertible_v<I2, I>
      constexpr iterator_accessor(default_accessor<element_type>) noexcept;

      -4- Constraints: I satisfies contiguous_iterator.

      -5- Effects: None.

      constexpr iterator_accessor(default_accessor<remove_const_t<element_type>>) noexcept;

      -6- Constraints: I satisfies contiguous_iterator and is_const_v<element_type> is true.

      -7- Effects: None.

      constexpr iterator_accessor(default_accessor<remove_volatile_t<element_type>>) noexcept;

      -8- Constraints: I satisfies contiguous_iterator and is_volatile_v<element_type> is true.

      -9- Effects: None.

      constexpr iterator_accessor(default_accessor<remove_cv_t<element_type>>) noexcept;

      -10- Constraints: I satisfies contiguous_iterator, is_const_v<element_type> is true, and is_volatile_v<element_type> is true.

      -11- Effects: None.

      constexpr operator default_accessor<element_type>() noexcept;

      -12- Constraints: I satisfies contiguous_iterator.

      -13- Effects: Equivalent to: return {};

      constexpr operator default_accessor<const element_type>() noexcept;

      -14- Constraints: I satisfies contiguous_iterator and is_const_v<element_type> is false.

      -15- Effects: Equivalent to: return {};

      constexpr operator default_accessor<volatile element_type>() noexcept;

      -16- Constraints: I satisfies contiguous_iterator and is_volatile_v<element_type> is false.

      -17- Effects: Equivalent to: return {};

      constexpr operator default_accessor<const volatile element_type>() noexcept;

      -18- Constraints: I satisfies contiguous_iterator, is_const_v<element_type> is false, and is_volatile_v<element_type> is false.

      -19- Effects: Equivalent to: return {};

      constexpr reference access(data_handle_type p, size_t i) const;

      -20- Preconditions: i is representable as a value of type iter_difference_t<I>.

      -21- Effects: Equivalent to: return p[static_cast<iter_difference_t<I>>(i)];

      constexpr data_handle_type offset(data_handle_type p, size_t i) const;

      -22- Preconditions: i is representable as a value of type iter_difference_t<I>.

      -23- Effects: Equivalent to: return p + static_cast<iter_difference_t<I>>(i);

    Acknowledgements

    The author would like to thank Arthur O'Dwyer and Mark Hoemmen for their valuable feedback and for providing insightful clarifications regarding mdspan.