32.5 Szövegkódolás, locale és random

A C szövegkezelését a Szövegkódolás, locale és UTF-8 C-ben (7.5) részben byte-sorozatként, nullterminált sztringként és locale-függő C függvényekkel modelláltuk. A chrono, duration és időpont (32.4) részben rögzítettük az időmérés és az óraolvasás szerződését, a Multidimenziós view és numeric utility térkép (31.5) rész pedig a numerikus utility-k térképét adta. Most a C++ oldalon ugyanazt a kérdést vesszük elő: mit ígér a standard könyvtár a szövegről, fájlokról, időről és I/O-ról, és hol ér véget ez az ígéret a libc/POSIX szemantikához képest. Ez a rész a szövegkódolás, a locale és a véletlenszámok hármasára szűkít; a fájlrendszer és a chrono részletei a filesystem, path és OS-határ (32.3), illetve a chrono, duration és időpont (32.4) anyagában maradnak.

A C++20 standard és a C++23 munkatervezet szövege nem automatikusan ellenőrzött ebben a fejezetben; a konkrét szakaszok, implementációs részletek és feature-test értékek a labor profiljában ellenőrizendők. A kódpéldák szemléltetők; a fejezet nem állítja, hogy azokat lefuttatták.

32.5.1 A karaktertípus és a tároló elválasztása

A C++ szövegmodelljének első döntése, hogy a karaktertípus és a kódolás két külön kérdés. A std::string byte-tároló: a size() a tárolt char objektumok számát adja, nem a felhasználó által érzékelt karakterekét, és nem is a Unicode kódpontokét. A std::u8string char8_t kódegységeket tárol, de a tartalom UTF-8 érvényességét nem ellenőrzi. A típus tehát a tárolási egységet rögzíti, a kódolást nem.

Ehhez három fogalmat kell szétválasztani. A kódpont (code point) egy Unicode skaláris érték: 0≤c≤0x10FFFF0 \le c \le 0x10FFFF, a szürrogát tartomány kivételével. A kódegység (code unit) az adott kódolás alapegysége: UTF-8-ban egy byte, UTF-16-ban egy 16 bites szó, UTF-32-ban egy 32 bites szó. A grapheme cluster a felhasználó által egy karakternek érzékelt egység, amely több kódpontból állhat: az „e” és a U+0301 combining acute accent két kódpont, de egy grapheme; egy család-emoji ZWJ-szekvenciája pedig több kódpontból álló grapheme. A std::u8string::size() a kódegységek számát adja, a utf8_codepoints jellegű számlálás a kódpontokét; egyik sem grapheme-számláló.

32.5.1.1 A karaktertípusok és literáljaik

Típus Standard szerinti szerep Megjegyzés
char byte-tároló, UTF-8 byte-sor is lehet signednesse implementációfüggő
char8_t UTF-8 kódegység C++20; az unsigned char-ral azonos méret, alignment és reprezentáció, tehát CHAR_BIT bit
char16_t UTF-16 kódegység C++11; legalább 16 bit, a gyakorlatban 16
char32_t UTF-32 kódpont C++11; legalább 32 bit, a gyakorlatban 32
wchar_t implementációfüggő széles karakter szélessége és kódolása platformfüggő; nem hordozható Unicode-tároló

A literálok típusa ezt követi: "..." típusa const char[N]; u8"..." C++20-tól const char8_t[N], C++17-ig const char[N]; u"..." const char16_t[N]; U"..." const char32_t[N]; L"..." const wchar_t[N]. A u8 literál típusváltozása C++20-ban szándékos, de a C++17-es kódbázisokban törő változás: a std::string-et váró függvénynek átadott u8"..." már nem fordul, és a std::cout << u8"..." sem, mert a char8_t-re nincs stream-insertion. A std::u8string és a std::string között nincs implicit konverzió; a byte-szintű átvételt explicit módon kell megtenni, például std::string(u8s.begin(), u8s.end()) alakban, tudva, hogy a char8_t értékek char-ba konvertálása a signedness miatt negatív értékeket adhat, miközben a byte-reprezentáció változatlan.

