27.1 Type traits, tag dispatch és if constexpr

A generikus C++ kód egyik alapproblémája, hogy ugyanaz az algoritmus típusonként mást jelent: egy 32 bites egész számot fix byte-sorrenddel kell kiírni, egy std::string-et hosszprefixed bájtokkal, egy nyers structot pedig csak akkor szabad memóriaként másolni, ha a típus erre valóban alkalmas. A döntést ideális esetben nem futásidőben, hanem fordításkor hozzuk meg, hogy a nem támogatott eset már a build során, érthető üzenettel bukjon el. A type trait, a tag dispatch és az if constexpr ennek a compile-time ágválasztásnak a három egymást kiegészítő eszköze. Az alábbiakban azt mutatjuk meg, hogyan lehet egy típus tulajdonságából dokumentált implementációt választani, és közben tisztán látni, hogy a trait mit garantál és mit nem.

A korábbi Generikus interfész és sablondiagnosztikai labor (26.6) részben rögzítettük a sablonok példányosításának és a hibaüzenetek olvasásának alapjait; itt ezekre építünk. Ahol a tárolók élettartamára vagy újrafelhasználására hivatkozunk, a Tároló újrafelhasználása és C++ objektumszabályok (20.6) rész definícióit használjuk. A fejezet a C++20 alapot követi; ahol egy képesség újabb, azt külön jelöljük. A megadott forrásjegyzet ehhez a részhez nem tartalmazott ellenőrzött hivatkozást, ezért konkrét ISO-paragrafusra vagy hivatkozható URL-re itt nem támaszkodunk; a megfogalmazás a C++20 nyelvi és könyvtári szerződések könyvbeli összefoglalása, nem független forrásellenőrzés.

27.1.1 Type trait: típusból levezetett compile-time állítás

A type trait olyan sablon, amelynek a paramétere egy (vagy több) típus, az eredménye pedig egy fordítási időben ismert érték vagy egy másik típus. A predikátum alakú trait-ek a std::integral_constant<bool, B> specializációi, ahol B a kérdéses tulajdonság. Ennek a mintának két fontos következménye van:

A std::integral_constant C++11 óta létezik; a std::bool_constant<B> ennek a bool specializációja, C++17 óta. A rövidített alakok: a _v változat az értéket adja (std::is_integral_v<T>), a _t változat a transzformált típust (std::remove_cvref_t<T>). A _v és _t alakok C++17, illetve C++14 óta érhetők el, a remove_cvref pedig C++20-as.

A transzformáló trait-ek közül kettőt érdemes szembeállítani. A std::remove_cvref_t<T> a legfelső szintű const/volatile minősítőt és a referenciát távolítja el: remove_cvref_t<const int&> értéke int. A std::decay_t<T> ennél agresszívebb: a fentieken kívül a tömböt pointerré, a függvényt függvénypointerré alakítja, és a const int-ből int lesz. A decay tehát a függvényparaméter-átadás “romlását” modellezi, a remove_cvref viszont megőrzi az eredeti kategoriát, és ezért generic kódban gyakrabban ez a helyes választás. Ha a cél a típus azonosságának vizsgálata, a decay csendesen más típusra vált, és ez hibás ágválasztáshoz vezet.

A gyakran használt trait-családok röviden:

Család Példa Jelentése
Elsődleges kategória is_integral_v, is_floating_point_v, is_pointer_v, is_array_v a típus nyelvi kategóriája
Összetett tulajdonság is_trivially_copyable_v, is_standard_layout_v, is_nothrow_destructible_v a típus műveleteinek garantált tulajdonsága
Transzformáció remove_cvref_t, decay_t, add_pointer_t, underlying_type_t új típus előállítása
Lekérdezés is_same_v, is_convertible_v, is_invocable_v típusok közötti viszony

A trait tehát nem “varázslat”: a szabvány által definiált, fordításidőben kiértékelt predikátum, amelynek az igazsága a típus deklarációjából következik.

27.1.2 Mit garantál egy trait, és mit nem

A legfontosabb fogalmi elkülönítés a szintaktikai képesség és a szemantikai szerződés között. A trait az előbbiről beszél: arról, hogy a típus műveletei milyen tulajdonságúak a nyelv szabályai szerint. Nem beszél arról, hogy a típus értékei milyen üzleti invariánst hordoznak, hogy két objektum byte-azonos reprezentációja ugyanazt jelenti-e, vagy hogy a másolat egy másik gépen is értelmes lesz.

