Skip to content
C++
Library
since C++23
Intermediate

std::mdspan

Multi-dimensional non-owning view over contiguous data with pluggable layout and accessor policies. C++23 replacement for raw pointer and stride arithmetic.

std::mdspansince C++23

A non-owning, zero-overhead, multi-dimensional view over a contiguous sequence of objects, parameterized by element type, extent shape, layout policy, and accessor policy.

Overview

std::mdspan (C++23, <mdspan>) generalizes std::span to N dimensions. It holds a data handle (typically a raw pointer) plus a mapping from multi-dimensional indices to a flat offset, then applies an accessor to convert that offset into a reference. All four concerns are separate, pluggable template parameters:

cpp
template<
    class T,                                        // element type
    class Extents,                                  // shape descriptor
    class LayoutPolicy   = std::layout_right,       // index-to-offset mapping
    class AccessorPolicy = std::default_accessor<T> // offset-to-reference conversion
> class mdspan;

Because the mapping and accessor inline to arithmetic at compile time, a well-optimized mdspan access generates identical machine code to hand-written ptr[i * stride + j].

Extents

std::extents<IndexType, Dims...> encodes rank and the size of each dimension. Individual dimensions are either static (embedded in the type) or dynamic (std::dynamic_extent, stored at runtime):

cpp
std::extents<int, 3, 4>                                       // fully static β€” both sizes in the type
std::extents<int, std::dynamic_extent, std::dynamic_extent>   // fully dynamic
std::extents<int, 3, std::dynamic_extent>                     // mixed β€” first static, second runtime
std::dextents<int, 2>                                         // convenience alias for all-dynamic rank-2

Static extents reduce storage and enable constant-folding in the mapping arithmetic. Dynamic extents store one IndexType per dynamic dimension in the mdspan object itself.

Element access β€” C++23 multi-dimensional subscript

C++23 extended operator[] to accept multiple arguments (P2128R6). m[i, j] is canonical; the older operator() workaround is no longer needed.

cpp
float data[12]{};
std::mdspan<float, std::extents<int, 3, 4>> m{data};

m[0, 0] = 1.0f;    // row 0, col 0  β€” C++23 multi-index subscript
m[2, 3] = 9.0f;

m.rank();           // 2   (compile-time constant β€” always static)
m.rank_dynamic();   // 0   (no dynamic extents in this instantiation)
m.extent(0);        // 3
m.extent(1);        // 4
m.size();           // 12  (total element count)
m.data_handle();    // float* to the underlying buffer

Syntax

Constructors

cpp
// Static extents β€” sizes fully encoded in the type
std::mdspan<float, std::extents<int, 3, 4>> m1{ptr};

// Dynamic extents β€” sizes passed at construction
std::mdspan<float, std::dextents<int, 2>> m2{ptr, rows, cols};

// Explicit mapping β€” required for layout_stride
std::mdspan<float, std::dextents<int, 2>, std::layout_stride> m3{ptr, mapping};

Layout policies (C++23)

PolicyMemory orderCanonical use
std::layout_right (default)Row-major (C-style)General purpose, cache-friendly row traversal
std::layout_leftColumn-major (Fortran-style)LAPACK, BLAS, column-traversal workloads
std::layout_strideArbitrary per-dimension stridesSub-matrices, padded buffers, interleaved channels
cpp
std::vector<float> buf(rows * cols);

// Column-major view β€” same buffer, Fortran order
std::mdspan<float, std::dextents<int, 2>, std::layout_left> col_major{
    buf.data(), rows, cols
};

// Strided view β€” interleaved data, col_stride=1, row_stride=2*cols
std::layout_stride::mapping<std::dextents<int, 2>> mapping{
    std::dextents<int, 2>{rows, cols},
    std::array<std::size_t, 2>{2 * cols, 1}  // {stride_dim0, stride_dim1}
};
std::mdspan<float, std::dextents<int, 2>, std::layout_stride> strided{buf.data(), mapping};

strided.stride(0);  // 2*cols β€” step in flat buffer to advance one row
strided.stride(1);  // 1      β€” step to advance one column

Examples

Layout-agnostic algorithm

The same function body operates correctly regardless of the underlying memory order:

cpp
template<typename T, typename Extents, typename Layout>
void fill_identity(std::mdspan<T, Extents, Layout> m) {
    static_assert(std::remove_cvref_t<decltype(m)>::rank() == 2);
    assert(m.extent(0) == m.extent(1));
    for (std::size_t i = 0; i < m.extent(0); ++i)
        for (std::size_t j = 0; j < m.extent(1); ++j)
            m[i, j] = (i == j) ? T{1} : T{0};
}

std::vector<float> buf(n * n);
fill_identity(std::mdspan<float, std::dextents<int, 2>>{buf.data(), n, n});
fill_identity(std::mdspan<float, std::dextents<int, 2>, std::layout_left>{buf.data(), n, n});

Matrix multiplication