A char16_t és char32_t Unicode-kódegység, de a std::u16string és std::u32string sem normalizál, sem grapheme-et nem számol. A std::wstring használata akkor indokolt, ha a platform API wchar_t-t vár; hordozható adattárolásra nem.

32.5.2 Konverziók, locale és az elavult codecvt

A std::locale nem kódoláskonverter. Az a szerepe, hogy a formázási, collation és karakterosztályozási faceteket biztosítsa: például a számok ezres tagolását, a dátumformátumot, a std::regex karakterosztályait. A locale neve platformfüggő ("hu_HU.UTF-8", "C"), a std::locale("") a környezetből vesz, a std::locale::global pedig globális mellékhatással jár. A std::format C++20-ban alapértelmezés szerint locale-független; a locale-t explicit kell átadni, és a L specifikátor jelzi a locale-függő formázást. A C++23-as std::print hasonló elven működik, de használata a __cpp_lib_print feature-testhez és a konkrét library támogatásához kötött.

A Unicode-konverzióra a C++20 standard nem ad kész, hordozható megoldást. A std::codecvt facetek és a std::wstring_convert C++17-ben elavulttá váltak; a C++20-ban még elérhetők, de használatuk kerülendő, és eltávolításuk verziófüggő. A std::mbrtowc/std::wcrtomb C függvények locale-függők és állapotfüggők; a setlocale és a C++ locale kapcsolata platformfüggő. A gyakorlatban platform API (iconv, Win32 MultiByteToWideChar) vagy harmadik fél (ICU) marad. A std::regex szintén locale-függő lehet, de nem UTF-8-tudatos: std::regex a char-on byte-alapon illeszt, std::wregex a wchar_t-n. A legtöbb implementáció visszalépéses, a standard nem garantál lineáris időt; nagy, támadó bemeneten a std::regex ReDoS-ra hajlamos lehet. Kis, ellenőrzött bemenetre vagy kézi parserre érdemes támaszkodni.

32.5.3 Random: engine és distribution

A C++ <random> két külön objektumot ad. Az engine egy UniformRandomBitGenerator: min(), max() és operator() szerződéssel, determinisztikus bit-generátor. A distribution az engine által adott egyenletes bitsorozatot alakítja a kívánt eloszlássá. Az engine és a distribution állapota különálló; a distribution operator() jellemzően nem const, mert belső állapotot módosíthat.

Az std::mt19937 és std::mt19937_64 algoritmusa specifikált, ezért azonos seeddel azonos bitsorozatot adnak. A std::default_random_engine ezzel szemben implementációfüggő, hordozható kódban kerülendő. A distribution kimenete nem garantáltan bitre reprodukálható: a standard a statisztikai tulajdonságokat írja elő, a konkrét algoritmust nem. Ha bitre azonos futtatás kell, rögzített engine és saját eloszlás, vagy dokumentáltan rögzített implementáció szükséges.

32.5.3.1 Seeding és a security kérdése

A seedeléshez a std::random_device népszerű, de nem univerzális security garancia. A std::random_device::entropy() visszatérhet 0.0-val, ami azt jelzi, hogy a forrás determinisztikus lehet; a std::random_device nem garantáltan kriptográfiai erősségű. A std::seed_seq a seed-adatokat keveri, de nem növeli az entrópiát. A std::chrono::steady_clock időbélyegből képzett seed kiszámítható, ezért csak reprodukálható teszthez jó. Biztonsági célra platform CSPRNG kell: Linuxon a getrandom(2), Windowson a BCryptGenRandom; a generált bájtokból legalább 128 bit entrópiával, base32/hex kódolással készül a token, és az összehasonlítás állandó idejű. A std::mt19937 kimenete nem kriptográfiai, akkor sem, ha a seedet std::random_device adta.

A reprodukálható és a biztonsági cél tehát külön API-hoz kötendő: teszthez rögzített seedű std::mt19937/std::mt19937_64 és rögzített distribution; biztonsági célra platform CSPRNG, nem <random>.

32.5.4 Kidolgozott példa: UTF-8 számlálás és seedelt generátor

