After span and lifetime and templates, std::mdspan (C++23) extends borrowing to multidimensional coordinates. The installed GCC/libstdc++ supports the real standard type; this lesson does not substitute nested vectors or a custom view.
A handle, extents, mapping and accessor
The data handle identifies existing storage. Extents describe the logical shape; static dimensions live in the type and dynamic dimensions are supplied at runtime. A layout mapping turns coordinates into offsets. An accessor turns the handle and offset into an element reference. The default accessor uses ordinary element access; custom accessors can serve other storage models.
#include <mdspan>
#include <array>
#include <cstdio>
int main() {
std::array<double,6> data{1,2,3,4,5,6};
std::mdspan<double,std::extents<std::size_t,2,3>> matrix(data.data());
auto copy=matrix;copy[1,2]=9;
std::printf("same storage %d, value %.0f\n",copy.data_handle()==matrix.data_handle(),data[5]);
}A copied view still borrows the same elements. Resizing/reallocating a backing
vector can invalidate the handle. const mdspan<double,...> makes the view
object const, while its elements remain writable; mdspan<const double,...>
provides read-only element access. This is the same distinction between a
const pointer and a pointer to const data.
Logical size does not imply a contiguous layout
layout_right makes the rightmost index vary fastest. layout_left makes the leftmost index fastest. layout_stride accepts a unique strided mapping, which can include padding. An mdspan is not inherently a contiguous matrix; do not flatten by size() unless the mapping and accessor support that operation.
For a two-row, three-column padded view with row stride 5, logical size is 6, but the last element lies at offset 7. The required span is therefore 8, not 6 and not necessarily the full rows×stride allocation. Extents count elements; strides in the default accessor mapping count element offsets, not bytes.
#include <mdspan>
#include <array>
#include <cstdio>
int main() {
double backing[8]{};
using View=std::mdspan<double,std::dextents<std::size_t,2>,std::layout_stride>;
View::mapping_type mapping(std::dextents<std::size_t,2>(2,3),std::array<std::size_t,2>{5,1});
View padded(backing,mapping);padded[1,2]=42;
std::printf("logical %zu, required %zu, last %.0f\n",padded.size(),mapping.required_span_size(),backing[7]);
}Validate before constructing or indexing
The mapping constructor has preconditions; it is not a bounds validator for
an arbitrary pointer and shape. The exercise's row-major padded factory accepts
positive row_stride >= columns. For nonempty extents it checks
(rows-1)*row_stride + columns without overflowing size_t, then checks the
claimed accessible storage and null-pointer rule. An empty dimension needs no
elements, but stride preconditions still apply.
The caller must actually own the number of accessible elements it claims. mdspan cannot inspect an allocation through a raw pointer. An invalid logical coordinate is still invalid even if the computed offset happens to be inside a larger buffer. Assertions or sanitizers do not replace input validation.
For transpose, validate both output dimensions before writing. The build accepts left/right/padded layouts and requires nonoverlapping logical input/output storage; it does not promise an in-place overlapping transpose. Keep ownership outside these helpers and use bounded small matrices to test indexing, shape, padding and read-only access independently of performance claims.