46.5 Hibaminták, döntési táblák és önellenőrző térkép

Ez a fejezet nem új mechanizmust vezet be, hanem a korábbi részek újraelérésének eszközeit rendezi össze: hibából szabályhoz, döntési helyzetből szerződéshez, tanulási célból ellenőrizhető bizonyítékhoz vezető útvonalakat ad. A cél az, hogy a kurzus anyaga ne lineáris olvasmányként, hanem kereshető referenciaként is használható legyen. Ezért itt nem ismételjük meg a definíciókat és a levezetéseket; minden sor egy korábbi részre mutat, ahol a feltételek és az indoklás megtalálhatók. A táblázatok használatának szabálya: előbb a megfigyelt tünetet vagy a meghozandó döntést azonosítod, aztán kikeresed a sort, végül visszalépve elolvasod az adott rész előfeltételeit. Egy táblázatsor önmagában nem bizonyíték, csak mutató.

46.5.1 A döntési táblák használata

46.5.1.1 Owner, borrower, move, copy és API paraméter

A visszatérő hibaforrás nem a szintaxis, hanem hogy a felek nem ugyanazt értik tulajdonláson. Az alábbi tábla a kérdést fordítja döntéssé; a részletes szerződéseket a Tulajdonlási szerződés és cleanup ágak, az Owner, view és erőforrásszerződés C++ API-ban, a Rule of zero, three és five és a Move constructor, moved-from állapot és std::move tartalmazza.

Helyzet Választás Következmény Tipikus hiba
A függvény új erőforrást ad visszatérési érték owner (unique_ptr, dokumentált free) a hívó felel a lezárásért nyers pointer ownerként, dokumentálatlanul
A függvény csak olvas const T*, std::string_view, std::span nincs másolás, nincs tulajdonátadás a view túléli az ownert
A függvény átveszi a tulajdont érték szerinti owner paraméter vagy unique_ptr&& a hívó tudja, hogy átadta const& átvétel, majd belső free
Az erőforrás lehet „nincs” unique_ptr, std::optional owner a hiány típusban látszik nullable nyers pointer ownerként
A paramétert eltároljuk érték vagy owner-másolat az élettartam független lesz string_view tagként tárolva
Nagy objektumot adunk át const& vagy span nincs másolás temporary const&, amely a kifejezés végén elhal

A döntés lényege mindig ugyanaz a kérdés: ki zárja le az erőforrást, és meddig él a hozzáférés? Ha a válasz nem fér bele egy mondatba, az API szerződése hiányos.

A következő C példa a „tulajdonos visszatérési érték” sort mutatja; a fordításhoz szükséges include-ok és a parancs is szerepelnek. A futtatás és a viselkedés ellenőrzése a saját laborprofilodban történik, ezt a fejezet nem állítja mérésnek.

/* c17: gcc -std=c17 -Wall -Wextra -Wpedantic -O2 -o dup dup.c */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

/* Szerződés: a visszatérési érték owner, a hívó free()-eli.
   Bemenet: borrowed, csak a hívás idejére kell érvényesnek lennie. */
char *dup_upper(const char *borrowed)
{
    if (borrowed == NULL) return NULL;
    size_t n = strlen(borrowed);
    char *owner = malloc(n + 1);
    if (owner == NULL) return NULL;
    for (size_t i = 0; i <= n; ++i) {
        char c = borrowed[i];
        owner[i] = (c >= 'a' && c <= 'z') ? (char)(c - 'a' + 'A') : c;
    }
    return owner;
}

int main(void)
{
    char *s = dup_upper("devops");
    if (s == NULL) { fputs("alloc failure\n", stderr); return 1; }
    puts(s);
    free(s);   /* owner lezárása */
    s = NULL;  /* a dangling újrahasználat megelőzése */
    return 0;
}

A C++ változat ugyanezt a döntést mutatja be a nem tulajdonló string_view és a tulajdonló std::string párral; a lényeg, hogy a view-t nem tároljuk a hívás után.

// c++20: g++ -std=c++20 -Wall -Wextra -Wpedantic -fsanitize=address -o count count.cpp
#include <cstdio>
#include <string>
#include <string_view>

// Szerződés: s csak a hívás idejére kell érvényesnek lennie (borrowed).
std::size_t count_upper(std::string_view s)
{
    std::size_t n = 0;
    for (char c : s)
        if (c >= 'A' && c <= 'Z') ++n;
    return n;
}

int main()
{
    std::string owner{"DevOps"};              // owner: élettartama main vége
    std::printf("%zu\n", count_upper(owner)); // a view az ownerre mutat
    return 0;
}

