43.2 mdspan mélyebben: layoutok, accessorok és C-interop

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.

43.2.1 Extents: statikus és dinamikus kiterjedés

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.

43.2.2 Layoutok és a mapping szerződése

A layout policy a logikai indexeket lineáris offsetre képezi. Kétdimenziós esetben a három alapvető mapping:

Mright(i,j)=i⋅N1+jM_{\text{right}}(i,j) = i\cdot N_1 + j

Mleft(i,j)=i+j⋅N0M_{\text{left}}(i,j) = i + j\cdot N_0

Mstride(i,j)=i⋅s0+j⋅s1M_{\text{stride}}(i,j) = i\cdot s_0 + j\cdot s_1

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 i⋅N1+ji\cdot N_1 + j jobb oldali (jj) igen igaz
layout_left i+j⋅N0i + j\cdot N_0 bal oldali (ii) nem igaz
layout_stride i⋅s0+j⋅s1i\cdot s_0 + j\cdot s_1 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:

maxi,jM(i,j)=(3−1)⋅5+(4−1)⋅1=13,required_span_size=14.\max_{i,j} M(i,j) = (3-1)\cdot 5 + (4-1)\cdot 1 = 13,\qquad \text{required\_span\_size} = 14.

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.

43.2.3 Saját accessor policy

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.

43.2.3.1 checked_accessor: lineáris bounds check

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.

43.2.3.2 atomic_accessor: atomikus hozzáférés

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;
    }
};

43.2.4 C-interop: C tömbök és külső pufferek

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.

43.2.5 Többdimenziós operator[]

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.

43.2.6 submdspan: C++26 kiegészítés

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 jj-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.

43.2.7 Tipikus hibák

43.2.8 Feladatok

43.2.8.1 Kódolási feladat: layout_stride paddinggal

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.

43.2.8.2 Diagnosztikai feladat: checked_accessor korlátja

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.

43.2.8.3 Tervezési feladat: atomic_accessor

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.

43.2.9 Megoldások

43.2.9.1 A layout_stride feladat megoldása

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.

43.2.9.2 A checked_accessor feladat megoldása

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.

43.2.9.3 Az atomic_accessor feladat megoldása

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ó.

43.2.10 Összefoglaló

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.