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 performanceC++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.0Core 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); // widthSubviews (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 viewLayout 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
| Feature | std::span | std::mdspan |
|---|---|---|
| Dimensions | 1D only | N-dimensional |
| Indexing | operator[] (1 index) | operator[] (multi-index) |
| Layout | Contiguous | Customizable (row/col major, strided) |
| Use Case | 1D arrays, buffers | Matrices, 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)