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.
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:
,
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ó.
| 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.
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.
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.
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>.
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.cppA 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.
u8"..." típusa C++20-ban
const char8_t[]. A C++17-es
std::string-átvétel és a
std::cout << u8"..." nem fordul. Javítás: explicit
std::string-konverzió vagy byte-szintű kezelés.std::string::size() nem grapheme- és nem
kódpontszám. A combining accent és a ZWJ-emoji több kódpont; a
size() kódegység. Javítás: kódpontszámláló vagy
Unicode-könyvtár, ha grapheme kell.std::random_device nem CSPRNG. Az
entropy() == 0.0 determinisztikus forrást jelezhet.
Javítás: platform CSPRNG biztonsági célra.std::default_random_engine és a distribution
nem hordozható bitre. Javítás: specifikált engine és saját
eloszlás, ha reprodukálhatóság kell.std::regex ReDoS-ra hajlamos és nem
UTF-8-tudatos. Javítás: kis bemenet, kézi parser vagy harmadik
fél.std::locale::global globális
mellékhatás. Javítás: explicit locale a formázási ponton.std::codecvt és a
std::wstring_convert elavult. Javítás: platform
API vagy harmadik fél.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?
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.
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);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?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.
#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.
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.
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ó.
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.