43.4 Alacsony szintű és C-interop eszközök C++23-ban

Ez az alfejezet a C++23 azon eszközeit tárgyalja, amelyek közvetlenül a bájtok, a nyers memória és a C API határán dolgoznak. Ezek az eszközök akkor hasznosak igazán, ha a mögöttes objektummodell már rögzített: a bájtok és az objektumreprezentáció alapjait Bitműveletek és objektumreprezentáció C++-ban tárgyalta, a tároló újrafelhasználásának szabályait Tároló újrafelhasználása és C++ objektumszabályok, a C API határát pedig extern C és két nyelvű header mutatta be. Az itt következő anyag ezekre épít, nem ismétli meg őket.

A laborprofil továbbra is ISO C++23, explicit -std=c++23 kapcsolóval. A generáló modell nem böngészett, ezért a konkrét GCC-, Clang-, libstdc++- és libc++-támogatást nem állítom ellenőrzöttnek. Minden új képességhez feature-test makrót és compile probe-ot kell használni; a C++20-as fallbackot a táblázat foglalja össze. A C oldali effective type szabályai Effective type, aliasing és objektumreprezentáció nem azonosak a C++ objektum-élettartam szabályaival, ezt a start_lifetime_as tárgyalásánál külön kiemelem.

43.4.1 Bájtsorrend, bájtswap és enum-alapú típuskonverzió

A std::byteswap C++23-as függvénysablon a <bit> fejlécben. Pontos alakja:

template<class T>
constexpr T byteswap(T value) noexcept;

Feltétel: T integrál vagy felsorolás típus. Visszatérési értéke az a T, amelynek bájtjai fordított sorrendben követik a bemenetét; ha sizeof(T) == 1, az érték változatlan. A függvény nem végez endian-konverziót önmagában: csak a bájtokat cseréli. A hálózati vagy fájlformátum-sorrendet a protokoll rögzíti, a natív sorrendet a C++20 óta ismert std::endian::native mondja meg. Helyes használat például:

#include <bit>
#include <cstdint>
#include <cstdio>

constexpr std::uint32_t to_network_order(std::uint32_t v) noexcept {
    if constexpr (std::endian::native == std::endian::little) {
        return std::byteswap(v);
    } else {
        return v;
    }
}

int main() {
    std::printf("%08x\n", to_network_order(0x01020304u));
}

A buildparancs: g++ -std=c++23 -Wall -Wextra -pedantic -o netorder netorder.cpp. Little-endian gépen a kimenet 04030201, big-endian gépen 01020304; a pontos érték a std::endian::native függvényében értelmezendő. A std::byteswap tehát feltételesen használható, a std::endian ellenőrzése nélkül könnyű rossz protokollt írni.

A std::to_underlying C++23-as segédfüggvény a <utility> fejlécben:

template<class Enum>
constexpr std::underlying_type_t<Enum> to_underlying(Enum e) noexcept;

Feltétel: Enum felsorolás típus. Visszatérési értéke a felsorolás alaptípusára konvertált érték. Előnye a static_cast-tal szemben, hogy kifejezi a szándékot, és scoped enumra is ugyanúgy működik. A std::is_scoped_enum C++23-as trait a <type_traits> fejlécben igaz, ha a típus scoped enum; ezzel overloadokat lehet megkülönböztetni. Példa:

#include <cstdint>
#include <type_traits>
#include <utility>

enum class Color : std::uint8_t { red = 1, green = 2 };

static_assert(std::is_scoped_enum_v<Color>);
static_assert(std::to_underlying(Color::green) == 2);

A C++20-as fallback: static_cast<std::underlying_type_t<E>>(e), illetve a scoped enum közelítése std::is_enum_v<T> && !std::is_convertible_v<T, int> feltétellel.

43.4.2 Objektum élettartam indítása nyers memóriában

A std::start_lifetime_as (P2590) C++23-as eszköz a <memory> fejlécben. Alapalakja:

template<class T>
T* start_lifetime_as(void* p) noexcept;

Feltételek: T implicit-lifetime típus; a p által mutatott tároló legalább sizeof(T) méretű és alignof(T)-nek megfelelően igazított; a területen nincs élő T objektum. A függvény elindítja egy T objektum élettartamát anélkül, hogy konstruktort hívna vagy bájtokat másolna; a bájtok változatlanok. A std::start_lifetime_as_array ugyanezt tömb esetére végzi. Az std::is_implicit_lifetime_v<T> trait C++23-ban eldönti, hogy T implicit-lifetime típus-e; tipikusan skalár, triviálisan másolható és triviálisan destruálható osztály, aggregate.

