Skip to content
C++

std::mdspan — Multidimensional Spans

C++ has always been used for numerical computing, image processing, and linear algebra — domains that need to interpret a flat block of memory as a two-dimensional matrix or a three-dimensional tensor. Before C++23, this required raw pointer arithmetic, fragile index calculations, or third-party libraries. std::mdspan is the standard answer: a non-owning view that wraps any contiguous sequence of objects and presents it through a multidimensional index operator, with configurable layout policies that can express row-major and column-major storage without any copy.

What std::mdspan is — and is not

std::mdspan is a view — it does not own the data it refers to. Like std::span for one-dimensional sequences, an mdspan is a lightweight handle: it stores a pointer to the data and the shape information. The underlying contiguous sequence can be a C-array, a pointer with a size, an std::array, an std::vector, or any other contiguous range. The mdspan simply decides how to map multidimensional indices onto that flat sequence.

The number of dimensions is called the rank. The size of each dimension is an extent. The product of all non-zero extents is the total size. You access elements using the multidimensional index operator [i, j] — a C++23 feature that allows the comma-separated index syntax directly inside brackets.

#include <mdspan>
#include <vector>
#include <print>

int main()
{
    std::vector data { 1, 2, 3, 4, 5, 6, 7, 8 };

    // Create a 2×4 view of the flat vector
    std::mdspan m { data.data(), 2, 4 };

    std::println("rank:    {}", m.rank());       // 2
    std::println("size:    {}", m.size());       // 8 = 2 × 4
    std::println("rows:    {}", m.extent(0));    // 2
    std::println("columns: {}", m.extent(1));    // 4

    // Multidimensional indexing with [i, j]
    for (std::size_t i = 0; i < m.extent(0); ++i) {
        for (std::size_t j = 0; j < m.extent(1); ++j)
            std::print("{} ", m[i, j]);
        std::println("");
    }
    // Output: 1 2 3 4
    //         5 6 7 8
}

The constructor call std::mdspan m {data.data(), 2, 4} uses class template argument deduction (CTAD), so you don't need to specify any template arguments manually in the simple case. The compiler deduces the element type from the pointer and the extents from the size arguments.

The four template parameters

std::mdspan is a class template with four parameters. Most code only needs to specify the first two; the last two have sensible defaults for the typical case.

template<
    class T,                                       // element type
    class Extents,                                 // number of dimensions + their sizes
    class LayoutPolicy   = std::layout_right,      // how dimensions map to memory
    class AccessorPolicy = std::default_accessor<T>// how elements are referenced
>
class mdspan;
T

Element type

The type of the elements in the multidimensional array. Can be int, double, or any other type. The mdspan stores a pointer to T (via the AccessorPolicy).

Extents

Dimension sizes

Specifies both the rank (number of dimensions) and the size of each dimension. Use std::extents<std::size_t, Dim0, Dim1, …> to set sizes. Each size can be a compile-time constant (static extent) or std::dynamic_extent (runtime-specified).

LayoutPolicy

Memory layout

Controls the formula that maps [i, j, …] index tuples to flat offsets. std::layout_right (default) is row-major: the last dimension varies fastest, as in C and Python. std::layout_left is column-major: the first dimension varies fastest, as in Fortran and MATLAB.

AccessorPolicy

Element access

Controls how the flat pointer is dereferenced. The default is std::default_accessor<T>, which just does pointer[offset]. Custom accessors can add bounds checking, apply transformations, or implement tiled memory patterns.

Static extents vs. dynamic extents

Each dimension of an std::mdspan can have its size specified at compile time (a static extent) or at runtime (a dynamic extent). Static extents make the shape part of the type — the compiler knows the dimensions and can optimise index calculations and eliminate bounds. Dynamic extents store the shape as runtime data and are more flexible, at the cost of a slightly larger object and slightly more work per index computation. The two can be mixed freely within a single std::extents.

Static extent — sizes baked into the type

