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;TElement 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).
ExtentsDimension 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).
LayoutPolicyMemory 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.
AccessorPolicyElement 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 8Dynamic 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 8Mixed 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 };| Feature | Static extent | Dynamic extent |
|---|---|---|
| Syntax | std::extents<size_t, 3, 4> | std::extents<size_t, dynamic_extent, dynamic_extent> |
| Size stored in | The type — zero runtime overhead | The mdspan object at runtime |
| Constructor args | Just the data pointer | Data pointer + size per dynamic dimension |
| Flexibility | Shape is fixed at compile time | Shape can vary at runtime |
| CTAD | Must specify full type explicitly | Deduced 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 8std::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| Policy | Offset formula (2D) | Fastest-varying dimension | Used by |
|---|---|---|---|
| layout_right | i × cols + j | Last (column) | C, C++, Python NumPy (default) |
| layout_left | j × rows + i | First (row) | Fortran, MATLAB, cuBLAS |
| layout_stride | custom strides per dim | Configurable | Sliced views, non-contiguous layouts |
Interface reference
| Member | Description |
|---|---|
| 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
| Approach | Problem | mdspan solves it by… |
|---|---|---|
| Raw pointer arithmetic: data[i * cols + j] | Error-prone, brittle when shape changes, no type safety | Encapsulating the index formula in a standardised, parameterised mapping |
| std::vector<std::vector<T>> | Heap-allocates each row separately; not contiguous; slow cache access | Being a view over a single flat contiguous block — optimal cache performance |
| Hand-rolled Matrix<T,R,C> class | Duplicated effort across every codebase; hard to pass to third-party APIs | Providing a standard ABI that any library can accept as a parameter |
| Eigen / BLAS raw arrays | Library-specific types; impedance mismatch when combining libraries | Standardised interoperability — pass the same buffer to different libraries with different layout policies |