Kidolgozott példa:

#include <cstdint>
#include <cstdio>
#include <cstring>
#include <memory>
#include <type_traits>

struct Header {
    std::uint32_t magic;
    std::uint32_t length;
};

static_assert(std::is_implicit_lifetime_v<Header>);

int main() {
    alignas(Header) unsigned char buffer[sizeof(Header)];
    const unsigned char bytes[sizeof(Header)] = {
        0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08
    };
    std::memcpy(buffer, bytes, sizeof(Header));
    Header* h = std::start_lifetime_as<Header>(buffer);
    std::printf("%08x %08x\n", h->magic, h->length);
}

A buildparancs: g++ -std=c++23 -Wall -Wextra -pedantic -o lifetime lifetime.cpp. A példa várt kimenete endianfüggő: a printf a bájtokból olvasott értéket mutatja, nem univerzális layoutot. A Header* érvényessége a buffer élettartamához kötött; ha a buffer egy dinamikusan allokált puffer, akkor a puffer felszabadítása után a Header* dangling lesz.

A start_lifetime_as nem használható nem implicit-lifetime típusra, például std::string-re, és nem használható olyan területre, ahol már él egy T objektum. Mindkét eset nem definiált viselkedés. A C oldali effective type szabályai Effective type, aliasing és objektumreprezentáció ettől eltérnek: a C++23-as eszköz a C++ objektum-élettartamot indítja el, nem a C effective type-ot állítja be.

Implementációs állapot: a generáló modell nem böngészett, ezért nem állítom, hogy bármelyik konkrét standard library támogatja. A feature-test makró neve __cpp_lib_start_lifetime_as; ha nem definiált, a képesség nem használható. A hordozható C++20-as fallback a std::memcpy egy lokális T objektumba, ami viszont másol, és nem ugyanaz a szemantika.

43.4.3 C API és okos pointerek: out_ptr és inout_ptr

A std::out_ptr és std::inout_ptr (P1132) C++23-as eszközök a <memory> fejlécben. Céljuk, hogy egy C API T** kimeneti paraméterét közvetlenül egy okos pointerhez lehessen kötni. A std::out_ptr(p, args...) egy out_ptr_t proxyt ad vissza, amely a C API hívás idejére átadja a nyers pointer címét. A proxy destruktora a C API által beírt pointerre reseteli a p okos pointert. Az inout_ptr abban különbözik, hogy a kezdeti pointert is átadja a C API-nak, így az a meglévő erőforrást is használhatja, például realloc-szerű műveletnél.

Példa std::unique_ptr-ral:

#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <memory>

struct Header {
    std::uint32_t magic;
    std::uint32_t length;
};

extern "C" int get_buffer(unsigned char** data, std::size_t* size) {
    auto* p = static_cast<unsigned char*>(std::malloc(64));
    if (!p) return -1;
    std::memset(p, 0, 64);
    const Header init{0x47414944u, 16u};   // natív reprezentáció: minden
    std::memcpy(p, &init, sizeof init);    // később olvasott bit definiált
    *data = p;
    *size = 64;
    return 0;
}

int main() {
    std::unique_ptr<unsigned char, decltype(&std::free)> buf(nullptr, &std::free);
    std::size_t n = 0;
    if (get_buffer(std::out_ptr(buf), &n) != 0) return 1;
    if (n >= sizeof(Header)) {
        Header* h = std::start_lifetime_as<Header>(buf.get());
        std::printf("%u %u\n", h->magic, h->length);
    }
}

A buildparancs: g++ -std=c++23 -Wall -Wextra -pedantic -o outptr outptr.cpp. A std::start_lifetime_as egyetlen dolgot tesz: a megadott tárolón elindítja egy Header objektum élettartamát, a tárolóban már meglévő bájtok értelmezésével. Nem inicializál: ha a bájtok határozatlanok (például egy frissen malloc-olt pufferben), a mezők olvasása határozatlan értéket ad. Ezért a get_buffer a példában minden később olvasott bájtot beállít. A bájtsorrendet sem kezeli: a bájtokat a gép natív reprezentációjaként értelmezi. A protokollt sem validálja: a length értékének ellenőrzése a hívó feladata.

Hálózatról vagy fájlból érkező, rögzített bájtsorrendű adatnál ezért a mezőnkénti dekódolás a helyes út, amely C++20-ban is működik:

// decode_header.cpp — C++20: mezőnkénti dekódolás little-endian wire formátumból
#include <cstddef>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <memory>

struct Header {
    std::uint32_t magic;
    std::uint32_t length;
};