std::vector data { 1, 2, 3, 4, 5, 6, 7, 8 };

// Both dimensions are compile-time constants: 2 rows, 4 columns
std::mdspan<int, std::extents<std::size_t, 2, 4>> m { data.data() };

std::println("rank: {}", m.rank());     // 2
std::println("rows: {}", m.extent(0));  // 2  (compile-time constant)
std::println("cols: {}", m.extent(1));  // 4  (compile-time constant)

for (std::size_t i = 0; i < m.extent(0); ++i) {
    for (std::size_t j = 0; j < m.extent(1); ++j)
        std::print("{} ", m[i, j]);
    std::println("");
}
// 1 2 3 4
// 5 6 7 8

Dynamic extent — sizes provided at runtime

// Both dimensions are runtime-specified
std::mdspan<int,
    std::extents<std::size_t,
                 std::dynamic_extent,
                 std::dynamic_extent>> m2 { data.data(), 4, 2 };

// Or equivalently, using CTAD — compiler deduces all template args:
std::mdspan m3 { data.data(), 4, 2 };   // 4 rows, 2 columns

for (std::size_t i = 0; i < m3.extent(0); ++i) {
    for (std::size_t j = 0; j < m3.extent(1); ++j)
        std::print("{} ", m3[i, j]);
    std::println("");
}
// 1 2
// 3 4
// 5 6
// 7 8

Mixed extents

// Row count fixed at compile time, column count at runtime:
std::mdspan<double,
    std::extents<std::size_t, 3, std::dynamic_extent>> mat { ptr, num_cols };

// Row count at runtime, column count fixed at 4:
std::mdspan<float,
    std::extents<std::size_t, std::dynamic_extent, 4>> mat2 { ptr, num_rows };
FeatureStatic extentDynamic extent
Syntaxstd::extents<size_t, 3, 4>std::extents<size_t, dynamic_extent, dynamic_extent>
Size stored inThe type — zero runtime overheadThe mdspan object at runtime
Constructor argsJust the data pointerData pointer + size per dynamic dimension
FlexibilityShape is fixed at compile timeShape can vary at runtime
CTADMust specify full type explicitlyDeduced from constructor arguments

Layout policies — row-major vs. column-major

The layout policy controls which formula the mdspan uses to convert a multidimensional index [i, j] into a flat offset into the underlying memory. This choice is critical when interfacing with numerical libraries: C, C++, and Python NumPy default to row-major storage; Fortran and MATLAB default to column-major. An mdspan with the wrong layout policy applied to a foreign buffer will return incorrect values — the data is the same but the interpretation differs.

Visualising layout_right vs layout_left for a 4×2 matrix

Memory:  [1, 2, 3, 4, 5, 6, 7, 8]

std::layout_right  (row-major, default)     std::layout_left  (column-major)
elements ordered:  1→2→3→4→5→6→7→8         elements ordered:  1↓3↓5↓7  2↓4↓6↓8

    col 0  col 1                                col 0  col 1
row 0: 1      2                            row 0: 1      5
row 1: 3      4                            row 1: 2      6
row 2: 5      6                            row 2: 3      7
row 3: 7      8                            row 3: 4      8
std::vector data { 1, 2, 3, 4, 5, 6, 7, 8 };
using Ext = std::extents<std::size_t, std::dynamic_extent, std::dynamic_extent>;

// Row-major (default): elements 1,2,3,4 are row 0
std::mdspan<int, Ext, std::layout_right> m_row { data.data(), 4, 2 };

// Column-major: elements 1,3,5,7 are column 0
std::mdspan<int, Ext, std::layout_left>  m_col { data.data(), 4, 2 };