A következő C++20-as példa a char8_t tárolást és a seedelt generátort mutatja. A utf8_codepoints feltételezi az érvényes UTF-8 bemenetet, és a folytató bájtokból számol; nem validál, és nem grapheme-et számol.

// u8demo.cpp
#include <array>
#include <cstddef>
#include <cstdint>
#include <cstdio>
#include <random>
#include <string_view>

std::size_t utf8_codepoints(std::u8string_view s) noexcept {
    std::size_t n = 0;
    for (unsigned char b : s) {
        if ((b & 0xC0u) != 0x80u) {
            ++n;
        }
    }
    return n;
}

int main() {
    constexpr std::u8string_view text = u8"árvíztűrő";
    std::printf("bytes=%zu codepoints=%zu\n",
                text.size(), utf8_codepoints(text));

    std::random_device rd;
    std::array<std::uint32_t, 8> seed_data{};
    for (auto& x : seed_data) {
        x = static_cast<std::uint32_t>(rd());
    }
    std::seed_seq seq(seed_data.begin(), seed_data.end());
    std::mt19937_64 engine(seq);
    std::uniform_int_distribution<int> d6(1, 6);
    std::printf("d6=%d\n", d6(engine));
}

C++20-at támogató GCC vagy Clang szükséges; a konkrét verziót a labor profil rögzíti. Fordítás:

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

A char8_t és a std::u8string_view elérhetősége a __cpp_char8_t és __cpp_lib_char8_t feature-testtel ellenőrizhető. A példában az u8"árvíztűrő" 13 byte és 9 kódpont; ezt a string literál ismert tartalma adja, nem futtatási eredmény. A d6 kimenete a random_device entrópiájától függ, ezért nem determinisztikus.

32.5.5 Gyakori hibák és javításuk

32.5.6 Feladatok

  1. Magyarázó feladat. Különböztesd meg a kódpont, a kódegység és a grapheme cluster fogalmát. Adj példát az „e” + U+0301 szekvenciára és egy ZWJ-emoji családra. Miért nem adja a std::u8string::size() a grapheme-ek számát?

  2. Kódolási feladat. Írj bool is_valid_utf8(std::string_view) függvényt, amely szigorúan validálja az UTF-8 byte-sorozatot. A std::string_view itt byte-tároló; a függvény ne értelmezze grapheme-ként. Adj legalább négy tesztesetet: üres string, ASCII, túl hosszú kódolás (0xC0 0xAF), szürrogát (0xED 0xA0 0x80) és egy érvényes többszörös byte-os karaktert.

  3. Diagnosztikai feladat. Az alábbi kód egy jelszó-visszaállítási tokent generál. Mi a hiba, ha a cél biztonsági token? Mi a hiba, ha a cél reprodukálható teszt?

std::mt19937 eng(std::random_device{}());
std::uniform_int_distribution<int> d(0, 35);
  1. Diagnosztikai feladat. Egy szolgáltatás std::regex-szel validál felhasználói bemenetet. Miért lehet ez ReDoS-kitett, és miért nem UTF-8-tudatos a std::regex? Milyen ellenőrzést vezetnél be?

32.5.7 Megoldások

32.5.7.1 1. megoldás

A kódpont a Unicode skaláris érték; a kódegység az adott kódolás alapegysége (UTF-8 byte, UTF-16 16 bites szó); a grapheme a felhasználó által érzékelt egység. Az „e” + U+0301 két kódpont, UTF-8-ban három byte, de egy grapheme. A ZWJ-emoji család több kódpontból és több ZWJ-ből áll, mégis egy grapheme. A std::u8string::size() a char8_t kódegységek számát adja, ezért sem a kódpontokét, sem a grapheme-ekét nem. Ellenőrzési feltétel: a válasz mindhárom fogalmat egyértelműen elhatárolja, és a size() értelmét a tárolt kódegységekhez köti.

32.5.7.2 2. megoldás

#include <cstddef>
#include <string_view>