A std::is_trivially_copyable_v<T> például azt garantálja, hogy a típus másoló/mozgató konstruktora és értékadó operátora triviális, és a destruktora is triviális. Ebből az következik, hogy az objektum reprezentációja byte-onként átmásolható egy másik, azonos típusú objektumba, és az eredmény ugyanaz az érték lesz. Ez azonban nem jelenti azt, hogy a byte-sorozat hordozható: a padding bájtok tartalma nem specifikált, a többbyte-es egészek byte-sorrendje implementációfüggő, a pointerek pedig folyamat- és futásidőfüggők. Egy struct Header { std::uint16_t type; std::uint32_t len; } tipikusan trivially copyable, de a sizeof(Header) értéke tartalmazhat paddinget, így a nyers memóriakép hossza és tartalma nem szerződés. A trait igaz, a hálózati szerializáció mégsem helyes rá.

A std::is_standard_layout_v<T> azt jelzi, hogy a típus megfelel a C-kompatibilis elrendezés bizonyos nyelvi feltételeinek (például minden nem statikus adattag ugyanabban az osztályban van, nincs virtuális függvény és virtuális bázis, és a hozzáférés-vezérlés egységes). Ez hasznos előfeltétele az offsetof használatának és a C ABI-val való együttműködésnek, de nem garantálja, hogy két különböző fordító vagy két architektúra ugyanazt a layoutot állítja elő. A standard layout tehát nyelvi tulajdonság, nem ABI-szerződés.

A destruktibilitásnál három kérdést kell szétválasztani. Az is_destructible_v<T> azt kérdezi, hogy a t.~T() kifejezés jól formált-e. Az is_trivially_destructible_v<T> azt, hogy a destruktor triviális, tehát nem kell meghívni a tároló felszabadításakor. Az is_nothrow_destructible_v<T> pedig azt, hogy a destruktor nem dob kivételt. Egy típus lehet destruktibilis, de nem triviálisan az, és lehet triviálisan destruktibilis, de a gyakorlatban mégis drága a használata. A tároló újrafelhasználásánál ez azért számít, mert a triviális destruktorból a fordító tudja, hogy a nyers tároló felszabadítása előtt nincs teendő; ez a Tároló újrafelhasználása és C++ objektumszabályok (20.6) részben tárgyalt objektumszabályok egyik gyakorlati következménye.

Végül a trait soha nem bizonyít futásidejű helyességet. Attól, hogy is_integral_v<T> igaz, még nem tudjuk, hogy a szám a protokoll által előírt tartományban van-e. A trait a típusról szól, nem az értékről.

27.1.3 Tag dispatch: címkével választott túlterhelés

A tag dispatch (címkealapú szétválasztás) a trait értékét egy üres címketípusba csomagolja, és a választást a függvény-túlterhelésre bízza. A std::true_type és a std::false_type a két alapvető címke, de tetszőleges std::integral_constant specializáció használható.

#include <cstddef>
#include <cstdint>
#include <type_traits>
#include <vector>

namespace wire {

inline void put_u32be(std::vector<std::byte>& out, std::uint32_t v) {
    out.push_back(static_cast<std::byte>((v >> 24) & 0xFFu));
    out.push_back(static_cast<std::byte>((v >> 16) & 0xFFu));
    out.push_back(static_cast<std::byte>((v >> 8) & 0xFFu));
    out.push_back(static_cast<std::byte>(v & 0xFFu));
}

// Ág 1: előjel nélküli 32 bites egész -> fix byte-sorrend
template <class T>
void encode_impl(std::vector<std::byte>& out, const T& v, std::true_type) {
    static_assert(std::is_unsigned_v<T>, "elojel nelkuli egesz szukseges");
    put_u32be(out, static_cast<std::uint32_t>(v));
}

// Ág 2: minden más -> csak trivially copyable esetén
template <class T>
void encode_impl(std::vector<std::byte>& out, const T& v, std::false_type) {
    static_assert(std::is_trivially_copyable_v<T>,
                  "encode: nem tamogatott tipus");
    const auto* p = reinterpret_cast<const std::byte*>(&v);
    out.insert(out.end(), p, p + sizeof(T));
}

template <class T>
void encode(std::vector<std::byte>& out, const T& v) {
    encode_impl(out, v,
        std::bool_constant<std::is_integral_v<T> && sizeof(T) == 4>{});
}

} // namespace wire