A tiltott minta ugyanennek a tükörképe: std::string_view v = std::string{"tmp"}; után a temporary elhal, a view dangling lesz. A helyes változat vagy owner stringet tart, vagy a view élettartamát a hívásra korlátozza; erről a Temporary, value category és dangling és a Borrowed range, dangling és iterator lifetime szól.

46.5.1.2 Pointer, tömb, span, string és view invalidációs index

Az alábbi index nem típuslexikon, hanem invalidációs térkép: azt mutatja, melyik művelet teszi érvénytelenné a hozzáférést. A részletek a Pointeraritmetika, objektumhatár és provenance, a Tárolóválasztási és invalidációs labor és a span, byte és bounds-szerződések részekben találhatók.

Hozzáférés Érvénytelenné teszi Ki az owner
T* nyers pointer free, realloc, a mögöttes objektum élettartamának vége a szerződés mondja meg, nem a típus
T[N] tömb a tömb hatókörének és élettartamának vége a tömböt tartalmazó objektum
std::span<T> a mögöttes tároló átméretezése vagy elhalása nem tulajdonol
std::string_view az owner string módosítása, elhalása nem tulajdonol
std::string::data() insert, erase, újraallokálás a string objektum
vector::data(), vector iterator push_back, reserve, insert a vector objektum
asszociatív tároló iteratora erase az adott elemen a tároló objektum

Formálisan: ha egy hozzáférés az owner [b,b+n)[b, b+n) tartományára mutat, akkor érvényes mindaddig, amíg az owner objektum élettartama tart és a tartomány nem invalidálódott. Az end és az one-past érték nem dereferálható; ez a fél-nyílt tartomány szerződése. Az invalidáció nem mindig jár együtt memóriahibával: a string kis méretű optimalizálása miatt a buffer helyben is átíródhat, ezért a régi data() pointer csendesen más tartalmat mutathat.

46.5.1.3 C és C++: inicializálás, élettartam, aliasing

Ez a táblázat kizárólag a két nyelv eltérésének gyors felidézésére való; a C oldalt a Scope, linkage, storage duration és lifetime és az Effective type, aliasing és objektumreprezentáció, a C++ oldalt az Az inicializálás különböző alakjai és a Tároló újrafelhasználása és C++ objektumszabályok tárgyalja.

Kérdés C (C17/C23) C++ (C++20)
Élettartam kezdete tárolási idő szerint: statikus a program indulásakor, automatikus a blokkba lépéskor, allokált a foglaláskor konstrukció befejezésekor; implicit-lifetime típusnál implicit létrehozással is
Nyers tárolóban objektum nincs „objektum-létrehozás”; az allokált tároló effective type-ját az első nem karaktertípusú írás vagy a memcpy forrása adja implicit-lifetime típusnál a malloc, operator new, std::allocator::allocate, memcpy/memmove, unsigned char/std::byte tömb implicit létrehozza az objektumot; más típushoz konstrukció kell (placement new, construct_at)
Alias-szabály effective type és strict aliasing; karaktertípuson át minden olvasható dinamikus típus és strict aliasing ([basic.lval]); char, unsigned char, std::byte kivétel; átlátszó helyettesítés híján std::launder
Nem inicializált érték indeterminate; olvasása UB, ha csapdareprezentáció lehet, vagy ha a címét soha nem vett automatikus változót olvassuk; unsigned char-ként olvasható default inicializált automatikus változó értéke indeterminate; olvasása UB, kivéve az unsigned char/std::byte egyes másolási eseteit (C++26: erroneous behaviour)
Kijelölt inicializálás C99 óta: tetszőleges sorrend, beágyazott (.a.b) és tömbdesignator ([3] =), keverhető pozícionálissal, felülírás megengedett C++20 óta: csak aggregate-nél, csak közvetlen tagra, deklarációs sorrendben, nem keverhető pozícionálissal, nincs tömb- és beágyazott designator
Lista-inicializálás {} csak aggregate-re és skalárra; szűkítés megengedett {} minden típusra; szűkítés tiltott; std::initializer_list konstruktor elsőbbsége

A gyakorlati következtetés: a C++ kódban a „nyers memória + cast” minta nem átvihető gondolkodás nélkül C-ből, mert ott az objektum élettartama nem a byte-ok létezéséhez, hanem a konstrukcióhoz kötődik — kivéve az implicit-lifetime típusokat, amelyeket a malloc/memcpy jellegű műveletek C++20-ban implicit módon létrehozhatnak (lásd Tároló újrafelhasználása és C++ objektumszabályok (20.6)). Erről bővebben a C és C++: azonos szintaxis, eltérő szemantika szól.

46.5.1.4 Jelölésjegyzék: milyen bizonyítékot fogadunk el

