| Document number | P4173R0 |
| Date | 2026-04-04 |
| Audience | LEWG |
| Reply-to | Hewill Kang <hewillk@gmail.com> |
iterator_accessor for mdspan
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.
Initial revision.
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()));
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.
element_type and referenceThe 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.
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.
access/offsetAccording 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.
The author implemented iterator_accessor based on libstdc++ along with the above example; see godbolt link for details.
This wording is relative to Latest Working Draft.
Add a new feature-test macro to 17.3.2 [version.syn]:
#define __cpp_lib_iterator_accessor YYYYMML // freestanding, also in <mdspan>
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;
[…]
}
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_accessormeets the accessor policy requirements.-2-
element_typeis required to be a complete object type that is neither an abstract class type nor an array type.-3- Each specialization of
iterator_accessoris a trivially copyable type that modelssemiregular.-4- [0, n) is an accessible range for an object
pof typedata_handle_typeand an object of typeiterator_accessorif and only if [p, p + n) is a valid range.-5- The member typedef-name
element_typeis defined as follows:
(5.1) - If
Imodelscontiguous_iterator, thenremove_reference_t<iter_reference_t<I>>.(5.2) - Otherwise, if
Imodelsconstant-iterator, thenconst iter_value_t<I>.(5.3) - Otherwise,
element_typedenotesiter_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
E2betypename iterator_accessor<I2>::element_type.
(1.1) -
is_constructible_v<I, I2>istrue.(1.2) -
contiguous_iterator<I> && contiguous_iterator<I2> && !is_convertible_v<E2(*)[], element_type(*)[]>isfalse.-2- Effects: None.
-3- Remarks: The expression inside
explicitis equivalent to:!is_convertible_v<I2, I>constexpr iterator_accessor(default_accessor<element_type>) noexcept;-4- Constraints:
Isatisfiescontiguous_iterator.-5- Effects: None.
constexpr iterator_accessor(default_accessor<remove_const_t<element_type>>) noexcept;-6- Constraints:
Isatisfiescontiguous_iteratorandis_const_v<element_type>istrue.-7- Effects: None.
constexpr iterator_accessor(default_accessor<remove_volatile_t<element_type>>) noexcept;-8- Constraints:
Isatisfiescontiguous_iteratorandis_volatile_v<element_type>istrue.-9- Effects: None.
constexpr iterator_accessor(default_accessor<remove_cv_t<element_type>>) noexcept;-10- Constraints:
Isatisfiescontiguous_iterator,is_const_v<element_type>istrue, andis_volatile_v<element_type>istrue.-11- Effects: None.
constexpr operator default_accessor<element_type>() noexcept;-12- Constraints:
Isatisfiescontiguous_iterator.-13- Effects: Equivalent to:
return {};constexpr operator default_accessor<const element_type>() noexcept;-14- Constraints:
Isatisfiescontiguous_iteratorandis_const_v<element_type>isfalse.-15- Effects: Equivalent to:
return {};constexpr operator default_accessor<volatile element_type>() noexcept;-16- Constraints:
Isatisfiescontiguous_iteratorandis_volatile_v<element_type>isfalse.-17- Effects: Equivalent to:
return {};constexpr operator default_accessor<const volatile element_type>() noexcept;-18- Constraints:
Isatisfiescontiguous_iterator,is_const_v<element_type>isfalse, andis_volatile_v<element_type>isfalse.-19- Effects: Equivalent to:
return {};constexpr reference access(data_handle_type p, size_t i) const;-20- Preconditions:
iis representable as a value of typeiter_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:
iis representable as a value of typeiter_difference_t<I>.-23- Effects: Equivalent to:
return p + static_cast<iter_difference_t<I>>(i);
The author would like to thank Arthur O'Dwyer and Mark Hoemmen for their valuable feedback and for providing
insightful clarifications regarding mdspan.