// A C oldal 8 bájtos fejlécet ad: magic és length, mindkettő little-endian.
extern "C" int get_buffer(unsigned char** data, std::size_t* size) {
    auto* p = static_cast<unsigned char*>(std::malloc(64));
    if (!p) return -1;
    std::memset(p, 0, 64);                               // minden bájt definiált
    const unsigned char wire[8] = {'D', 'I', 'A', 'G', 16, 0, 0, 0};
    std::memcpy(p, wire, sizeof wire);
    *data = p;
    *size = 64;
    return 0;
}

std::uint32_t load_le32(const unsigned char* p) noexcept {
    return std::uint32_t{p[0]} | std::uint32_t{p[1]} << 8 |
           std::uint32_t{p[2]} << 16 | std::uint32_t{p[3]} << 24;
}

int main() {
    unsigned char* raw = nullptr;
    std::size_t n = 0;
    if (get_buffer(&raw, &n) != 0) return 1;
    std::unique_ptr<unsigned char, decltype(&std::free)> buf(raw, &std::free);
    if (n < 8) return 1;                                 // méretvalidálás
    const Header h{load_le32(buf.get()), load_le32(buf.get() + 4)};
    if (h.length > n - 8) return 1;                      // protokollvalidálás
    std::printf("magic=0x%08x length=%u\n", static_cast<unsigned>(h.magic),
                static_cast<unsigned>(h.length));
}

Buildjegyzőkönyv (lab-gcc profil). A decode_header.cpp g++ -std=c++20 -Wall -Wextra -Wpedantic beállítással figyelmeztetés nélkül fordult, és magic=0x47414944 length=16 kimenettel futott, ASan/UBSan alatt is. A std::out_ptr és a std::start_lifetime_as a GCC 13.3 libstdc++-ában nem érhető el, ezért az outptr.cpp-t a lab-gcc profilban nem fordítottuk le; az a lab-next profilhoz tartozik, és ott ellenőrizendő. A C oldali fejlécben a get_buffer extern "C" blokkban deklarálandó; a kétnyelvű header részleteit extern C és két nyelvű header tárgyalta. A példában a get_buffer minden esetben ír a data pointerbe, ezért az out_ptr biztonságos. Ha a C API hibás ágban nem ír a pointerbe, az out_ptr destruktora nullára resetelheti a buf-ot, és a korábbi erőforrás elveszhet; ilyen szerződésnél inout_ptr vagy manuális raw változó és feltételes reset kell. A Header* érvényessége a buf élettartamához kötött: a buf felszabadítása után a pointer dangling.

43.4.4 Allokáció és string puffer

Az allocate_at_least (P0401) C++23-ban az allokátorok olyan művelete, amely legalább a kért elemszámot allokálja, és visszaadja a ténylegesen allokált elemszámot is. Két felületen érhető el: tagfüggvényként a std::allocator<T>::allocate_at_least(n), általános allokátorra pedig a std::allocator_traits<A>::allocate_at_least(a, n); std::allocate_at_least szabad függvény nincs. Az eredmény egy std::allocation_result<T*>, ptr és count taggal, ahol count >= n. A felszabadítás szerződése ([C++23] [allocator.members]): ha a p egy allocate_at_least(req) hívásból származik, a deallocate(p, n) hívásban az n bármely érték lehet, amelyre req <= n <= ret.count. Kérés 10, count 16 esetén tehát a 10 és a 16 közötti bármely méret elfogadható. A count megőrzése és használata ajánlott, mert így a tényleges kapacitást a teljes élettartam alatt ismerjük, de a kért mérettel való felszabadítás nem hiba. Példa:

#include <cstdio>
#include <memory>

int main() {
    std::allocator<int> a;
    auto r = a.allocate_at_least(10);
    std::printf("count = %zu\n", r.count);
    a.deallocate(r.ptr, r.count);
}

A buildparancs: g++ -std=c++23 -Wall -Wextra -pedantic -o alloc alloc.cpp. A std::vector C++23-as implementációja használhatja ezt a növekedésnél, de ez implementációfüggő, nem garantált. A lab-gcc profil GCC 13.3-as libstdc++-ában az allocate_at_least nem érhető el (nincs __cpp_lib_allocate_at_least), ezért a példa a lab-next profilhoz tartozik, és ott ellenőrizendő.

A std::string::resize_and_overwrite (P1072) C++23-as tagfüggvény lehetővé teszi, hogy a string pufferebe közvetlenül írjunk, elkerülve a kétszeres inicializálást. Alakja:

template<class Operation>
constexpr void resize_and_overwrite(size_type n, Operation op);