A könyv következetesen megkülönbözteti az állítás forrásának típusát. A jelölés a szövegben az állítás mellett áll, nem helyettesíti a hivatkozást, és nem garantálja, hogy az adott verzió a te laborodban elérhető.

Címke Mit jelent Hol ellenőrizd
ISO-C kiadott C szabvány nyelvi szabálya a rögzített szabványverzió szövege
ISO-C++ kiadott C++ szabvány nyelvi szabálya a rögzített szabványverzió szövege
DRAFT nyilvános munkatervezet, nem kiadott szabvány a tervezet státusza és dátuma
POSIX POSIX specifikációs követelmény a vonatkozó specifikációs fejezet
LINUX Linux-specifikus viselkedés kernel/libc dokumentáció
ABI bináris felület, layout, hívási konvenció a cél ABI specifikációja
COMPILER fordító vagy könyvtár dokumentált viselkedése a rögzített kiadás dokumentációja
GUIDELINE ajánlás, nem normatív szabály az ajánlás szövege
MEASUREMENT konkrét mérés eredménye a mérés leírása és környezete
ILLUSTRATION szemléltető érték, például cím nem állítás, nem bizonyíték

A C++23 és C++26 állapotára vonatkozó kijelentések ezért mindig DRAFT vagy COMPILER címkét kapnak, és a támogatás verziőfüggő; erről a C++23 összefoglaló és migrációs táblázat és a C++26 és későbbi irányok ellenőrzött státusszal ad eligazítást. Ez a fejezet nem garantál semmilyen jövőbeli támogatást.

A hiba helyét a tünet alapján érdemes szűkíteni, mert a rossz szinten keresett hiba időt rabol. Az útvonal sorrendje nem esztétikai kérdés: a korábbi fázis hibája elfedi a későbbit.

  1. Preprocesszor: hiányzó include, makróhelyettesítés, feltételes fordítás. Lásd Headerek, deklarációk és include-higiénia és Makrók, feltételes fordítás és veszélyes helyettesítés.
  2. Fordítás: típus, deklaráció, konverzió. Lásd Deklarációk, typedef és típusminősítők.
  3. Linkelés: undefined reference esetén deklaráció/definíció, extern "C", szimbólumláthatóság. Lásd Szimbólumok, linkage és relocation és extern C és két nyelvű header.
  4. Betöltés: nem található megosztott könyvtár, eltérő SONAME, rpath. Lásd ELF-betöltés, PLT/GOT és dinamikus szimbólumok.
  5. Indulás és leállás: statikus inicializálás sorrendje, atexit, nem lokális vezérlés. Lásd Statikus és thread-local objektumok életciklusa.
  6. Futásidejű helyesség: UB, adatverseny, élettartam-hiba. Lásd ASan, UBSan, MSan, TSan és Valgrind és Konkurens élettartam és nehéz hibák.
  7. Erőforrás- és teljesítményprofil: szivárgás, allokációs minta, költség. Lásd Allokációs hiba-injektálás és leak diagnosztika és Profilozás, benchmark és cost model.

Az útvonal ellenőrzési szabálya: minden lépésnél írd le, mit vársz és mit figyeltél meg, és a kettő eltérését ne ugorásként kezeld. A „buildel, de összeomlik” tünet például nem a 3. lépésnél kezdődik, hanem a 6.-nál, viszont a 3. lépés figyelmeztetései gyakran a 6. okát jelzik előre.

46.5.1.6 Cél–feladat–bizonyíték lefedettségi mátrix

A mátrix nem haladási napló, hanem annak ellenőrzése, hogy egy témacsoportot valóban lefedtél-e: van-e hozzá megoldott feladat és olyan bizonyíték, amelyet más is reprodukálhat. A lefedettség aránya: lefedettség=#{igazolt cél}#{cél}.\text{lefedettség} = \frac{\#\{\text{igazolt cél}\}}{\#\{\text{cél}\}}. Az arány önmagában félrevezető: a nevezőt és a bizonyíték típusát is rögzíteni kell, különben a mutató tartalom nélkül telítődik.

Témacsoport Cél Feladat típusa Bizonyíték
Tulajdonlás és élettartam owner/borrower API-t indokolni kódolási szerződéslista + sanitizer futás naplója
Invalidáció view érvényességét megőrizni diagnosztikai reprodukált hiba és javított változat
C/C++ szemantika eltérést megnevezni magyarázó kétnyelvű példa és indoklás
Build és link hibát fázishoz kötni diagnosztikai parancs, hibaüzenet, ok
Teljesítmény költséget mérni mérési mérési környezet és nyers adat

A mátrix használatakor a bizonyíték oszlopba ne „megértettem” kerüljön, hanem visszakereshető nyom: parancs, kimenet, hibaüzenet vagy indoklás. A Reprodukálható fordítási és diagnosztikai labor és a Release, debug symbol és reprodukálhatóság adja az ehhez szükséges környezeti fegyelmet.

