A Multidimenziós view és numeric utility térkép
(31.5) részben az mdspan alapvető használatát, a
std::span-tól való eltérését és a multidimenzionós nézet
gondolatát rögzítettük. A static operator(),
többdimenziós indexelés és lambdaváltozások (40.3) rész a C++23-as
többdimenziós operator[] nyelvi hátterét adta, a Többdimenziós tömbök, VLA és layout (7.2) rész
pedig a C tömbök sor-major layoutját. Most ezekre építve a layoutok, az
accessorok és a C-interop szerződéseit vesszük sorra. A cél nem az
mdspan újratanítása, hanem az, hogy saját layout- és
accessor-policyt tudjunk indokoltan készíteni, és felismerjük, hol ér
véget a memóriabiztonság, amit egy accessor nyújthat.
A laborprofil továbbra is ISO C++23 (-std=c++23), a
C++20-as rész profiljának kiterjesztéseként. A C++23-as
mdspan és a C++20-as atomic_ref elérhetősége
fordító- és standard library-függő; ezt a fejezet nem tekinti
ellenőrzöttnek. A példák ezért feature-test makrót és compile probe-ot
tartalmaznak, a konkrét GCC/Clang és libstdc++/libc++ verziót, valamint
a __cpp_lib_mdspan tényleges értékét a labornak kell
rögzítenie.
Az extents a multidimenzionális index tér dimenziónkénti
kiterjedését írja le. A statikus kiterjedés a típus része, ezért compile
time ismert; a dinamikus kiterjedés futásidejű érték. A C++23-as
std::extents<std::size_t, 3, std::dynamic_extent, 4>
például háromdimenziós extents, amelynek első és harmadik kiterjedése
statikus, a középső dinamikus. A
std::dextents<std::size_t, 2> mindkét dimenzióban
dinamikus.
A statikus extents előnye, hogy a méret a típusban van, így a mapping számítása optimalizálható, és a hibás dimenziószám már fordításkor kiderül. Hátránya, hogy a méret a típus részévé válik, ezért eltérő méretekhez eltérő típus tartozik. A dinamikus extents rugalmasabb, de a kiterjedés értékét futásidőben kell átadni, és a mapping számítása is futásidejű adatokra támaszkodik.
Az mdspan első három template paramétere az elem típusa,
az extents és a layout policy; a negyedik az accessor policy. A
std::mdspan<int, std::dextents<std::size_t, 2>>
tehát kétdimenziós, dinamikus kiterjedésű, alapértelmezett
layout_right és default_accessor<int>
típus. A rank(), extent(r),
static_extent(r) és stride(r) tagfüggvények
adják a metaadatot. Az mdspan nem tulajdonol: a mögöttes
puffer élettartama a hívó felelőssége.
A layout policy a logikai indexeket lineáris offsetre képezi. Kétdimenziós esetben a három alapvető mapping:
A layout_right a jobb oldali indexet, a
layout_left a bal oldali indexet tekinti leggyorsabban
változónak. A C tömbök sor-major elrendezése a
layout_right-nak felel meg; a Fortran-oszlop-major a
layout_left-nak. A layout_stride explicit
stride-okat fogad, ezért paddingolt sorú mátrixhoz, szelethez vagy külső
pufferhez illik. A stride-ok azonban nem tetszőlegesek: a konstruktor
előfeltétele, hogy minden stride pozitív legyen, és hogy a leképezés
egyedi maradjon, vagyis két különböző indexpár ne essen ugyanarra az
offsetre ([C++23] [mdspan.layout.stride.cons]). A nulla stride
(broadcast: egy sor ismétlése minden sorindexre) és az egymást
átfedő leképezés tehát előfeltétel-sértés, nem támogatott bemenet; ilyen
nézethez saját layout policy kell, amelynek is_unique()-ja
hamis.
| layout | 2D mapping | leggyorsabb index | C tömb | is_always_unique() |
|---|---|---|---|---|
layout_right |
jobb oldali () | igen | igaz | |
layout_left |
bal oldali () | nem | igaz | |
layout_stride |
a stride-ok döntik el | padding esetén | igaz |
A mapping szerződése: az operator()(indices...)
noexcept, a required_span_size()
noexcept, és a mapping nem ellenőriz bounds-ot. A
layout_left és layout_right mindig egyedi és
kimerítő. A layout_stride is mindig egyedi
(is_always_unique() igaz), mert a konstruktor előfeltétele
kizárja az átfedést; az is_always_exhaustive() viszont
hamis, és padding esetén az is_exhaustive() is hamis. Az
egyediséget (nincs két index ugyanazon az offseten) nem szabad
összekeverni a kimerítőséggel (minden offset a tartományban használt). A
required_span_size() a legnagyobb offset plusz egy. Egy
3×4-es, 5-ös sorstride-os példában:
A puffernek tehát legalább 14 int-et kell tartalmaznia,
noha csak 12 elem logikailag látható. A két extra elem a padding.
Az accessor policy a data_handle_type és a lineáris
offset közötti hozzáférést írja le. Követelményei:
element_type, data_handle_type,
reference, offset_policy, valamint az
access(data_handle_type, std::size_t) és
offset(data_handle_type, std::size_t) tagfüggvények. Az
access a lineáris offsetet kapja, nem a logikai indexeket;
a mapping már elvégezte az indexszámítást. Ez a kulcs ahhoz, hogy
megértsük, mit tud és mit nem tud egy accessor.
A checked_accessor a data_handle_type-ba
csomagolja a pointert és a puffer méretét. Az access
ellenőrzi, hogy a lineáris offset a méreten belül van; az
offset a subview-hoz lépteti a handle-t, és csökkenti a
méretet.
#include <array>
#include <atomic>
#include <cassert>
#include <cstddef>
#include <iostream>
#include <mdspan>
template<class T>
struct checked_handle {
T* ptr;
std::size_t size;
};
template<class T>
struct checked_accessor {
using element_type = T;
using data_handle_type = checked_handle<T>;
using reference = T&;
using offset_policy = checked_accessor;
constexpr reference access(data_handle_type h, std::size_t i) const noexcept {
assert(i < h.size);
return h.ptr[i];
}
constexpr data_handle_type offset(data_handle_type h, std::size_t i) const noexcept {
assert(i <= h.size);
return {h.ptr + i, h.size - i};
}
};
extern "C" void c_fill(int* p, std::size_t rows, std::size_t cols, std::size_t stride) {
for (std::size_t i = 0; i < rows; ++i)
for (std::size_t j = 0; j < cols; ++j)
p[i * stride + j] = static_cast<int>(i * cols + j);
}
int main() {
int a[3][4] = {};
using E = std::dextents<std::size_t, 2>;
std::mdspan<int, E, std::layout_right, checked_accessor<int>> m(
checked_handle<int>{&a[0][0], 12}, 3, 4);
m[1, 2] = 42;
std::cout << a[1][2] << '\n'; // 42
c_fill(m.data_handle().ptr, m.extent(0), m.extent(1), m.stride(0));
int buf[15] = {};
using E2 = std::extents<std::size_t, 3, 4>;
std::array<std::size_t, 2> strides{5, 1};
std::layout_stride::mapping<E2> map(E2{}, strides);
std::mdspan<int, E2, std::layout_stride> ms(buf, map);
ms[2, 3] = 7;
std::cout << buf[2 * 5 + 3] << '\n'; // 7
std::cout << ms.size() << ' ' << ms.mapping().required_span_size() << '\n'; // 12 14
}A required_span_size() a mapping
tagfüggvénye, nem az mdspan-é: a tárolóigényt az
ms.mapping().required_span_size() adja, a logikai
elemszámot az ms.size().
Buildjegyzőkönyv. A lab-gcc profil GCC
13.3-as libstdc++-ában nincs <mdspan>, ezért a
programot ott nem lehet lefordítani. Ellenőrzésképpen a C++23
std::mdspan Kokkos-féle referencia-implementációjával
fordítottuk le (egyfejléces változat, 86eefa3d6653 commit,
2026-10-16), GCC 13.3-mal,
-std=c++23 -Wall -Wextra -Wpedantic mellett. A kimenet
42, 7, 12 14. Az eredeti
ms.required_span_size() hívás ugyanitt
has no member named 'required_span_size' hibát adott. Egy
szabványos <mdspan>-t nyújtó könyvtárral
(lab-next profil) a futtatás a saját laborban
ismétlendő.
A példa a checked_accessor assert-jére
épül; NDEBUG esetén az assert kikapcsol, ezért
release-ben a valódi bounds checkhez std::abort vagy
kivétel kell. A checked_accessor csak a lineáris offsetet
ellenőrzi: a m[3,0] offsetje 12, ezt az
assert(12 < 12) elkapja, de a m[0,4]
offsetje 4, ami a 12 elemes pufferben van, ezért azt nem. A logikai
indexek ellenőrzése az extents ismeretét igényli, amit az accessor nem
kap meg.
Az atomic_accessor reference típusa
std::atomic_ref<T>, így az mdspan elemei
atomikusan érhetők el. A std::atomic_ref<T>
követelménye, hogy T trivially copyable legyen;
int esetén ez teljesül. Az atomic_ref nem
másolható, ezért a hagyományos m[i,j] = 42 nem működik;
store és load kell.
template<class T>
struct atomic_accessor {
using element_type = T;
using data_handle_type = T*;
using reference = std::atomic_ref<T>;
using offset_policy = atomic_accessor;
constexpr reference access(T* p, std::size_t i) const noexcept {
return std::atomic_ref<T>{p[i]};
}
constexpr T* offset(T* p, std::size_t i) const noexcept {
return p + i;
}
};A C kétdimenziós tömb (int a[3][4]) folytonos,
sor-major; ezért a layout_right illeszkedik hozzá. A
&a[0][0] pointerből képzett mdspan nézet
nem tulajdonol, és addig érvényes, amíg a tömb él. Külső puffer
(malloc, std::vector::data(),
mmap) esetén ugyanez a szabály: a puffer élettartama
hosszabb kell legyen, mint a nézeté. A std::vector
újraallokálása invalidálja az mdspan-t; a
checked_handle mérete nem véd a felszabadítás ellen.
A C API-nak átadott pointer típusa accessor-függő.
default_accessor esetén m.data_handle()
T*; checked_accessor esetén
checked_handle<T>, ezért a C API-nak
m.data_handle().ptr-t kell adni. A stride-ot és az
extents-et külön kell átadni, ahogy a c_fill példa
mutatja.
A C++23-as P2128 többdimenziós subscript operátora miatt az
mdspan m[i,j] formában indexelhető. Ez nem
azonos a m[i][j] láncolással: 2D mdspan esetén
az m[i] nem ad 1D mdspan-t, ezért a
m[i][j] tipikus hiba. A korai implementációk és tervek
operator()-t használtak; a C++23-as szabvány
operator[]-t ír elő. A szeleteléshez a C++23-as
mdspan nem ad operator[]-alapú subview-t; erre
a C++26-os submdspan szolgál.
A std::submdspan (P2630) C++26-os kiegészítés, ezért a
C++23-as laborban nem használjuk, és a feature-test makrója
(__cpp_lib_submdspan) is csak ott lehet releváns. A
C++23-as megoldás kézi subview layout_stride-dal, a
stride-ok megőrzésével. Egy layout_right-os C tömb
-edik
oszlopa például:
using E1 = std::extents<std::size_t, 3>;
std::array<std::size_t, 1> col_stride{4};
std::mdspan<int, E1, std::layout_stride> col(
&a[0][j], E1{}, std::layout_stride::mapping<E1>{E1{}, col_stride});A col[i] ekkor a[i][j]-vel egyezik. A
C++26-os submdspan ezt a mintát általánosítja, de a
C++23-as kódban nem jelölhető C++23-as képességként.
layout_left használata C tömbhöz: a C tömb sor-major,
ezért a layout_left rossz indexelést ad.m[i][j] 2D mdspan-ra: az
mdspan többdimenziós operator[]-t vár, nem
láncolt indexelést.mdspan dangling lesz a mögöttes
std::vector újraallokálása után; a nézet nem
tulajdonol.checked_accessor csak a lineáris offsetet ellenőrzi,
a logikai indexeket nem; a m[0,4] hibát nem kapja el.data_handle() típusa accessor-függő; C API-nál a
checked_handle esetén .ptr kell.atomic_accessor reference típusa
std::atomic_ref<T>, ezért m[i,j] = 42
helyett store/load kell.submdspan C++26-os; C++23-as laborban nem használható
jelöletlenül.Készíts 3×4-es mdspan-t layout_stride-dal
úgy, hogy a sorok között egy int padding legyen. Ellenőrizd
a mapping required_span_size() értékét
(m.mapping().required_span_size()) és az
mdspan size()-át, és állapítsd meg az
is_unique() és is_exhaustive() eredményét. Írd
ki a m[2,3] elem lineáris offsetjét a pufferben.
Adott a checked_accessor-os m 3×4-es, 12
elemes pufferrel. Magyarázd el, miért nem kapja el a m[0,4]
hibát, miközben a m[3,0]-t elkapja. Adj olyan wrappert,
amely a logikai indexeket is ellenőrzi.
Készíts atomic_accessor-t, és mutasd meg, hogyan lehet
atomikusan írni és olvasni egy mdspan elemet. Magyarázd el,
miért nem működik a m[i,j] = 42, és milyen követelményeket
támaszt a std::atomic_ref<T> a T-vel
szemben.
A stride-ok {5,1}, az extents 3×4. A
legnagyobb offset (3-1)*5 + (4-1)*1 = 13, ezért
required_span_size() == 14. A puffer 14 int, a
12 logikai elem mellett 2 padding. A mapping egyedi (minden
layout_stride mapping az, a konstruktor előfeltétele
miatt), de nem kimerítő, mert a 14 elemből 12-t használ. A
m[2,3] offsetje 2*5 + 3 = 13.
// stride_props.cpp — C++23: a layout_stride mindig egyedi
#include <array>
#include <cstddef>
#include <mdspan>
using E = std::extents<std::size_t, 3, 4>;
using M = std::layout_stride::mapping<E>;
static_assert(M::is_always_unique()); // minden layout_stride mapping egyedi
static_assert(!M::is_always_exhaustive()); // padding lehet: nem mindig kimerítő
static_assert(M::is_always_strided());
int main() {
int buf[14] = {};
M map(E{}, std::array<std::size_t, 2>{5, 1});
std::mdspan<int, E, std::layout_stride> m(buf, map);
m[2, 3] = 7;
const bool ok = buf[13] == 7 && m.size() == 12 &&
m.mapping().required_span_size() == 14 &&
m.mapping()(2, 3) == 13 &&
m.is_unique() && !m.is_exhaustive();
return ok ? 0 : 1;
}Ellenőrzési feltétel: a size() 12, a
mapping().required_span_size() 14, a [2,3]
offsetje 13, a buf[13] értéke 7, az
is_unique() igaz, az is_exhaustive() hamis; a
program 0 kóddal lép ki. A referencia-implementációval (lásd fent)
ellenőrizve.
A m[3,0] lineáris offsetje 3*4 + 0 = 12; a
checked_handle mérete 12, ezért az
assert(12 < 12) hamis, és a program megáll. A
m[0,4] offsetje 0*4 + 4 = 4; ez a 12 elemes
pufferben van, ezért az assert(4 < 12) igaz, és az
accessor nem jelez. Az accessor csak a lineáris offsetet kapja; a
logikai indexek érvényességét nem ismeri. A teljes ellenőrzéshez olyan
wrapper kell, amely az extents-et is ismeri:
template<class M>
struct bounds_checked_mdspan {
M m;
decltype(auto) operator[](std::size_t i, std::size_t j) const {
assert(i < m.extent(0));
assert(j < m.extent(1));
return m[i, j];
}
};Ez a wrapper a m[0,4]-et is elkapja, mert a
j < 4 feltétel hamis. Az ellenőrzési feltétel: a
bounds_checked_mdspan m[0,4] hívása
assert-et vált ki, a m[0,3] nem.
Az atomic_accessor reference típusa
std::atomic_ref<int>. Az access a
p[i]-re hivatkozó atomic_ref-et ad vissza. Az
atomikus írás és olvasás:
std::mdspan<int, std::dextents<std::size_t, 2>,
std::layout_right, atomic_accessor<int>> am(buf, 3, 4);
am[0, 0].store(1, std::memory_order_relaxed);
int v = am[0, 0].load(std::memory_order_relaxed);
assert(v == 1);A m[i,j] = 42 azért nem működik, mert a
std::atomic_ref nem másolható, és nem biztosít olyan
operator=-t, amely a mögöttes int-et
értékadással írná; a store a szabványos írási művelet. A
std::atomic_ref<T> követelménye, hogy T
trivially copyable legyen, valamint copy constructible és copy
assignable; int esetén ez teljesül. Ellenőrzési feltétel: a
store után a load az eltárolt értéket adja
vissza, és a std::atomic_ref nem másolható.
Az mdspan három rétege az extents, a layout mapping és
az accessor. A layout_right a C sor-major, a
layout_left a Fortran oszlop-major, a
layout_stride pedig paddingolt vagy szeletelt pufferhez
való. A mapping a logikai indexeket lineáris offsetre képezi, de nem
ellenőriz bounds-ot; az accessor már csak a lineáris offsetet látja,
ezért a checked_accessor a memóriahozzáférés biztonságát
javítja, a logikai indexekét nem. A data_handle_type
accessor-függő, a C API-nál a pointert kell kinyerni. A C++23-as
operator[] többdimenziós indexelést ad, a
submdspan viszont C++26-os kiegészítés, ezért a C++23-as
laborban kézi, layout_stride-os subview-t használunk.