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.
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:
::value-ként érhető el, amely
static constexpr tag, így if constexpr,
static_assert és nem típusú sablonparaméter feltételében is
állandó kifejezés.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.
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.
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 wireA 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.
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.
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_encodeBuildjegyző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.
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.
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.
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.
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.
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");
}
}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.
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.