cpp
void matmul(
    std::mdspan<const float, std::dextents<int, 2>> A,  // MΓ—K
    std::mdspan<const float, std::dextents<int, 2>> B,  // KΓ—N
    std::mdspan<float,       std::dextents<int, 2>> C   // MΓ—N output
) {
    const int M = A.extent(0), K = A.extent(1), N = B.extent(1);
    for (int i = 0; i < M; ++i)
        for (int j = 0; j < N; ++j) {
            float acc = 0.0f;
            for (int k = 0; k < K; ++k)
                acc += A[i, k] * B[k, j];
            C[i, j] = acc;
        }
}

4D image tensor (NCHW)

cpp
const int N = 8, C = 3, H = 224, W = 224;
std::vector<float> data(N * C * H * W);

std::mdspan<float, std::dextents<int, 4>> nchw{data.data(), N, C, H, W};

// Zero the red channel (channel 0) across all batches
for (int n = 0; n < nchw.extent(0); ++n)
    for (int h = 0; h < nchw.extent(2); ++h)
        for (int w = 0; w < nchw.extent(3); ++w)
            nchw[n, 0, h, w] = 0.0f;

Custom accessor for debug bounds checking

The accessor policy controls how a flat offset becomes a reference β€” the right place to add per-element validation without touching the layout or call sites:

cpp
template<typename T>
struct bounds_checked_accessor {
    using element_type     = T;
    using reference        = T&;
    using data_handle_type = T*;
    using offset_policy    = bounds_checked_accessor;

    reference access(data_handle_type p, std::ptrdiff_t i) const noexcept {
        return p[i];
    }
    data_handle_type offset(data_handle_type p, std::ptrdiff_t i) const noexcept {
        return p + i;
    }
};

#ifdef NDEBUG
using Accessor = std::default_accessor<float>;
#else
using Accessor = bounds_checked_accessor<float>;
#endif

std::mdspan<float, std::dextents<int, 2>, std::layout_right, Accessor>
    safe_m{buf.data(), rows, cols};

Sub-views with std::submdspan (C++26)

std::submdspan ships in C++26, not C++23. Verify your standard library version before using it:

cpp
// C++26 only
auto m = std::mdspan<float, std::extents<int, 6, 6>>{data};

// Block: rows [1,4), all columns β†’ 3Γ—6 mdspan with layout_stride
auto block = std::submdspan(m, std::pair{1, 4}, std::full_extent);

// Row slice β†’ rank-1 mdspan
auto row2  = std::submdspan(m, 2, std::full_extent);

// Column slice β†’ rank-1 mdspan with stride = 6
auto col3  = std::submdspan(m, std::full_extent, 3);

The result type automatically acquires layout_stride whenever the selected region cannot be represented by a simpler mapping.


Best Practices

Pass mdspan by value. It is a lightweight view (pointer + small inline metadata). Passing by const reference adds an indirection and prevents the compiler from placing fields in registers.

Use const element type for read-only parameters. mdspan<const float, ...> documents intent and enables aliasing optimizations. mdspan<float, E, L> implicitly converts to mdspan<const float, E, L> with the same extents and layout.

Use dextents for fully runtime-sized views. It avoids spelling out extents<int, dynamic_extent, dynamic_extent, ...> and is less error-prone when rank changes.

Match layout to your access pattern. Traversing rows in a layout_left (column-major) mdspan produces stride-rows accesses β€” devastating for cache on large matrices. Either choose the layout that matches your hottest loop, or restructure loops to match the layout.

Use layout_left for BLAS/LAPACK interop. Both libraries default to Fortran column-major order. A layout_left mdspan means no copy is needed to hand data to dgemm or similar routines.


Common Pitfalls

m[i, j] is C++23 β€” earlier standards silently miscompile it. In C++20 and earlier, the comma inside [] is the built-in comma operator: m[i, j] parses as m[(i, j)] = m[j], a 1D access into the flat buffer. It compiles without warning and produces wrong answers.

std::submdspan requires C++26, not C++23. Many articles present it as part of the original mdspan feature set; it was standardized a cycle later.

Default layout is row-major (layout_right). Passing a row-major mdspan to a BLAS routine that expects column-major silently interprets the data as transposed.

Strides in layout_stride map dimension index, not the matrix row. For a 3D (D0, D1, D2) tensor in row-major order, the stride array is {D1*D2, D2, 1} β€” not {D0, 1} as one might guess from 2D intuition.

mdspan does not own its data. The underlying buffer must outlive every view over it. Storing an mdspan as a class member while the source std::vector lives elsewhere is a dangling-pointer bug waiting to fire.

rank() is always a compile-time constant. Even for fully dynamic mdspans, the rank is encoded in the Extents type and resolved at compile time. rank_dynamic() gives the count of dimensions whose sizes are stored at runtime.


See Also

  • std::span (C++20) β€” 1D non-owning view; same ownership model, simpler interface
  • std::submdspan (C++26) β€” slicing and sub-view extraction from an existing mdspan
  • std::layout_right, std::layout_left, std::layout_stride (C++23) β€” standard layout policies
  • std::default_accessor (C++23) β€” the default accessor; baseline for custom implementations
  • std::extents, std::dextents (C++23) β€” extent specification types for shape encoding