A mechanizmus lényege, hogy az encode_impl két túlterhelése közül a harmadik paraméter típusa dönt. A hívó oldalon nincs feltétel, nincs if: a fordítás során kiválasztott overloadban lévő static_assert csak akkor értékelődik ki, amikor az adott overload példányosul. A nem hívott ág törzse nem generálódik, így az ott szereplő static_assert sem bukik el. Fontos azonban, hogy a deklarációk mindig láthatók, és ha két overload címkéje azonos, a hívás kétértelmű lesz.

A tag dispatch előnye, hogy C++11/14-gyel is működik, jól illeszkedik az overload feloldáshoz, és a kiválasztott ág a hívási helyen dokumentálható. Hátránya, hogy a döntés nem lokális: a logika szétoszlik a segédfüggvények között, és a címke típusát minden híváshoz elő kell állítani. Ezért új kódban általában az if constexpr a természetesebb, a tag dispatch-et pedig ott érdemes használni, ahol overload feloldásra vagy testreszabható viselkedésre van szükség.

27.1.4 if constexpr és a discarded branch

Az if constexpr C++17-es nyelvi elem: a feltétele constant expression, és a nem kiválasztott ág discarded statement. A discarded ág nem példányosul, így az ott szereplő, a sablonparamétertől függő kifejezések nem okoznak fordítási hibát. Ez teszi lehetővé, hogy egyetlen függvénytörzsben több, egymást kizáró implementációt írjunk le.

Két félreértést érdemes elkerülni. Egyrészt a discarded ágnak szintaktikailag érvényesnek kell lennie, és a nem a sablonparamétertől függő neveket a fordító ellenőrzi; csak a függő részek nem példányosulnak. Másrészt az if constexpr nem helyettesíti a requires-t: a túlterhelés kiválasztásában nem vesz részt, csak a kiválasztott függvény törzsében ágazik el. A visszatérési típusra viszont hathat. Explicit visszatérési típusnál nem változtat rajta; levezetett (auto) visszatérési típusnál azonban a discarded ág return utasításai nem vesznek részt a levezetésben, ezért két specializáció eltérő típust adhat vissza — egy specializáción belül csak a kiválasztott ág return-jeinek kell egyezniük:

// constexpr_return.cpp — C++20: auto visszatérési típus és if constexpr
#include <type_traits>

template <class T>
auto widen(T v) {
    if constexpr (std::is_integral_v<T>) {
        return static_cast<int>(v);        // integrál T: int
    } else {
        return static_cast<double>(v);     // egyéb T: double
    }                                      // a discarded return nem vesz részt
}

static_assert(std::is_same_v<decltype(widen(short{1})), int>);
static_assert(std::is_same_v<decltype(widen(1.5f)), double>);

template <class T>
int widen_fixed(T v) {                     // explicit visszatérési típus:
    if constexpr (std::is_integral_v<T>) return v;   // mindkét ág int-et ad
    else return static_cast<int>(v);
}
static_assert(std::is_same_v<decltype(widen_fixed(1.5f)), int>);

int main() {}

Buildjegyzőkönyv (lab-gcc profil). g++ -std=c++20 -Wall -Wextra -Wpedantic constexpr_return.cpp figyelmeztetés nélkül fordul: a widen(short{1}) típusa int, a widen(1.5f) típusa double, a widen_fixed mindkét esetben int.

A static_assert ezzel együtt a legjobb diagnosztikai eszköz. A C++20-as alaphoz hordozható minta a sablonparamétertől függő hamis érték:

template <class> inline constexpr bool always_false_v = false;

template <class T>
void encode(std::vector<std::byte>&, const T&) {
    static_assert(always_false_v<T>, "encode: nem tamogatott tipus");
}

A static_assert(false, ...) önmagában a nem függő esetben már a sablon definíciójakor hibát adna, ezért kell a feltételt a T-től függővé tenni. A C++23 ezt a korlátot enyhíti a discarded ágakban, de ezt a céleszköz támogatásához kell kötni; C++20-on a fenti minta a biztonságos.

