std::mdspan (C++23)

Multi-dimensional array views without data ownership

The Multi-Dimensional Problem

Representing multi-dimensional data in C++ traditionally requires complex indexing or specialized containers.

// Traditional: Manual 2D indexing with 1D storage
std::vector<double> matrix(rows * cols);

// Access element at [row][col]
matrix[row * cols + col] = 42.0;  // Error-prone, hard to read

// Or use nested vectors (inefficient, not cache-friendly)
std::vector<std::vector<double>> matrix2(rows, std::vector<double>(cols));
matrix2[row][col] = 42.0;  // Better syntax, worse performance

C++23: std::mdspan

std::mdspan provides a multi-dimensional view over contiguous data with zero overhead.

#include <mdspan>
#include <vector>
#include <iostream>

// 1D storage
std::vector<double> data(100);  // 100 elements

// View as 10x10 matrix (zero overhead)
std::mdspan matrix(data.data(), 10, 10);

// Access with intuitive syntax
matrix[5, 3] = 42.0;  // Row 5, Column 3

std::cout << matrix.extent(0) << "x" << matrix.extent(1);  // 10x10
std::cout << matrix[5, 3];  // 42.0

Core Concepts

Non-Owning View

mdspan doesn't own data - it only provides multi-dimensional indexing. Data lifetime must be managed separately.

Extensible

Supports static and dynamic extents, custom layouts (row-major, column-major), and custom accessors.

Zero Overhead

Static extents (known at compile-time) have no runtime overhead compared to manual indexing.

Interoperable

Works with any contiguous memory: std::vector, std::array, C arrays, GPU memory, etc.

Creating mdspan Views

1. Dynamic Extents

// Runtime dimensions using dextents
std::vector<double> data(rows * cols);

// All dimensions are runtime values
auto matrix = std::mdspan(data.data(), rows, cols);

// Can also use std::dextents explicitly
std::mdspan<double, std::dextents<size_t, 2>> matrix2(
    data.data(), rows, cols
);

2. Static Extents

// Compile-time dimensions (zero overhead)
std::array<double, 100> data;

// 10x10 matrix with compile-time dimensions
std::mdspan<double, std::extents<size_t, 10, 10>> matrix(data.data());

// Mixed static/dynamic
std::mdspan<double, std::extents<size_t, 10, std::dynamic_extent>> matrix2(
    data.data(), cols  // Only cols is runtime
);

3. Multi-Dimensional (3D+)

// 3D tensor: depth x height x width
std::vector<double> data(depth * height * width);

std::mdspan tensor(data.data(), depth, height, width);

// Access element
tensor[z, y, x] = 1.0;

// Query dimensions
tensor.extent(0);  // depth
tensor.extent(1);  // height
tensor.extent(2);  // width

Subviews (slices)

Create views of portions of the data without copying:

std::vector<double> data(100);
std::mdspan matrix(data.data(), 10, 10);

// Slice: Get row 5
auto row5 = std::submdspan(matrix, 5, std::full_extent);
// row5 is a 1D mdspan of 10 elements

// Slice: Get column 3
auto col3 = std::submdspan(matrix, std::full_extent, 3);

// Slice: Get sub-matrix (rows 2-5, cols 3-7)
auto sub = std::submdspan(
    matrix, 
    std::tuple{2, 5},  // rows 2 to 5
    std::tuple{3, 7}   // cols 3 to 7
);
// sub is a 3x4 matrix view

Layout Policies

// Default: row-major (C-style)
std::mdspan<double, std::extents<size_t, 4, 4>> row_major(data.data());
// Memory: [0,0], [0,1], [0,2], [0,3], [1,0], [1,1]...

// Column-major (Fortran-style)
std::mdspan<double, std::extents<size_t, 4, 4>, std::layout_left> col_major(
    data.data()
);
// Memory: [0,0], [1,0], [2,0], [3,0], [0,1], [1,1]...

// Strided layout (custom strides)
std::layout_stride::mapping mapping(
    std::extents<size_t, 4, 4>{},  // 4x4 matrix
    std::array{8, 2}                // row stride 8, col stride 2
);
std::mdspan<double, std::extents<size_t, 4, 4>, std::layout_stride> strided(
    data.data(), mapping
);

Practical Example: Image Processing

struct Image {
    std::vector<uint8_t> pixels;
    size_t width, height;
    
    // 3-channel RGB view
    auto rgb_view() {
        return std::mdspan(
            pixels.data(), 
            height, 
            width, 
            3  // RGB channels
        );
    }
};

// Process image
void apply_grayscale(Image& img) {
    auto view = img.rgb_view();
    
    for (size_t y = 0; y < view.extent(0); ++y) {
        for (size_t x = 0; x < view.extent(1); ++x) {
            uint8_t r = view[y, x, 0];
            uint8_t g = view[y, x, 1];
            uint8_t b = view[y, x, 2];
            
            uint8_t gray = static_cast<uint8_t>(0.299 * r + 0.587 * g + 0.114 * b);
            
            view[y, x, 0] = gray;
            view[y, x, 1] = gray;
            view[y, x, 2] = gray;
        }
    }
}

// Get row without copying
auto get_row(Image& img, size_t row) {
    return std::submdspan(img.rgb_view(), row, std::full_extent, std::full_extent);
}

Comparison: mdspan vs span

Featurestd::spanstd::mdspan
Dimensions1D onlyN-dimensional
Indexingoperator[] (1 index)operator[] (multi-index)
LayoutContiguousCustomizable (row/col major, strided)
Use Case1D arrays, buffersMatrices, tensors, images

Best Practices

  • Use static extents when dimensions are known at compile-time
  • Pass mdspan by value (it's lightweight, like a pointer + extents)
  • Use submdspan to create views without copying data
  • Don't let the underlying data go out of scope while mdspan exists
  • Don't use mdspan when you need to own the data (use vector/array)