// m_row[0, 0]=1, m_row[0, 1]=2, m_row[1, 0]=3, m_row[1, 1]=4
// m_col[0, 0]=1, m_col[0, 1]=5, m_col[1, 0]=2, m_col[1, 1]=6
PolicyOffset formula (2D)Fastest-varying dimensionUsed by
layout_righti × cols + jLast (column)C, C++, Python NumPy (default)
layout_leftj × rows + iFirst (row)Fortran, MATLAB, cuBLAS
layout_stridecustom strides per dimConfigurableSliced views, non-contiguous layouts

Interface reference

MemberDescription
md[i, j, …]Access the element at the given multidimensional index (C++23 multi-subscript)
md.rank()Returns the number of dimensions (constexpr)
md.size()Returns the product of all extents — the total number of elements
md.extent(i)Returns the size of dimension i
md.data_handle()Returns a pointer to the beginning of the contiguous data
md.mapping()Returns the layout mapping object — use to query strides, offsets
md.accessor()Returns the accessor policy object
md.is_unique()True if every index maps to a unique element (true for layout_right/left)
md.is_exhaustive()True if every element in the memory range is referenced by some index
md.is_strided()True if the mapping uses uniform strides per dimension

Practical patterns

Generic matrix function — works with any shape, any element type

// A function that accepts any 2-D mdspan of doubles
template <class Extents, class Layout>
void print_matrix(std::mdspan<const double, Extents, Layout> mat)
{
    for (std::size_t i = 0; i < mat.extent(0); ++i) {
        for (std::size_t j = 0; j < mat.extent(1); ++j)
            std::print("{:8.3f} ", mat[i, j]);
        std::println("");
    }
}

// Works for any shape and both row-major and column-major:
std::vector<double> buf(12, 1.0);
std::mdspan row_major { buf.data(), 3, 4 };                         // layout_right
std::mdspan<double, std::dextents<std::size_t,2>, std::layout_left>
    col_major { buf.data(), 3, 4 };                                 // layout_left

print_matrix(row_major);
print_matrix(col_major);

3-D tensor — same API, one more dimension

// A 2×3×4 tensor viewed over a flat array
std::vector<float> data(24);
std::iota(data.begin(), data.end(), 0.0f);

std::mdspan tensor { data.data(), 2, 3, 4 };

std::println("rank: {}", tensor.rank());          // 3
std::println("size: {}", tensor.size());          // 24

// Access: [batch, row, col]
for (std::size_t b = 0; b < tensor.extent(0); ++b)
    for (std::size_t r = 0; r < tensor.extent(1); ++r)
        for (std::size_t c = 0; c < tensor.extent(2); ++c)
            std::print("{} ", tensor[b, r, c]);

Passing a submatrix — layout_stride for non-contiguous slices

// std::submdspan (C++26, or third-party now) extracts a non-contiguous view
// Until then, layout_stride lets you describe any strided slice manually
std::vector<int> grid(100);
std::iota(grid.begin(), grid.end(), 0);

// 10×10 grid, then take every other row (strides: row_stride=20, col_stride=1)
std::mdspan<int,
    std::extents<std::size_t, 5, 10>,
    std::layout_stride>
every_other_row {
    grid.data(),
    std::layout_stride::mapping<std::extents<std::size_t, 5, 10>>{
        std::extents<std::size_t, 5, 10>{},
        std::array<std::size_t, 2>{20, 1}   // strides: 20 per row, 1 per column
    }
};

Why std::mdspan over the alternatives

ApproachProblemmdspan solves it by…
Raw pointer arithmetic: data[i * cols + j]Error-prone, brittle when shape changes, no type safetyEncapsulating the index formula in a standardised, parameterised mapping
std::vector<std::vector<T>>Heap-allocates each row separately; not contiguous; slow cache accessBeing a view over a single flat contiguous block — optimal cache performance
Hand-rolled Matrix<T,R,C> classDuplicated effort across every codebase; hard to pass to third-party APIsProviding a standard ABI that any library can accept as a parameter
Eigen / BLAS raw arraysLibrary-specific types; impedance mismatch when combining librariesStandardised interoperability — pass the same buffer to different libraries with different layout policies
Sign in to track progress