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.
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.
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.
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.
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"
#endifstd::byteswap önmagában nem endian-konverzió; a
std::endian::native ellenőrzése nélkül használva rossz
protokollsorrendet ad.std::start_lifetime_as nem implicit-lifetime típusra,
például std::string-re nem definiált viselkedés.std::start_lifetime_as olyan területre hívva, ahol
már él T objektum, szintén nem definiált viselkedés.std::out_ptr hibaágban elveszítheti a régi
erőforrást, ha a C API nem ír a pointerbe; ilyenkor
inout_ptr vagy manuális kezelés kell.resize_and_overwrite visszatérési értéke nem lehet
nagyobb a kért n-nél; ha mégis, nem definiált
viselkedés.allocate_at_least eredményét olyan mérettel
deallokálni, amely kisebb a kértnél vagy nagyobb a visszaadott
count-nál. A req <= n <= count
tartományon belül bármely méret megengedett; a count
megőrzése a legegyszerűbb, egyértelmű szabály.string::contains C++23-as; C++20-ban a
find és a npos összehasonlítása
szükséges.Í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.
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*.
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.#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.
#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.
#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.
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.