27.1.5 Kidolgozott példa: típusvezérelt wire-kódolás

A következő példa összekapcsolja a három eszközt. A wire::encode célja, hogy egy értéket determinisztikus byte-folyamba írjon. A string hosszprefixed, az előjel nélküli 32 bites egész fix big-endian, minden más csak akkor megy át, ha trivially copyable — és még akkor is figyelmeztetünk a padding veszélyére.

// wire_encode.cpp
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <limits>
#include <stdexcept>
#include <string>
#include <type_traits>
#include <vector>

namespace wire {

template <class> inline constexpr bool always_false_v = false;

// A protokoll string-limitje: a 32 bites hosszmező tartománya. Ha a protokoll
// kisebb közös frame-limitet ír elő, ide azt kell írni.
inline constexpr std::size_t max_string_len =
    std::numeric_limits<std::uint32_t>::max();

// Ellenőrzött hossz: a kimenet módosítása ELŐTT dönt; ugyanezt az értéket
// írjuk a prefixbe és ennyi bájtot másolunk.
inline std::uint32_t checked_wire_len(std::size_t n) {
    if (n > max_string_len) {
        throw std::length_error("wire: string tul hosszu a 32 bites hosszmezohoz");
    }
    return static_cast<std::uint32_t>(n);
}

inline void put_u32be(std::vector<std::byte>& out, std::uint32_t v) {
    out.push_back(static_cast<std::byte>((v >> 24) & 0xFFu));
    out.push_back(static_cast<std::byte>((v >> 16) & 0xFFu));
    out.push_back(static_cast<std::byte>((v >> 8) & 0xFFu));
    out.push_back(static_cast<std::byte>(v & 0xFFu));
}

template <class T>
void encode(std::vector<std::byte>& out, const T& value) {
    if constexpr (std::is_same_v<T, std::string>) {
        const std::uint32_t len = checked_wire_len(value.size());   // dobhat
        put_u32be(out, len);
        const auto* p = reinterpret_cast<const std::byte*>(value.data());
        out.insert(out.end(), p, p + len);
    } else if constexpr (std::is_integral_v<T> && sizeof(T) == 4) {
        static_assert(std::is_unsigned_v<T>,
                      "32 bites elojeles egesz nem tamogatott");
        put_u32be(out, static_cast<std::uint32_t>(value));
    } else if constexpr (std::is_trivially_copyable_v<T>) {
        const auto* p = reinterpret_cast<const std::byte*>(&value);
        out.insert(out.end(), p, p + sizeof(T));
    } else {
        static_assert(always_false_v<T>, "encode: nem tamogatott tipus");
    }
}

} // namespace wire

static bool rejects(std::size_t n) {
    try { (void)wire::checked_wire_len(n); return false; }
    catch (const std::length_error&) { return true; }
}

int main() {
    std::vector<std::byte> out;
    wire::encode(out, std::uint32_t{0x01020304u});   // 4 bájt
    wire::encode(out, std::string{"OK"});            // 4 bájt hossz + 2 bájt payload

    const unsigned char expected[] = {0x01, 0x02, 0x03, 0x04,   // u32, big-endian
                                      0x00, 0x00, 0x00, 0x02,   // string hossza
                                      0x4F, 0x4B};              // 'O' 'K'
    if (out.size() != sizeof expected ||
        std::memcmp(out.data(), expected, sizeof expected) != 0) {
        return 1;
    }

    // Határérték-próba allokáció nélkül: csak a helper fut.
    if (wire::checked_wire_len(0) != 0 || rejects(0)) return 2;
    if (rejects(wire::max_string_len)) return 3;
    if constexpr (sizeof(std::size_t) > sizeof(std::uint32_t)) {
        if (!rejects(wire::max_string_len + 1)) return 4;
    }
    return 0;
}

A normál kimenet 10 bájt: a 32 bites egész 4 bájtja, majd a string framing-je (4 bájtos hosszprefix) és payload-ja (2 bájt). A main nem csak a méretet, hanem a teljes várt bájtsort ellenőrzi, így a fordított bájtsorrendet és a hiányzó hosszprefixet is észreveszi. A string hosszát a checked_wire_len a kimenet módosítása előtt ellenőrzi; a prefixbe írt és a másolt hossz ugyanaz az ellenőrzött érték. A helper külön, allokáció nélkül tesztelhető a 0, a maximális és a maximálisnál eggyel nagyobb hosszal.