Az op hívása (char* p, size_type n) szignatúrával történik; visszatérési értéke a string új mérete, amely nem lehet nagyobb n-nél. Az op csak a puffer első n karakterébe írhat, a string objektumot nem módosíthatja. Példa POSIX read-dal:

#include <cerrno>
#include <string>
#include <unistd.h>

bool read_all(int fd, std::string& out, std::size_t max, int& err) {
    err = 0;
    out.resize_and_overwrite(max, [&](char* p, std::size_t n) -> std::size_t {
        std::size_t total = 0;
        while (total < n) {
            ssize_t r = ::read(fd, p + total, n - total);
            if (r < 0) {
                if (errno == EINTR) continue;
                err = errno;
                return total;
            }
            if (r == 0) break;
            total += static_cast<std::size_t>(r);
        }
        return total;
    });
    return err == 0;
}

A buildparancs: g++ -std=c++23 -Wall -Wextra -pedantic -o read_all read_all.cpp. A hibaágban a string mérete a részlegesen olvasott bájtok száma lesz, a hiba kódja pedig err-ben. Ha a lambda visszatérési értéke nagyobb n-nél, az nem definiált viselkedés.

A std::string::contains és std::string_view::contains C++23-as bővítés: bool contains(basic_string_view sv) const és bool contains(char c) const. Példa:

#include <cassert>
#include <string>

int main() {
    std::string s = "GET / HTTP/1.1";
    assert(s.contains("HTTP/1.1"));
    assert(s.contains('/'));
}

A C++20-as fallback: find(...) != std::string::npos.

A következő táblázat a feature-test makrókat és a C++20-as fallbackokat foglalja össze. A makrók pontos értékét és elérhetőségét a választott fordító fejléce és support-táblázata alapján kell ellenőrizni; a táblázat nem állítja, hogy bármelyik implementáció támogatja őket.

Eszköz Feature-test makró (ellenőrizendő) C++20 fallback
std::byteswap __cpp_lib_byteswap saját bájtswap unsigned típusra
std::to_underlying __cpp_lib_to_underlying static_cast<std::underlying_type_t<E>>(e)
std::is_scoped_enum __cpp_lib_is_scoped_enum std::is_enum_v<T> && !std::is_convertible_v<T, int>
std::start_lifetime_as __cpp_lib_start_lifetime_as std::memcpy lokális T objektumba (másol)
std::out_ptr __cpp_lib_out_ptr manuális T* raw; ...; p.reset(raw);
std::allocate_at_least __cpp_lib_allocate_at_least allocate(n), count = n
resize_and_overwrite __cpp_lib_string_resize_and_overwrite resize(n), majd resize(r)
string::contains __cpp_lib_string_contains find(...) != npos

A labor első lépése lehet egy compile probe, amely hiányzó képesség esetén szándékosan fordítási hibát ad:

#include <bit>
#include <memory>
#include <string>
#include <type_traits>

#if !defined(__cpp_lib_byteswap) || \
    !defined(__cpp_lib_to_underlying) || \
    !defined(__cpp_lib_start_lifetime_as) || \
    !defined(__cpp_lib_out_ptr) || \
    !defined(__cpp_lib_allocate_at_least) || \
    !defined(__cpp_lib_string_resize_and_overwrite) || \
    !defined(__cpp_lib_string_contains)
#error "A laborprofil C++23 képességei hiányosak"
#endif

43.4.5 Tipikus hibák

43.4.6 Feladatok

  1. Írj egy to_network_order(std::uint32_t) függvényt, amely a hoszt byte orderből hálózati byte orderbe konvertál. Használd a std::endian és std::byteswap eszközöket. Add meg a buildparancsot, és magyarázd el, miért nem elég a std::byteswap önmagában. Ellenőrizd az eredményt little-endian és big-endian esetre.

  2. Adott a következő C API:

extern "C" int get_buffer(unsigned char** data, std::size_t* size);

amely egy malloc-olt puffert ad vissza. Készíts egy std::unique_ptr<unsigned char, decltype(&std::free)>-t, és használd std::out_ptr-t. Ezután értelmezd a puffer első sizeof(Header) bájtját std::start_lifetime_as<Header>-rel. Sorolj fel minden szükséges előfeltételt, és mondd meg, meddig érvényes a kapott Header*.

  1. Implementálj egy read_all(int fd, std::string& out, std::size_t max, int& err) függvényt, amely resize_and_overwrite-t használ, legfeljebb max bájtot olvas, kezeli az EINTR-t, és a string méretét a ténylegesen olvasott bájtokra állítja. Add meg a buildparancsot és az ellenőrzési feltételt.

