A korábbi byte order és biztonságos
szerializálás rész már rögzítette, hogy a wire formátumot nem a
natív struct layout írja le. A mostani rész ezt a C++20
<bit> eszközeivel pontosítja: a flagkészletet
típusbiztos enum mögé tesszük, a reprezentáció-átalakítást
std::bit_cast-ra vagy explicit byte-összerakásra bízzuk, a
számlálást és forgatást a standard <bit>
függvényeire, az endianness-kezelést pedig std::endian-re
és std::byteswap-re. A span, byte és
bounds-szerződések rész nézetfogalmára építünk: a
std::span nem tulajdonol, a std::bitset
viszont igen; a type traits pedig a feltételek
fordítás idejű ellenőrzéséhez kell.
C-ben a flagkészlet tipikus alakja
enum { PERM_READ = 1 << 0, PERM_WRITE = 1 << 1 };.
Ez működik, de az enumerátorok típusa int, a
PERM_READ | PERM_WRITE eredménye is int, a
~PERM_READ pedig negatív; a függvényhatáron implicit
konverziók és előjelproblémák keletkeznek. C++20-ban a scoped enum fix
underlying type-pal adja a méretet és a tartományt:
enum class Perm : std::uint8_t {
none = 0,
read = 1u << 0,
write = 1u << 1,
exec = 1u << 2,
};A szabvány scoped enumra nem ad beépített bitwise operátort, ezért a
„bitmask enum idiom” része a műveletek definiálása. A |,
&, ^ és ~ operátorokat
constexpr és noexcept függvényként adjuk meg,
a ~ eredményét pedig az underlying type szélességére
maszkoljuk, hogy ne keletkezzen előjeles értelmezés:
constexpr Perm operator|(Perm a, Perm b) noexcept {
return static_cast<Perm>(
static_cast<std::uint8_t>(a) | static_cast<std::uint8_t>(b));
}
constexpr bool has(Perm set, Perm bit) noexcept {
return (static_cast<std::uint8_t>(set) &
static_cast<std::uint8_t>(bit)) != 0;
}C++23-tól a std::to_underlying kiváltja a
static_cast-ot; C++20-as fallbackként marad a cast. A
std::bitset<N> más eset: N fordítási
idejű, a tárolót a bitset tulajdonolja, és nem nevesített flagkészletet,
hanem tetszőleges bitsorozatot kezel. count(),
test(), set(), flip() és
operator[] áll rendelkezésre; az operator[]
proxyreferenciát ad, a to_ullong() pedig
std::overflow_error-t dob, ha a bitsorozat nem fér el. A
bitset nagy része C++23-ban lett constexpr; C++20-ban ezt
ne feltételezzük. Nevesített protokollflagre maradjunk az enum
idiómánál, ismeretlen szélességű bitkészletre pedig bitsetnél.
A std::bit_cast C++20-as, <bit>
headerben található függvénysablon:
template<class To, class From>
constexpr To bit_cast(const From& from) noexcept;Feltételei: sizeof(To) == sizeof(From), mindkét típus
triviálisan másolható, a From nem bit-field, és a
From objektumreprezentációjához tartozzék
To-beli érték. Ha nincs ilyen érték, a viselkedés nem
definiált. A nem értékreprezentációs bitek — például
struct-padding — a resultspecifikáció szerint nem
specifikáltak; az ilyen eredményt ne hasonlítsuk, ne hasheljük, és ne
tekintsük wire formátumnak.
A legfontosabb szemantikai különbség: a bit_cast nem
numerikus konverzió. Nem kerekít, nem trunkiál, nem változtatja meg a
bitmintát. A static_cast<int>(2.9) értéket konvertál
(2), a bit_cast<std::uint32_t>(2.9f) a
2.9f reprezentációját adja vissza. A
static_cast<bool>(x) az x != 0
szemantikát jelenti; a bit_cast<bool>(x) bitmintát
másol, és ha a minta nem 0/1, nincs megfelelő bool érték,
tehát UB. Ugyanez a helyzet egy szűk értéktartományú, fix underlying
type nélküli enummal: nem minden bitminta tartozik értékhez.
static_assert(sizeof(float) == sizeof(std::uint32_t));
static_assert(std::numeric_limits<float>::is_iec559);
constexpr std::uint32_t bits = std::bit_cast<std::uint32_t>(1.5f);
static_assert(bits == 0x3FC00000u); // IEC 559 binary32A bit_cast ugyanazt a byte-másolást végzi, mint a
memcpy triviálisan másolható típusok között, de
típusbiztos, fordítás idejű méretellenőrzést ad, és
constexpr. A unionos type punninggel szemben nem olvas
inaktív tagot; a reinterpret_cast<T*>(buf) és
dereferencia ezzel szemben nem hoz létre T objektumot, és
strict aliasing / lifetime szempontból UB. A bit_cast
viszont új To objektumot ad vissza, nem nézetet.
<bit> eszközeiA C++20 <bit> header portable,
constexpr és noexcept függvényeket ad. Ezek
csak unsigned integer típusra mennek; signed típusra
std::make_unsigned_t<T>-vel konvertáljunk.
std::popcount(x): az 1-bitek száma.std::countl_zero(x) / std::countr_zero(x):
vezető, illetve záró 0-bitek száma; x == 0 esetén a típus
szélességét adja.std::countl_one(x) / std::countr_one(x):
vezető, illetve záró 1-bitek száma.std::has_single_bit(x): pontosan egy 1-bit van-e (azaz
kettőhatvány-e).std::bit_width(x): 1 + floor(log2(x)),
nullára 0; std::bit_ceil(x) a legkisebb kettőhatvány, amely
legalább x; std::bit_floor(x) a legnagyobb
kettőhatvány, amely legfeljebb x.std::rotl(x, s) / std::rotr(x, s):
körkörös forgatás; a gyakorlatban s-et a típus szélességére
normalizáljuk.A bit_ceil előfeltétele, hogy az eredmény ábrázolható
legyen; túl nagy x esetén UB. Alignment-ellenőrzésre
std::countr_zero hasznos: egy std::uintptr_t
cím akkor 16-byte aligned, ha countr_zero(p) >= 4; a
nulla cím matematikailag „aligned”, de dereferálni természetesen nem
szabad. A GCC __builtin_popcount és társai extensionök; a
standard változat hordozható, de ugyanazt a gépi utasítást
használhatja.
std::endian és std::byteswapA std::endian C++20-as, fordítás idejű enum:
little, big és native. A
native lehet little vagy big, de
a szabvány megengedi azt is, hogy egyik se legyen (mixed-endian). Ezért
ne feltételezzük vakon a little-endiant:
if constexpr (std::endian::native == std::endian::little) {
// natív little-endian optimalizáció
} else if constexpr (std::endian::native == std::endian::big) {
// natív big-endian optimalizáció
} else {
// mixed-endian: csak explicit byte-assembly
}A std::byteswap C++23-as, <bit>
headerben; integrál típusok byte-jait fordítja meg, bool-ra
nem alkalmazható. Feature-testje
__cpp_lib_byteswap >= 202110L. C++20-as fallbackként
kézi shift vagy compiler intrinsic (__builtin_bswap32)
jöhet szóba. A wire séma azonban ne a natív endiannesshez igazodjon: a
protokoll rögzítse a bájtsorrendet (a hálózati byte order hagyományosan
big-endian), a kód pedig mindig expliciten állítsa elő a byte-okat.
constexpr std::array<std::byte, 4> be32(std::uint32_t v) noexcept {
return {std::byte(v >> 24), std::byte(v >> 16),
std::byte(v >> 8), std::byte(v)};
}
constexpr std::uint32_t from_be32(std::span<const std::byte, 4> b) noexcept {
return (std::to_integer<std::uint32_t>(b[0]) << 24) |
(std::to_integer<std::uint32_t>(b[1]) << 16) |
(std::to_integer<std::uint32_t>(b[2]) << 8) |
std::to_integer<std::uint32_t>(b[3]);
}A std::endian legfeljebb optimalizálásra vagy natív
buffer gyors átalakítására kell; a wire séma ettől független
szerződés.
A std::memcpy akkor definiált értékmegőrző másolásra, ha
a cél egy triviálisan másolható objektum, és a forrás érvényes
objektumreprezentáció. Nem triviálisan másolható osztálynál a
byte-másolás nem indít élettartamot. A C++20 implicit-lifetime szabályai
bizonyos esetekben — malloc, memcpy,
bit_cast — mégis létrehoznak implicit-lifetime típusú
objektumot a tárolóban. Implicit-lifetime típus a skalár, az olyan
osztály, amelynek van legalább egy triviális, nem törölt konstruktora és
triviális destruktora (az aggregátum is ilyen), valamint ezek tömbje. Ha
az osztálynak felhasználói konstruktora vagy destruktora van, a
memcpy nem elég; konstruktor kell. C++23-ban a
std::start_lifetime_as<T> /
start_lifetime_as_array explicit módon indít élettartamot;
feature-testje __cpp_lib_start_lifetime_as >= 202207L,
és csak akkor használjuk, ha a fordítási profil támogatja.
Típus-hozzáférés szempontjából char,
unsigned char és std::byte bármely objektum
reprezentációját olvashatja és írhatja. Más típuson keresztül csak
akkor, ha ott tényleg létezik ilyen objektum. A
reinterpret_cast<FrameHeader*>(buf.data()) ezért nem
teremt FrameHeader objektumot, és a std::byte
tömb alignmentje 1, míg a FrameHeader-é jellemzően 4: a
dereferencia misaligned és lifetime szempontból is UB lehet. A
std::bit_cast és az explicit byte-összerakás kerüli
ezt.
A wire forma 8 byte, rögzített mezőkkel:
| Offset | Méret | Mező | Wire kódolás |
|---|---|---|---|
| 0 | 4 | magic |
uint32, big-endian, 0x46524D31
("FRM1") |
| 4 | 2 | length |
uint16, big-endian |
| 6 | 1 | flags |
uint8 bitmask (Perm) |
| 7 | 1 | version |
uint8 |
C-ben ezt jellemzően memcpy-vel vagy
unsigned char* pásztázással olvastuk. A C++ változat
explicit byte-assemblyt és enum class-t használ; a
sizeof(FrameHeader) nem szerződés, ezért nem
memcpy-zük a structot.
// codec.cpp — C++20
#include <array>
#include <bit>
#include <cassert>
#include <cstddef>
#include <cstdint>
#include <limits>
#include <optional>
#include <span>
enum class Perm : std::uint8_t {
none = 0, read = 1u << 0, write = 1u << 1, exec = 1u << 2,
};
constexpr Perm operator|(Perm a, Perm b) noexcept {
return static_cast<Perm>(static_cast<std::uint8_t>(a) |
static_cast<std::uint8_t>(b));
}
constexpr bool has(Perm set, Perm bit) noexcept {
return (static_cast<std::uint8_t>(set) &
static_cast<std::uint8_t>(bit)) != 0;
}
inline constexpr std::uint32_t kMagic = 0x46524D31u; // "FRM1"
struct FrameHeader {
std::uint32_t magic = kMagic;
std::uint16_t length = 0;
Perm flags = Perm::none;
std::uint8_t version = 1;
};
constexpr std::array<std::byte, 8> encode(const FrameHeader& h) noexcept {
return {std::byte(h.magic >> 24), std::byte(h.magic >> 16),
std::byte(h.magic >> 8), std::byte(h.magic),
std::byte(h.length >> 8), std::byte(h.length),
std::byte(static_cast<std::uint8_t>(h.flags)),
std::byte(h.version)};
}
constexpr std::optional<FrameHeader>
decode(std::span<const std::byte, 8> in) noexcept {
const auto u32 = [&](std::size_t i) {
return (std::to_integer<std::uint32_t>(in[i]) << 24) |
(std::to_integer<std::uint32_t>(in[i + 1]) << 16) |
(std::to_integer<std::uint32_t>(in[i + 2]) << 8) |
std::to_integer<std::uint32_t>(in[i + 3]);
};
const auto u16 = [&](std::size_t i) {
return static_cast<std::uint16_t>(
(std::to_integer<std::uint16_t>(in[i]) << 8) |
std::to_integer<std::uint16_t>(in[i + 1]));
};
if (u32(0) != kMagic) return std::nullopt;
FrameHeader h{};
h.length = u16(4);
h.flags = static_cast<Perm>(std::to_integer<std::uint8_t>(in[6]));
h.version = std::to_integer<std::uint8_t>(in[7]);
return h;
}
int main() {
static_assert(sizeof(float) == sizeof(std::uint32_t));
static_assert(std::numeric_limits<float>::is_iec559);
constexpr auto fbits = std::bit_cast<std::uint32_t>(1.5f);
static_assert(fbits == 0x3FC00000u);
FrameHeader h{};
h.length = 0x1234;
h.flags = Perm::read | Perm::write;
h.version = 2;
const auto wire = encode(h);
const auto back = decode(wire);
assert(back.has_value());
assert(back->length == h.length);
assert(back->version == h.version);
assert(has(back->flags, Perm::read));
assert(has(back->flags, Perm::write));
assert(!has(back->flags, Perm::exec));
}Fordítás a laborban:
g++ -std=c++20 -Wall -Wextra -Wpedantic -O2 codec.cpp -o codecA példa C++20-at igényel; a std::byteswap és a
std::start_lifetime_as külön C++23-es képesség,
feature-testtel ellenőrizendő. A kód a wire byte-okat expliciten állítja
elő, ezért a natív endianness és a sizeof(FrameHeader) nem
befolyásolja a protokollt. A static_assert-ek a szerződést
rögzítik, nem helyettesítik a futtatott teszteket.
std::bit_cast<bool> tetszőleges
uint8_t-ből: a 0/1-en kívüli minta UB.struct paddingjának bit_cast-olása, majd
memcmp-pel hasonlítás: a padding bitek nem
specifikáltak.reinterpret_cast<FrameHeader*>(buf.data()) és
dereferencia: nincs ott FrameHeader objektum, és az
alignment is hibás lehet.std::byteswap használata C++20-ban feature-test nélkül:
a függvény nem feltétlenül létezik.std::popcount signed típusra: nem fordul; előbb
std::make_unsigned_t-vé kell alakítani.std::bit_ceil túl nagy argumentumra: az eredmény nem
ábrázolható, UB.struct layout és a wire séma összemosása: a
kettő nem ugyanaz a szerződés.A bitmaskot scoped enum mögé tesszük, a műveleteket
constexpr függvényekként definiáljuk; a
std::bitset a nem nevesített, fix szélességű bitsorozat
eszköze. A std::bit_cast reprezentációt másol, nem értéket
konvertál; feltétele a méret-egyezés, a triviális másolhatóság és a
megfelelő To-érték létezése. A <bit>
portable számlálást, forgatást és kettőhatvány-kezelést ad. Az
endianness wire-szinten explicit séma, nem a
std::endian::native függvénye. A memcpy csak
triviálisan másolható objektumra és érvényes élettartam mellett
biztonságos; a reinterpret_cast + dereferencia helyett
bit_cast vagy explicit byte-dekódolás kell.
Magyarázó feladat. Magyarázd meg, miért UB a
std::bit_cast<bool>(std::uint8_t{2}), és miért
definiált a std::bit_cast<std::uint32_t>(1.5f) egy
IEC 559 float esetén. Milyen fordítás idejű ellenőrzéseket
tennél a kódba?
Kódolási feladat. Írj
constexpr std::uint32_t bswap32(std::uint32_t) függvényt,
amely C++20-ban portable, C++23-ban pedig std::byteswap-et
használ, ha elérhető. Bizonyítsd
static_assert-ekkel.
Diagnosztikai feladat. Az alábbi kód egy
std::array<std::byte, 8> bufferból közvetlenül
FrameHeader*-t készít. Sorolj fel legalább három önálló
problémát, és javítsd a kódot.
std::array<std::byte, 8> buf = receive();
auto* h = reinterpret_cast<FrameHeader*>(buf.data());
if (h->magic == kMagic) { use(h->length, h->flags); }A bool értékkészlete false és
true; a 0x02 bitmintához nem tartozik
bool érték, ezért a bit_cast feltétele sérül,
és a viselkedés UB. A float IEC 559 binary32 esetén minden
32 bites minta értelmezhető (véges, végtelen vagy NaN), és
sizeof(float) == 4, így a uint32_t felé van
megfelelő érték. Ellenőrzés:
static_assert(sizeof(float) == sizeof(std::uint32_t)); és
static_assert(std::numeric_limits<float>::is_iec559);.
Nem IEC 559 vagy eltérő méret esetén a bit_cast nem
használható erre a célra; ott marad az explicit byte-kezelés. A megoldás
helyességi feltétele, hogy a float mérete és IEC 559 volta
fordítás időben rögzítve legyen.
#include <bit>
#include <cstdint>
constexpr std::uint32_t bswap32(std::uint32_t x) noexcept {
#if defined(__cpp_lib_byteswap) && __cpp_lib_byteswap >= 202110L
return std::byteswap(x);
#else
return ((x & 0x000000FFu) << 24) |
((x & 0x0000FF00u) << 8) |
((x & 0x00FF0000u) >> 8) |
((x & 0xFF000000u) >> 24);
#endif
}
static_assert(bswap32(0x11223344u) == 0x44332211u);
static_assert(bswap32(0x00000000u) == 0x00000000u);
static_assert(bswap32(0xFFFFFFFFu) == 0xFFFFFFFFu);Az elágazás fordítás idejű: C++23-ban a
__cpp_lib_byteswap makró definiált, C++20-ban nem. A kézi
változat minden byte-ot maszkolással emel ki, ezért nem függ a natív
endiannesstől, és nem használ signed shiftet. Ellenőrzési feltétel: a
három static_assert mellett érdemes 16 és 64 bites típusra
is általánosítani, és std::uint8_t-re külön ellenőrizni,
hogy a byteswap ne kapjon bool-t.
Problémák: (1) a buf tárolójában nem létezik
FrameHeader objektum, a reinterpret_cast nem
indít élettartamot, a dereferencia strict aliasing és lifetime
szempontból UB; (2) a std::byte tömb alignmentje 1, a
FrameHeader-é jellemzően 4, így a pointer misaligned lehet,
és a dereferencia alignment-UB; (3) a wire big-endian, a natív gép
little-endian lehet, ezért a magic, length és
flags értelmezése is hibás; (4) a
sizeof(FrameHeader) nem garantáltan 8, a struct paddingot
tartalmazhat, ezért a nyers struct-olvasás még akkor sem hordozható, ha
véletlenül egyezik a méret. Javítás: a
decode(std::span<const std::byte, 8>) függvény
expliciten rakja össze a byte-okat, ellenőrizze a magicet, és csak utána
használja a mezőket:
std::array<std::byte, 8> buf = receive();
if (const auto h = decode(buf)) {
use(h->length, h->flags);
}A decode a std::to_integer-rel olvas, nem
végez misaligned dereferenciát, és a wire endiannesshez igazodik.
Ellenőrzési feltétel: a javított kód nem tartalmaz
reinterpret_cast-ot a bufferre, a magic
ellenőrzés a dekódolás része, és a hossz/flag mezők csak sikeres
dekódolás után használhatók. A laborban az alignment- és UB-sanitizer a
régi változatnál jelezhet; a javított változat tervezetten nem végez
ilyen műveletet.