Buildparancs GCC-vel és Clangdel, rögzített nyelvi móddal:

g++ -std=c++20 -Wall -Wextra -Wpedantic -O2 wire_encode.cpp -o wire_encode
clang++ -std=c++20 -Wall -Wextra -Wpedantic -O2 wire_encode.cpp -o wire_encode

Buildjegyzőkönyv (lab-gcc profil). A GCC-s build figyelmeztetés nélkül fordult, a ./wire_encode 0-val lépett ki; a Clang-sort a nem ellenőrzött lab-clang helyett a Clang 22 (ziglang) fordítóval próbáltuk, szintén 0 kilépési kóddal. Két szándékos hibával is lefuttattuk: a hosszprefix kihagyása és a little-endian put_u32be egyaránt 1-es kilépési kódot adott. A határérték-próba 64 bites size_t mellett a max_string_len + 1 értéket elutasította; ez a 64→32 bites szűkítés védelmét igazolja, nem egy több GiB-os string tényleges kódolását. A példa tanulsága hármas. Először, az ágválasztás a típus tulajdonságán alapul, és a nem támogatott típus már fordításkor elbukik. Másodszor, az is_integral_v<T> && sizeof(T) == 4 ágban külön static_assert védi az előjel nélküliséget: a feltétel igaz lehet egy előjeles típusra is, és ilyenkor nem az ágválasztás, hanem az ágon belüli követelmény bukik el. Harmadszor, a trivially copyable ág nyelvileg helyes, de a padding és a byte-sorrend miatt nem feltétlenül determinisztikus; a trait itt a memóriabiztonságot igazolja, a protokoll-szerződést nem.

27.1.6 Tipikus hibák

A leggyakoribb hiba a decay_t és a remove_cvref_t összecserélése. Ha egy tömböt adunk át, a decay_t<T> pointerré alakítja, és az “ez tömb?” kérdésre hamis választ ad; ilyenkor remove_cvref_t vagy remove_reference_t kell. A második hiba, hogy a szerző a trivially copyable tényt összekeveri a szerializálhatósággal: a padding és az endianness miatt a nyers byte-másolat nem hordozható, és két különböző futás eltérő bájtokat adhat. A harmadik, hogy a static_assert feltételét nem teszi függővé, ezért a sablon már a definíciókor elbukik. A negyedik, hogy a tag dispatch-nél elfelejt egy címkéhez overloadot adni, és a hiba nem ott jelentkezik, ahol a hiány van. Végül az ötödik, hogy a trait-re mint üzleti invariáns bizonyítékára hivatkoznak: attól, hogy egy típus másolható, még nem biztos, hogy a másolat ugyanazt a logikai állapotot jelenti.

27.1.7 Összefoglaló

A type trait a típusról tett, fordításidőben kiértékelt állítás; az integral_constant/bool_constant adja az értéket és a címkét, a _v/_t alakok pedig a rövid használatot. A tag dispatch a trait értékét overload feloldással fordítja implementációválasztásra, az if constexpr pedig egyetlen törzsön belül teszi lehetővé a nem példányosuló ágakat. Mindhárom eszköz a szintaktikai képességről dönt; a szemantikai szerződést, a paddinget, a byte-sorrendet és az üzleti invariánst nem ismeri. A trait tehát szükséges, de nem elégséges feltétele a helyes generikus implementációnak.

27.1.8 Feladatok

  1. Magyarázó feladat. Definiáld, mit garantál a std::is_trivially_copyable_v<T>, és adj egy olyan struct-ot, amelyre a trait igaz, a nyers byte-másolat mégsem alkalmas hálózati protokollhoz. Indokold a padding és a byte-sorrend szerepét.

  2. Kódolási feladat. Írd át a fejezet wire::encode függvényét tag dispatch-re úgy, hogy a viselkedés változatlan maradjon, és a nem támogatott típus továbbra is fordításkor bukjon el. Készíts hozzá egy kis main-t, amely legalább egy elfogadott és egy elutasított típust kipróbál.

  3. Diagnosztikai feladat. Az alábbi kódrészlet szándékosan hibás. Magyarázd meg, miért nem fordul le, és javítsd minimális változtatással:

    template <class T>
    void f(T) {
        if constexpr (std::is_integral_v<T>) {
            static_assert(false, "nem egesz");
        }
    }
  4. Tervezési feladat. Egészítsd ki a wire::encode-ot egy std::vector<std::uint8_t> és egy std::array<std::byte, N> esetére. Döntsd el, melyik trait-re építesz, és indokold, miért nem a is_trivially_copyable-re.