43.4.7 Megoldások

43.4.7.1 1. feladat megoldása

#include <bit>
#include <cstdint>
#include <cstdio>

constexpr std::uint32_t to_network_order(std::uint32_t v) noexcept {
    if constexpr (std::endian::native == std::endian::little) {
        return std::byteswap(v);
    } else {
        return v;
    }
}

int main() {
    std::printf("%08x\n", to_network_order(0x01020304u));
}

A std::byteswap csak bájtokat cserél; a hálózati sorrendet a protokoll rögzíti, ezért kell a std::endian::native feltétel. Little-endian gépen a várt kimenet 04030201, big-endian gépen 01020304. Ellenőrzési feltétel: a kimenet a gépi endiannek megfelelően a hálózati sorrendet tükrözze, és a program -Wall -Wextra -pedantic mellett figyelmeztetés nélkül forduljon.

43.4.7.2 2. feladat megoldása

#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <memory>

struct Header {
    std::uint32_t magic;
    std::uint32_t length;
};

extern "C" int get_buffer(unsigned char** data, std::size_t* size) {
    auto* p = static_cast<unsigned char*>(std::malloc(64));
    if (!p) return -1;
    std::memset(p, 0, 64);
    const Header init{0x47414944u, 16u};   // natív reprezentáció: minden
    std::memcpy(p, &init, sizeof init);    // később olvasott bit definiált
    *data = p;
    *size = 64;
    return 0;
}

int main() {
    std::unique_ptr<unsigned char, decltype(&std::free)> buf(nullptr, &std::free);
    std::size_t n = 0;
    if (get_buffer(std::out_ptr(buf), &n) != 0) return 1;
    if (n < sizeof(Header)) return 1;
    Header* h = std::start_lifetime_as<Header>(buf.get());
    std::printf("%u %u\n", h->magic, h->length);
}

Előfeltételek: a get_buffer sikeres hívás esetén ír a data pointerbe, és a puffer első sizeof(Header) bájtját definiált értékkel tölti fel (a start_lifetime_as nem inicializál); a puffer mérete legalább sizeof(Header); a malloc igazítása legalább alignof(Header); a Header implicit-lifetime típus; a pufferben nincs élő Header objektum. A Header* a buf élettartamáig érvényes; a buf felszabadítása után dangling. Ellenőrzési feltétel: a program csak akkor hívja meg a start_lifetime_as-t, ha a méret megfelelő, és a Header*-ot nem használja a buf felszabadítása után.

43.4.7.3 3. feladat megoldása

#include <cerrno>
#include <string>
#include <unistd.h>

bool read_all(int fd, std::string& out, std::size_t max, int& err) {
    err = 0;
    out.resize_and_overwrite(max, [&](char* p, std::size_t n) -> std::size_t {
        std::size_t total = 0;
        while (total < n) {
            ssize_t r = ::read(fd, p + total, n - total);
            if (r < 0) {
                if (errno == EINTR) continue;
                err = errno;
                return total;
            }
            if (r == 0) break;
            total += static_cast<std::size_t>(r);
        }
        return total;
    });
    return err == 0;
}

A lambda visszatérési értéke a ténylegesen olvasott bájtok száma, így a string mérete ezzel egyezik. Az EINTR esetén a ciklus újrapróbálja az olvasást; más hiba esetén a részleges hosszt adja vissza, és az err tartalmazza a hiba kódját. Buildparancs: g++ -std=c++23 -Wall -Wextra -pedantic -o read_all read_all.cpp. Ellenőrzési feltétel: egy pipe-ból ismert hosszú adatot olvasva a string tartalma és mérete a beírt adatnak feleljen meg; hiba esetén a méret a részlegesen olvasott bájtok száma legyen, és a visszatérési érték hamis.

43.4.8 Összefoglaló

A C++23 alacsony szintű eszközei akkor biztonságosak, ha a mögöttes objektum-élettartam- és C API-szerződést pontosan betartjuk. A std::byteswap csak bájtcserét végez, az endian-döntést a std::endian és a protokoll adja. A std::start_lifetime_as nyers tárolóban indít implicit-lifetime objektumot, feltételei megsértése nem definiált viselkedés. A std::out_ptr és std::inout_ptr az okos pointer és a C API közötti tulajdonjog-átadást teszi kifejezővé, de a hibaág szerződését ellenőrizni kell. Az allocate_at_least, a resize_and_overwrite és a contains a teljesítmény és a kényelem irányába bővítik a standard libraryt; használatukhoz feature-test makró és a tényleges implementáció ellenőrzése szükséges.