46.5.2 Feladatok

  1. Diagnosztikai útvonal. Egy C program a build után azonnal Segmentation fault-tal áll le, a -Wall -Wextra nem jelez semmit, a linker hibátlan. Írd le, melyik diagnosztikai lépésekből melyiket választod, milyen sorrendben, és melyik megfigyelés zárná ki az adott szintet. Add meg a használt kapcsolókat is.
  2. API-szerződés javítása. Adott egy C API, amely const char *name paramétert kap és a mutatót egy hosszú élettartamú struktúrában eltárolja. Írd át a szerződést és a kódot úgy, hogy az owner/borrower döntési tábla szerinti legyen, és adj ellenőrzési tervet arra az esetre, amikor a hívó ideiglenes buffert ad át.
  3. Lefedettségi terv. Válassz egy témacsoportot a mátrixból, és készíts hozzá egy cél–feladat–bizonyíték sort úgy, hogy a bizonyíték más által is reprodukálható legyen. Jelöld meg, melyik állításod ISO-C, ISO-C++, ABI vagy MEASUREMENT címkét kapna.

46.5.3 Megoldások

1. megoldás. A tünet alapján a futásidejű szint a valószínű, de a sorrendet nem szabad megfordítani. Először ellenőrizd, hogy a lefordított program a várt forrásból származik-e (friss build, nem elavult bináris), majd futtasd címzett diagnosztikával: -fsanitize=address,undefined és debug információ. Ha az ASan pontos helyet ad, a hiba a 6. szintre tartozik; ha nem szólal meg, de a program összeomlik, gyanús a nem definiált viselkedés olyan formája, amelyet a sanitizer nem fog el, vagy a linkelt könyvtár eltérő. Az indoklás: az „nincs figyelmeztetés” nem bizonyíték a helyességre, csak annyit jelent, hogy a fordító nem talált diagnosztizálható mintát. Ellenőrzési feltétel: a hibaüzenet és a javított futás összevethető, és a javítás okát egy mondatban meg tudod nevezni.

2. megoldás. A hiba az, hogy a függvény tulajdonosi felelősséget vállal egy olyan pointerért, amelyet csak kölcsönkapott. A javítás két helyes irány egyike: vagy a függvény másolatot készít (strdup-szerű, dokumentált free kötelezettséggel), vagy a hívó adja át a tulajdont egyértelmű típusban. C-ben az első a szokásos; a szerződést a headerben kell rögzíteni, és a hibakimenetet is meg kell nevezni. Ha a hívó ideiglenes buffert ad, a másolat elkészítése után a buffer felszabadítható; ezért az ellenőrzési terv: hozz létre egy lokális buffert, add át, majd azonnal írd felül, és igazold, hogy a tárolt érték változatlan. Ez a teszt a tulajdonlást ellenőrzi, nem a memóriát. Kapcsolódó részek: Opaque handle és stabil C könyvtári interfész, C API-szerződések ellenőrzése.

3. megoldás. Például az invalidáció témacsoportnál a cél: „egy view érvényességi határát meg tudom nevezni”. A feladat: készíts olyan programot, amelyben egy string_view túléli az owner módosítását, figyeld meg a következményt, majd javítsd a kódot owner-másolattal vagy a view élettartamának szűkítésével. A bizonyíték: a hiba előtti és utáni kimenet, a sanitizer jelentése és a javítás indoklása; a mérési környezet rögzítése. A címkék: a string_view invalidációs szabálya ISO-C++, a sanitizer viselkedése COMPILER, a konkrét kimenet MEASUREMENT. Az ellenőrzési feltétel, hogy a bizonyítékból kiderüljön, melyik állítás melyik forrásra támaszkodik, és hogy a javított változat nem függ a régi buffer tartalmától.

46.5.4 Összefoglaló

Ez a fejezet a kurzus anyagát keresőfelületté alakítja: a döntési táblák a tulajdonlás és az API-szerződés kérdéseit fordítják választássá, az invalidációs index a hozzáférések érvényességi határait rögzíti, a C/C++ táblázat a két nyelv eltérését idézi fel, a jelölésjegyzék pedig azt szabályozza, milyen bizonyítékot fogadunk el egy állítás mellett. A diagnosztikai útvonal a tünettől a fázisig vezet, a lefedettségi mátrix pedig a tanulást teszi ellenőrizhetővé: nem elég egy témát elolvasni, kell hozzá megoldott feladat és reprodukálható bizonyíték. A táblázatok nem helyettesítik a korábbi részek feltételeit; minden sor mögött ott van a hivatkozott rész, amelyet döntés előtt érdemes újraolvasni.