std::mdspan

Multi-dimensional array views made simple

📊 What is std::mdspan?

std::mdspan is a C++23 feature that provides a non-owning view over multi-dimensional data. It allows you to work with matrices, tensors, and other multi-dimensional arrays efficiently and safely.


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

int main() {
    std::vector<int> data = {1, 2, 3, 4, 5, 6};
    std::mdspan matrix(data.data(), 2, 3);  // 2x3 matrix
    
    std::cout << "Element [1,2]: " << matrix[1, 2] << std::endl;
    return 0;
}
                                    

Output:

Element [1,2]: 6

Key Features of std::mdspan

👁️

Non-owning

Views existing data without copying

// No memory allocation
📐

Multi-dimensional

Support for any number of dimensions

mdspan<int, 3, 4, 5>
🔧

Flexible Layout

Customizable memory layout patterns

// Row-major, column-major

Zero Overhead

Compile-time optimizations

// No runtime cost

🔹 Basic 2D Matrix

A 2D matrix view provides efficient access to two-dimensional data without copying. Implemented via a class or a span, it maps a one-dimensional underlying array (like a std::vector) into rows and columns using index calculations (index = row * cols + col). This offers a natural interface for algorithms in image processing, linear algebra, and game grids. The view maintains the original data's layout and performance while presenting a convenient 2D abstraction for element access and iteration.

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

int main() {
    // Create data storage
    std::vector<int> data = {
        1, 2, 3,
        4, 5, 6,
        7, 8, 9
    };
    
    // Create 3x3 matrix view
    std::mdspan matrix(data.data(), 3, 3);
    
    // Access elements
    std::cout << "Matrix:" << std::endl;
    for (int i = 0; i < 3; ++i) {
        for (int j = 0; j < 3; ++j) {
            std::cout << matrix[i, j] << " ";
        }
        std::cout << std::endl;
    }
    
    return 0;
}

Output:

Matrix:
1 2 3 
4 5 6 
7 8 9

🔹 Dynamic Dimensions

Handling runtime-determined matrix dimensions requires flexible memory management. The number of rows and columns can be specified at runtime, necessitating dynamic allocation (e.g., std::vector). The matrix view must compute strides correctly to support non-contiguous data if needed. This flexibility is crucial for applications that load data from files or user input, such as scientific simulations, spreadsheet tools, or graphic applications where the data size is not known at compile time.

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

void print_matrix(std::mdspan<int, std::dynamic_extent, std::dynamic_extent> mat) {
    for (size_t i = 0; i < mat.extent(0); ++i) {
        for (size_t j = 0; j < mat.extent(1); ++j) {
            std::cout << mat[i, j] << " ";
        }
        std::cout << std::endl;
    }
}

int main() {
    std::vector<int> data = {10, 20, 30, 40};
    
    // 2x2 matrix
    std::mdspan matrix(data.data(), 2, 2);
    
    std::cout << "2x2 Matrix:" << std::endl;
    print_matrix(matrix);
    
    return 0;
}

Output:

2x2 Matrix:
10 20 
30 40

🔹 3D Tensor Example

A 3D tensor extends the matrix concept to three dimensions for volumetric data. It can be viewed as a vector of matrices or a single flat array with a more complex indexing scheme (index = (depth * rows + row) * cols + col). Tensors are fundamental in areas like 3D graphics (voxel data), multi-channel image processing (e.g., RGB with time), and machine learning (multidimensional batches). Efficient slicing and subview operations on tensors enable powerful data manipulations without duplication.

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

int main() {
    // 2x3x4 tensor (24 elements)
    std::vector<int> data(24);
    
    // Fill with sequential values
    std::iota(data.begin(), data.end(), 1);
    
    // Create 3D view
    std::mdspan tensor(data.data(), 2, 3, 4);
    
    std::cout << "3D Tensor [0][1][2]: " << tensor[0, 1, 2] << std::endl;
    std::cout << "3D Tensor [1][2][3]: " << tensor[1, 2, 3] << std::endl;
    
    // Print dimensions
    std::cout << "Dimensions: " << tensor.extent(0) << "x" 
              << tensor.extent(1) << "x" << tensor.extent(2) << std::endl;
    
    return 0;
}

Output:

3D Tensor [0][1][2]: 7
3D Tensor [1][2][3]: 24
Dimensions: 2x3x4

🔹 Subviews and Slicing

Subviews and slicing enable efficient access to contiguous or strided subsets of tensor data without copying. Operations like extracting a specific row, column, or rectangular region from a multi-dimensional array return a lightweight view object that references the original data. For example, a 3D tensor of shape (4, 5, 6) can be sliced to extract a 2×3 subregion starting at index (1, 2, 1) using tensor.slice({1, 2, 1}, {2, 3, 4}), returning a view with shape (2, 3, 4). Slices are defined using start indices, strides, and element counts for precise control. This capability is essential for performance-critical numerical computing, image processing tasks like region-of-interest extraction, and implementing algorithms that partition and manipulate large datasets efficiently without memory overhead.

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

int main() {
    std::vector<int> data = {
        1, 2, 3, 4,
        5, 6, 7, 8,
        9, 10, 11, 12
    };
    
    // Original 3x4 matrix
    std::mdspan matrix(data.data(), 3, 4);
    
    // Create subview (first 2 rows, first 3 columns)
    auto subview = std::submdspan(matrix, 
                                  std::pair{0, 2}, 
                                  std::pair{0, 3});
    
    std::cout << "Subview:" << std::endl;
    for (size_t i = 0; i < subview.extent(0); ++i) {
        for (size_t j = 0; j < subview.extent(1); ++j) {
            std::cout << subview[i, j] << " ";
        }
        std::cout << std::endl;
    }
    
    return 0;
}

Output:

Subview:
1 2 3 
5 6 7

🧠 Test Your Knowledge

What does std::mdspan provide?