bool is_valid_utf8(std::string_view s) noexcept {
    std::size_t i = 0;
    while (i < s.size()) {
        const unsigned char c = static_cast<unsigned char>(s[i]);
        if (c <= 0x7Fu) {
            ++i;
            continue;
        }

        std::size_t len = 0;
        unsigned char lo = 0x80u;
        unsigned char hi = 0xBFu;

        if (c >= 0xC2u && c <= 0xDFu) {
            len = 2;
        } else if (c >= 0xE0u && c <= 0xEFu) {
            len = 3;
            if (c == 0xE0u) lo = 0xA0u;
            if (c == 0xEDu) hi = 0x9Fu;
        } else if (c >= 0xF0u && c <= 0xF4u) {
            len = 4;
            if (c == 0xF0u) lo = 0x90u;
            if (c == 0xF4u) hi = 0x8Fu;
        } else {
            return false;
        }

        if (i + len > s.size()) return false;
        const unsigned char c1 = static_cast<unsigned char>(s[i + 1]);
        if (c1 < lo || c1 > hi) return false;

        for (std::size_t k = 2; k < len; ++k) {
            const unsigned char ck = static_cast<unsigned char>(s[i + k]);
            if (ck < 0x80u || ck > 0xBFu) return false;
        }
        i += len;
    }
    return true;
}

A 0xC0/0xC1 vezető byte túl hosszú kódolást jelentene, ezért nem szerepel a megengedett tartományban; a 0xE0 és 0xF4 speciális második byte-tartománya a túl hosszú, illetve a U+10FFFF feletti kódpontokat zárja ki; a 0xED a szürrogátokat. Tesztesetek:

#include <cassert>
int main() {
    assert(is_valid_utf8(""));
    assert(is_valid_utf8("abc"));
    assert(!is_valid_utf8("\xC0\xAF"));
    assert(!is_valid_utf8("\xED\xA0\x80"));
    assert(is_valid_utf8("\xC3\xA1"));
}

Ellenőrzési feltétel: a függvény minden nem valid szekvenciára false-t ad, a validakra true-t; a határfeltételek (i + len > s.size(), a folytató byteok tartománya) helyesek.

32.5.7.3 3. megoldás

Biztonsági tokenhez a std::mt19937 nem kriptográfiai; a std::random_device{}() egyetlen unsigned int-et ad, legfeljebb 32 bitet, és lehet determinisztikus; a std::uniform_int_distribution kimenete nem garantáltan bitre reprodukálható, de ez itt nem is biztonsági kérdés. A tokenhez platform CSPRNG (getrandom Linuxon) és legalább 128 bit entrópia kell, base32/hex kódolással, állandó idejű összehasonlítással. Reprodukálható teszthez rögzített seedű std::mt19937_64 és rögzített distribution használható, de a kettő nem ugyanaz az API. Ellenőrzési feltétel: a válasz külön kezeli a biztonsági és a reprodukálhatósági igényt, és nem állítja, hogy a std::random_device CSPRNG.

32.5.7.4 4. megoldás

A legtöbb std::regex implementáció visszalépéses, a standard nem garantál lineáris időt; a bemenet hosszával a futásidő exponenciálisan nőhet. A std::regex a char-on byte-alapú, nem UTF-8-kódpont-tudatos, ezért a . egy byte-ot jelent, nem egy kódpontot. Ellenőrzési feltétel: a bemenet mérete korlátozott, a regex csak kis, ellenőrzött bemenetre fut, és a laborban időmérés történik; nagy bemenetre kézi parser vagy harmadik fél használható.

32.5.8 Összefoglaló

A C++ szövegmodelljében a karaktertípus a tárolási egységet rögzíti, a kódolást nem. A std::string byte-tároló, a std::u8string char8_t-tároló; egyik sem grapheme- és nem is automatikusan validáló konténer. A u8 literál típusa C++20-ban const char8_t[], ami törő változás a C++17-es kódhoz képest. A locale nem encoding; a std::codecvt és a std::wstring_convert elavult, a std::regex nem UTF-8-tudatos és ReDoS-kitett. A random esetében az engine és a distribution külön szerződés: az engine lehet specifikált és reprodukálható, a distribution nem garantáltan bitre az; a std::random_device nem univerzális security garancia, biztonsági célra platform CSPRNG kell.