библиотека — невладеющие представленияlibrary — non-owning views
Многомерное невладеющее представление данных, лежащих в одной непрерывной (или описываемой через layout policy) области памяти — обобщение std::span из C++20 на произвольное число измерений.A multidimensional non-owning view of data living in one contiguous (or layout-policy-describable) memory region — a generalization of C++20's std::span to an arbitrary number of dimensions.
std::mdspan<T, Extents, LayoutPolicy, AccessorPolicy> — невладеющее представление данных как N-мерного массива поверх единого буфера памяти. Extents задают размерность и размеры по каждой оси — статические (известные на этапе компиляции), динамические (известные в рантайме), или смешанные. LayoutPolicy определяет, как индексы [i, j, k] транслируются в линейный офсет: row-major/C-order по умолчанию через layout_right, column-major/Fortran-order через layout_left, или произвольный strided layout для нестандартной раскладки данных.A std::mdspan<T, Extents, LayoutPolicy, AccessorPolicy> is a non-owning view of data as an N-dimensional array over a single memory buffer. Extents specify the dimensionality and size along each axis — static (known at compile time), dynamic (known at runtime), or a mix of both. LayoutPolicy defines how indices [i, j, k] translate to a linear offset: row-major/C-order by default via layout_right, column-major/Fortran-order via layout_left, or an arbitrary strided layout for a non-standard data arrangement.
void process_matrix(double* data, std::size_t rows, std::size_t cols) {
for (std::size_t i = 0; i < rows; ++i) {
for (std::size_t j = 0; j < cols; ++j) {
// формула офсета разбросана по всему коду,// the offset formula is scattered across the code,
// легко ошибиться в row-major vs column-major// easy to mix up row-major vs column-major
data[i * cols + j] *= 2.0;
}
}
}
void process_matrix(std::mdspan<double, std::dextents<std::size_t, 2>> m) {
for (std::size_t i = 0; i < m.extent(0); ++i) {
for (std::size_t j = 0; j < m.extent(1); ++j) {
m[i, j] *= 2.0; // офсет считает layout policy, не программист// the layout policy computes the offset, not the programmer
}
}
}
Представление многомерных данных без владения/копирования до mdspan упиралось в одну из двух неудачных крайностей:Representing multidimensional data without owning/copying it, before mdspan, boiled down to one of two unsatisfying extremes:
Формула data[i * cols + j] (или её эквивалент для 3+ измерений) повторялась в каждой функции, работающей с "плоским" буфером как с матрицей — при рефакторинге размерности или смене row-major на column-major нужно было находить и исправлять каждое вхождение вручную, легко пропустив одно.The formula data[i * cols + j] (or its equivalent for 3+ dimensions) was repeated in every function treating a "flat" buffer as a matrix — refactoring the dimensionality, or switching row-major to column-major, meant hunting down and fixing every occurrence by hand, easy to miss one.
Eigen, xtensor и подобные библиотеки решали ту же задачу со своими собственными типами матриц/тензоров — код, использующий одну библиотеку, не мог напрямую передать данные в код, написанный поверх другой, без явной конвертации или копирования, а выбор библиотеки становился архитектурным решением всего проекта.Eigen, xtensor, and similar libraries solved the same problem with their own matrix/tensor types — code using one library couldn't directly hand data to code built on another without an explicit conversion or copy, and picking a library became a project-wide architectural decision.
mdspan даёт способ описать "как эти байты интерпретировать как N-мерный массив", общий для всей стандартной библиотеки и не зависящий от выбора сторонней библиотеки линейной алгебры — функция, принимающая mdspan, может получить данные из любого источника (сырой буфер, vector, память, отображённая через mmap), не зная заранее, откуда именно они пришли.mdspan provides a way to describe "how to interpret these bytes as an N-dimensional array" that's common to the whole standard library and independent of any third-party linear-algebra library choice — a function accepting an mdspan can receive data from any source (a raw buffer, a vector, memory mapped via mmap) without knowing in advance exactly where it came from.
Под капотом mdspan, как и span (см. cpp20/span.html), — это просто указатель плюс метаданные, ничего не аллоцирующие и не освобождающие. Разница со span в том, что метаданных больше: не одно число "длина", а объект Extents, описывающий размер по каждой из N осей, плюс LayoutPolicy, отвечающий за перевод многомерного индекса в линейный офсет внутри буфера.Under the hood, mdspan — like span (see cpp20/span.html) — is just a pointer plus metadata, allocating and freeing nothing. The difference from span is that there's more metadata: not a single "length" number, but an Extents object describing the size along each of N axes, plus a LayoutPolicy responsible for translating a multidimensional index into a linear offset inside the buffer.
#include <mdspan>
double data[12] = {1,2,3,4, 5,6,7,8, 9,10,11,12};
// статические extents — 3 строки, 4 столбца, известно на этапе компиляции// static extents — 3 rows, 4 columns, known at compile time
std::mdspan<double, std::extents<std::size_t, 3, 4>> m(data);
for (std::size_t i = 0; i < m.extent(0); ++i)
for (std::size_t j = 0; j < m.extent(1); ++j)
m[i, j] = 0.0; // многомерный operator[] из этого же стандарта// the multidimensional operator[] from this same standard
// см. cpp23/multidim-subscript.html — новый operator[] с несколькими аргументами// see cpp23/multidim-subscript.html — the new multi-argument operator[]
Смена layout policy меняет только формулу офсета, не требуя ничего от вызывающего кода, работающего через operator[] — но, как показано ниже, layout_right и layout_left дают разную физическую раскладку одних и тех же логических индексов в памяти:Switching the layout policy only changes the offset formula, requiring nothing from calling code that goes through operator[] — but, as shown below, layout_right and layout_left give the same logical indices a different physical layout in memory:
int buf[6] = {0, 1, 2, 3, 4, 5};
// layout_right (по умолчанию) — row-major/C-order: строка идёт подряд в памяти// layout_right (default) — row-major/C-order: a row is contiguous in memory
std::mdspan<int, std::extents<std::size_t, 2, 3>, std::layout_right> row_major(buf);
// row_major[0, 1] == buf[1], row_major[1, 0] == buf[3]// row_major[0, 1] == buf[1], row_major[1, 0] == buf[3]
// layout_left — column-major/Fortran-order: столбец идёт подряд в памяти// layout_left — column-major/Fortran-order: a column is contiguous in memory
std::mdspan<int, std::extents<std::size_t, 2, 3>, std::layout_left> col_major(buf);
// col_major[0, 1] == buf[2], col_major[1, 0] == buf[1] — те же индексы, другой буфер-офсет// col_major[0, 1] == buf[2], col_major[1, 0] == buf[1] — same indices, different buffer offset
Данные, полученные из файла или сетевого сообщения как один непрерывный массив, интерпретируются как 2D-таблица без единой лишней аллокации.Data received from a file or network message as one contiguous array, interpreted as a 2D table with zero extra allocations.
std::vector<float> load_raw_samples(const std::filesystem::path& p);
void analyze(const std::filesystem::path& p, std::size_t channels, std::size_t frames) {
std::vector<float> raw = load_raw_samples(p);
std::mdspan<float, std::dextents<std::size_t, 2>> samples(raw.data(), channels, frames);
for (std::size_t c = 0; c < channels; ++c)
for (std::size_t f = 0; f < frames; ++f)
process_sample(samples[c, f]);
}
Обработка изображений, где число каналов известно на этапе компиляции (например, всегда RGB — 3), а ширина и высота известны только в рантайме.Image processing where the channel count is known at compile time (say, always RGB — 3), but width and height are only known at runtime.
using ImageView = std::mdspan<std::uint8_t, std::extents<std::size_t, std::dynamic_extent, std::dynamic_extent, 3>>;
void to_grayscale(ImageView img) {
for (std::size_t y = 0; y < img.extent(0); ++y)
for (std::size_t x = 0; x < img.extent(1); ++x) {
auto gray = (img[y, x, 0] + img[y, x, 1] + img[y, x, 2]) / 3;
img[y, x, 0] = img[y, x, 1] = img[y, x, 2] = static_cast<std::uint8_t>(gray);
}
}
Численные библиотеки вроде BLAS/LAPACK исторически ожидают column-major раскладку — mdspan с layout_left позволяет работать с тем же буфером в C++-стиле индексов, не переставляя данные в памяти.Numerical libraries like BLAS/LAPACK historically expect column-major layout — an mdspan with layout_left lets you work with the same buffer using C++-style indices without rearranging the data in memory.
extern "C" void dgemm_(/* BLAS-сигнатура, ожидает column-major буфер *//* BLAS signature, expects a column-major buffer */ /*...*//*...*/);
void wrap_blas_result(double* fortran_buf, std::size_t rows, std::size_t cols) {
std::mdspan<double, std::dextents<std::size_t, 2>, std::layout_left> m(fortran_buf, rows, cols);
print_matrix(m); // корректные индексы без транспонирования данных// correct indices without transposing the data
}
Извлечение прямоугольного региона внутри большей матрицы для локальной обработки — аналог span::subspan, но для многомерного случая.Extracting a rectangular region within a larger matrix for local processing — the analogue of span::subspan, but for the multidimensional case.
void blur_region(std::mdspan<float, std::dextents<std::size_t, 2>> image,
std::size_t row0, std::size_t col0, std::size_t rows, std::size_t cols) {
auto region = std::submdspan(image,
std::pair{row0, row0 + rows}, std::pair{col0, col0 + cols});
// region смотрит внутрь того же буфера, что и image, без копии// region points into the same buffer as image, no copy
apply_kernel(region);
}
Ровно как span и string_view (см. соответствующие гочи в cpp20/span.html и cpp17/string-view.html), mdspan становится висячим, если исходный буфер уничтожается или реаллоцируется раньше самого mdspan — например, mdspan над данными std::vector, который затем растёт через push_back и переезжает в новую память. Это тот же класс use-after-free багов, компилятор не предупреждает.Exactly like span and string_view (see the corresponding gotchas in cpp20/span.html and cpp17/string-view.html), an mdspan goes dangling if the source buffer is destroyed or reallocated before the mdspan itself — for instance, an mdspan over a std::vector's data that then grows via push_back and moves to new memory. It's the same class of use-after-free bugs, and the compiler gives no warning.
mdspan с layout_right и mdspan с layout_left для одних и тех же Extents — формально разные типы, не конвертируемые друг в друга неявно (в отличие от static->dynamic extent у обычного span). Это иногда удивляет при попытке написать generic-функцию, "принимающую любой mdspan" — без явного шаблонного параметра под layout такая функция либо жёстко фиксирует один layout, либо должна сама быть шаблоном по нему.An mdspan with layout_right and an mdspan with layout_left for the same Extents are formally distinct types, not implicitly convertible into each other (unlike the static-to-dynamic extent conversion on a plain span). This sometimes surprises when writing a generic function meant to "accept any mdspan" — without an explicit template parameter for the layout, such a function either hard-codes one layout or must itself be templated on it.
Если span (см. cpp20/span.html) уже понятен — mdspan решает ту же задачу (невладеющее представление непрерывных данных), только в N измерениях вместо одного: там, где span хранит одно число "длина", mdspan хранит объект Extents с размером по каждой оси, а там, где у span тривиальный офсет "индекс = офсет", у mdspan за это отвечает LayoutPolicy.If span (see cpp20/span.html) is already familiar — mdspan solves the same problem (a non-owning view of contiguous data), just in N dimensions instead of one: where span stores a single "length" number, mdspan stores an Extents object with a size per axis, and where span has a trivial "index = offset" mapping, mdspan delegates that to a LayoutPolicy.