Как std::mdspan отделяет многомерную индексацию от владения данными?
std::mdspan — это невладеющее представление многомерного массива. Оно хранит ссылку или указатель на существующие элементы, описание размеров и правила преобразования многомерных индексов в смещение, но не управляет временем жизни данных. Благодаря этому один и тот же буфер можно интерпретировать с разными размерностями и схемами размещения без копирования.
До появления std::mdspan многомерные данные часто представляли как вложенные контейнеры, вручную вычисляли смещения или передавали указатель вместе с набором размеров. Такие решения смешивали хранение данных, их форму и способ размещения, из-за чего появлялись дублирование логики и ошибки в арифметике индексов.
Подход mdspan, стандартизированный в C++23, выделяет представление массива в отдельный объект. Это особенно важно для численных библиотек, где один буфер может быть размещён построчно, по столбцам или с произвольными шагами.
Пусть данные хранятся в плоском буфере, но алгоритм должен обращаться к ним как к матрице. Самостоятельное вычисление смещения требует помнить размер строки и выбранный порядок размещения. Ошибка в формуле приводит к обращению не к тому элементу, а вложенный std::vector добавляет отдельные выделения памяти и не гарантирует единого непрерывного буфера.
mdspan не продлевает время жизни буфера. Если исходный контейнер уничтожен или перевыделил память, представление становится недействительным. Кроме того, обычная индексация не обязана выполнять проверку границ, поэтому корректность индексов остаётся ответственностью вызывающего кода.
Тип std::mdspan параметризован несколькими составляющими: типом элемента, описанием размерностей, политикой размещения и политикой доступа. Размерности могут быть статическими или динамическими; политика std::layout_right по умолчанию задаёт размещение, при котором последний индекс изменяется быстрее всего.
При вызове представления с индексами mdspan(i, j) они передаются его отображению. Отображение вычисляет линейное смещение, а accessor превращает это смещение в доступ к элементу. Поэтому код работы с индексами не зависит напрямую от формулы размещения.
В примере matrix(1, 2) обращается к шестому элементу плоского буфера: при layout_right смещение равно 1 * 3 + 2. Сам объект matrix не владеет data, поэтому сначала должен жить контейнер, содержащий массив.
Для другого порядка размещения можно выбрать std::layout_left, а для нерегулярных шагов — std::layout_stride. Статические размерности позволяют зафиксировать часть формы в типе и иногда обнаруживать ошибки уже при компиляции; динамические размерности удобнее для данных, форма которых известна только во время выполнения.
Главный компромисс — отсутствие владения и обычно отсутствие проверки границ ради минимальной стоимости представления и эффективной работы с существующей памятью. Если требуется владение, его следует выразить отдельно, например через std::vector, а mdspan создавать только на время доступа к его данным.
В библиотеке обработки изображений кадр хранится в одном std::vector<float>, но разные алгоритмы получают его как матрицу. Рассматривались два варианта: передавать указатель и размеры, что дёшево, но оставляет арифметику индексов в каждом алгоритме, или использовать std::vector<std::vector<float>>, что упрощает синтаксис, но создаёт вложенное хранение и лишние выделения.
Выбран std::mdspan поверх плоского вектора. Алгоритмы получили единый интерфейс многомерной индексации без копирования, а владелец буфера остался отдельно и явно управляет временем жизни. Для изображения с шагом строки, превышающим ширину видимой области, применена политика layout_stride, поэтому представление учитывает реальное размещение памяти без перекладки данных.
Копируется только небольшой объект-представление: указатель или handle, описание размерностей и связанные политики. Элементы не копируются, поэтому две копии mdspan обычно обращаются к одной и той же памяти. Это делает его похожим на невладеющий view, но одновременно требует самостоятельно контролировать время жизни исходного буфера.
Обычный доступ через operator() не следует считать проверяемым: стандартный интерфейс ориентирован на производительность, и корректные индексы должен обеспечивать вызывающий код. Наличие динамической размерности не означает автоматического исключения при неверном индексе. Если проверка нужна, её следует выполнить отдельно до обращения или предоставить безопасную оболочку на уровне приложения.
Да. Для стандартного размещения по столбцам используется std::layout_left, а для явно заданных расстояний между элементами — std::layout_stride. Также можно применять совместимую пользовательскую политику отображения, если она удовлетворяет требованиям mdspan. Это меняет вычисление смещения, но не переносит владение данными в mdspan и не исправляет проблемы времени жизни буфера.