27.1.9 Megoldások

1. megoldás. A is_trivially_copyable_v<T> azt garantálja, hogy a másoló/mozgató műveletek és a destruktor triviálisak, ezért az objektum reprezentációja byte-onként átmásolható egy másik, azonos típusú objektumba, és az eredmény ugyanaz az érték. Például:

struct Header {
    std::uint16_t type;
    std::uint32_t len;
};
static_assert(std::is_trivially_copyable_v<Header>);

A sizeof(Header) a 4 bájtos igazítás miatt jellemzően 8, és a type után 2 bájt padding lehet, amelynek tartalma nem specifikált. A len byte-sorrendje implementációfüggő. Ezért a memcpy-vel előállított kép hossza és tartalma nem hordozható protokoll; a helyes megoldás a mezőnkénti, fix szélességű, fix endiannnessű kódolás. Ellenőrzési feltétel: a válasz nevezze meg a paddingot és a byte-sorrendet, és ne állítsa, hogy a trait hamis.

2. megoldás. A tag dispatch változat:

template <class T>
void encode_impl(std::vector<std::byte>& out, const T& value, std::true_type) {
    put_u32be(out, static_cast<std::uint32_t>(value));
}

template <class T>
void encode_impl(std::vector<std::byte>& out, const T& value, std::false_type) {
    static_assert(std::is_trivially_copyable_v<T>,
                  "encode: nem tamogatott tipus");
    const auto* p = reinterpret_cast<const std::byte*>(&value);
    out.insert(out.end(), p, p + sizeof(T));
}

template <class T>
void encode(std::vector<std::byte>& out, const T& value) {
    if constexpr (std::is_same_v<T, std::string>) {
        put_u32be(out, static_cast<std::uint32_t>(value.size()));
        const auto* p = reinterpret_cast<const std::byte*>(value.data());
        out.insert(out.end(), p, p + value.size());
    } else {
        encode_impl(out, value,
            std::bool_constant<std::is_integral_v<T> && sizeof(T) == 4>{});
    }
}

A stringág maradhat if constexpr, a numerikus/nyers ágat a címke választja. Az elutasított típusra a false_type overloadban futó static_assert ad hibát. Ellenőrzési feltétel: a std::uint32_t elfogadott, a double vagy egy nem trivially copyable típus fordítási hibát ad, és a hibaüzenet tartalmazza a szándékolt szöveget.

3. megoldás. A static_assert(false, ...) feltétele nem függ a sablonparamétertől, ezért a fordító már a sablon definíciójakor kiértékeli, és hibát ad, függetlenül attól, hogy az ágat kiválasztják-e. A minimális javítás a feltétel függővé tétele:

template <class> inline constexpr bool always_false_v = false;

template <class T>
void f(T) {
    if constexpr (std::is_integral_v<T>) {
        static_assert(always_false_v<T>, "nem egesz");
    }
}

Így a hiba csak akkor jelentkezik, ha a T egész típusú, és az ág példányosul. Ellenőrzési feltétel: f(1) fordítási hibát adjon, f(1.0) ne.

4. megoldás. A std::vector<std::uint8_t> és a std::array<std::byte, N> esetében a cél a hossz + nyers bájtok kiírása. A std::array esetén a std::is_trivially_copyable_v igaz lehet, de az elemek és a padding miatt nem ez a jó szerződés; használjunk explicit hosszúságot és data()-t:

template <std::size_t N>
void encode(std::vector<std::byte>& out,
            const std::array<std::byte, N>& value) {
    put_u32be(out, static_cast<std::uint32_t>(N));
    out.insert(out.end(), value.begin(), value.end());
}

A std::vector<std::uint8_t> esetén a value.data() és value.size() adja a szerződést; a is_trivially_copyable az elemtípusra igaz lehet, de a tárolóra nem ez a lényeg. Ellenőrzési feltétel: a kódolás hosszprefixed legyen, és a visszafejtés ellenőrizze a hossz egyezését.