31.4 Bitműveletek és objektumreprezentáció C++-ban

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.

31.4.1 Bitmask enum és bitset

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.

31.4.2 std::bit_cast: reprezentáció, nem szemantika

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 binary32

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

31.4.3 Számlálás, forgatás és a <bit> eszközei

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

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.

31.4.4 Endianness: std::endian és std::byteswap

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

31.4.5 memcpy, élettartam és típus-hozzáféré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.

31.4.6 Kidolgozott példa: a C codec fejléce

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 codec

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

31.4.7 Tipikus hibák

31.4.8 Összefoglaló

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.

31.4.9 Feladatok

  1. 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?

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

  3. 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); }

31.4.10 Megoldások

31.4.10.1 1. feladat megoldása

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.

31.4.10.2 2. feladat megoldása

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

31.4.10.3 3. feladat megoldása

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.