4.2 Promóciók, signedness és túlcsordulás

Egy hálózati hosszmező, egy előjeles hibajelzés és egy memóriaméret gyakran ugyanabban a kifejezésben találkozik. Matematikailag mindegyik egész szám, C-ben azonban a típusuk dönti el, hogyan történik az összehasonlítás, a kivonás vagy a szorzás. A hibás konverzió következménye lehet elutasított érvényes index, óriásira változó negatív hossz vagy túl kicsire számított allokáció.

Az Egész típusok és adatmodellek alapján már különválasztjuk a típus nevét, tartományát és reprezentációját. Most a kifejezés típusát követjük: ezt általában nem az eredményt fogadó változó, hanem az operátor és az operandusok típusa határozza meg.

Az alábbi nyelvi szabályok az ISO C17-re vonatkoznak. A konkrét számpéldák laborprofilja Linux, x86-64 System V ABI, LP64, CHAR_BIT == 8; itt az int 32 bites, a long 64 bites, a size_t pedig unsigned long. A használt exact-width típusok rendelkezésre állását külön feltételezzük. A példák eredményei levezetett elvárások, nem dokumentált futtatási eredmények.

4.2.1 Az implicit konverziók két lépcsője

4.2.1.1 Egész promóciók

Az egész promóciók (integer promotions) a kis rangú egész típusokat emelik int vagy unsigned int típusra. A rang (integer conversion rank) a nyelv által meghatározott típusviszony; nem egyszerűen a tárolási méret másik neve.

A szokásos kis egész típusokra, például _Bool, char, signed char, unsigned char, short és unsigned short esetén:

Nem az aktuális érték számít. Egy unsigned short akkor is ugyanarra a típusra promótálódik, ha éppen nulla van benne. Az enumerációk és bitmezők is érintettek lehetnek, saját típusszabályaik szerint.

C17-részlet a megadott laborprofilra:

uint16_t received = 4;
uint16_t header = 8;
int payload = received - header;

A levezetés:

  1. A uint16_t értéktartománya [0,65535][0,65535].
  2. A profil 32 bites int típusa ezt teljes egészében ábrázolja.
  3. Mindkét operandus int típusra promótálódik.
  4. A kivonás ezért int aritmetikával történik: az eredmény -4.

Az unsigned változók tehát nem garantálják, hogy az aritmetikai művelet is unsigned lesz.

4.2.1.2 Szokásos aritmetikai konverziók

Az olyan bináris operátoroknál, mint a +, -, *, / és az összehasonlítások, az egész promóciókat a szokásos aritmetikai konverziók (usual arithmetic conversions) követik.

Két egész operandusra, a promóciók után:

  1. Azonos típus esetén nincs további konverzió.
  2. Azonos előjelesség esetén a kisebb rangú operandus a nagyobb rangú típusra konvertálódik.
  3. Vegyes signed/unsigned esetben legyen az előjeles típus SS, az előjel nélküli UU:

A laborprofilon:

Operandustípusok Közös típus Indok
int, unsigned int unsigned int Az unsigned rang nem kisebb
long, unsigned int long A 64 bites long minden 32 bites unsigned értéket ábrázol
int, size_t unsigned long A size_t unsigned, rangja nagyobb

Összehasonlításnál ez a vizsgált operandusok közös típusa; az összehasonlítás eredménye C-ben int, értéke nulla vagy egy.

Ezek a szabályok nem minden operátorra alkalmazhatók változtatás nélkül. Az értékadás például a kiszámított értéket a bal oldal típusára konvertálja; a léptetőoperátorok külön szabályait a Kifejezések kiértékelése, sequencing és bitműveletek tárgyalja.

4.2.2 Negatív index unsigned hossz mellett

C17-részlet a laborprofilra:

int index = -1;
size_t length = 100;

if (index < length) {
    /* Nem ez az ág következik a típuskonverziókból. */
}

A közös típus size_t, vagyis unsigned long. Az egész érték unsigned típusra konvertálásakor az eredmény a cél értéktartományához tartozó moduló szerinti érték.

Ha a cél maximuma MM, akkor:

−1≡M(modM+1). -1 \equiv M \pmod{M+1}.

Az összehasonlítás így a profilon SIZE_MAX < 100, tehát hamis. A matematikailag kisebb negatív index a konverzió miatt nagy pozitív értékként szerepel.

A helyes ellenőrzés előbb a jelentést, utána a reprezentációt kezeli. Hordozható C17-részlet, <stdint.h> használatával:

if (index >= 0 &&
    (uintmax_t)index < (uintmax_t)length) {
    /* Az index a [0, length) tartományban van. */
}

A && rövidzáras: negatív indexnél a jobb oldal nem értékelődik ki. A uintmax_t minden unsigned egész típus értékeit képes ábrázolni; a nemnegatív int érték is belefér.

A megszokott (size_t)index < length változat ugyancsak megfelelő, ha előtte ellenőrizzük a nemnegativitást, és az alkalmazott profilban minden nemnegatív int belefér a size_t típusba. Ez a választott LP64 profilban teljesül, de nem önálló, minden C-implementációra érvényes axióma.

4.2.3 Moduláris aritmetika, UB és szűkítés

Ha a művelet eredménytípusa NN értékbites unsigned egész típus, az összeadás, kivonás és szorzás eredménye modulo 2N2^N értendő:

r=(a∘b)mod2N,∘∈{+,−,×}. r=(a\mathbin{\circ}b)\bmod 2^N, \qquad \circ\in\{+,-,\times\}.

Ez definiált körbefordulás (wraparound), de méretszámításban rendszerint programhiba. Egy körbefordult kéréshez túl kevés memória foglalható.

Ezzel szemben, ha egy előjeles aritmetikai eredmény nem ábrázolható az eredménytípusban, az nem definiált viselkedés, UB. Az INT_MAX + 1 nem hordozható módja az INT_MIN előállításának. Az optimalizáló nem köteles a processzor esetleges körbefordulását megőrizni.

Fontos különbség az aritmetika és az utólagos konverzió között:

/* C17-részlet; uint8_t létezik. */
uint8_t a = 250;
uint8_t b = 10;
uint8_t result = a + b;

Itt az operandusok int típusra promótálódnak. A 260 eredmény ábrázolható int-ben, tehát nincs aritmetikai túlcsordulás. Ezután az értékadás uint8_t típusra konvertál: 260mod256=4260\bmod256=4.

Szűkítésnek (narrowing conversion) itt az olyan konverziót nevezzük, amelynél a cél nem képes a forrástípus minden értékét ábrázolni. C17-ben:

A signed célra történő konverzió tehát nem ugyanaz a szabálykategória, mint a signed aritmetikai túlcsordulás.

Az explicit cast konverziós kérés, nem tartományellenőrzés. C17-részlet:

if (wire_length <= UINT16_MAX) {
    uint16_t short_length = (uint16_t)wire_length;
    /* short_length itt veszteségmentesen előállított érték. */
}

Feltétel: wire_length típusa uint32_t, és mindkét exact-width típus elérhető. Signed forrásnál az alsó határt is ellenőrizni kell.

Különösen veszélyes minta:

/* Hibás lehet: a cast túl későn történik. */
size_t bytes = (size_t)(count * stride);

Ha count és stride típusa int, előbb int szorzás történik. A későbbi cast nem javítja meg a korábbi UB-t.

4.2.4 Méretszámítás allokáció előtt

Legyen a,ba,b két size_t érték, M=𝚂𝙸𝚉𝙴_𝙼𝙰𝚇M=\texttt{SIZE\_MAX}.

Az összeadás pontosan akkor fér el, ha:

a+b≤M⇔a≤M−b. a+b\le M \iff a\le M-b.

A szorzás pedig akkor fér el, ha:

b=0vagya≤⌊Mb⌋. b=0 \quad\text{vagy}\quad a\le\left\lfloor\frac{M}{b}\right\rfloor.

Bizonyításvázlat. Mivel 0≤b≤M0\le b\le M, az M−bM-b kivonás biztonságos. Pozitív bb mellett az ab≤Mab\le M egyenlőtlenség osztható bb-vel; egész aa esetén a lefelé kerekített hányados adja a pontos korlátot. A nulla szorzót külön kezeljük, így nincs nullával osztás.

A következő teljes C17-program egy már dekódolt hálózati elemszámból számít méretet. Nem végez allokációt: annak külön hibaszerződése a malloc, calloc, free és allokációs szerződés témája. A 24 és 16 itt alkalmazásszerződésből származó, C bájtban megadott méret, nem struktúralayout.

#include <stdint.h>
#include <inttypes.h>
#include <stdio.h>

#ifndef UINT32_MAX
#error "This example requires uint32_t."
#endif

int main(void)
{
    uint32_t wire_count = 0;

    /* Kizárólag ismert, ábrázolható tesztérték beolvasása. */
    if (sscanf("42", "%" SCNu32, &wire_count) != 1) {
        fputs("conversion failed\n", stderr);
        return 1;
    }

    if ((uintmax_t)wire_count > (uintmax_t)SIZE_MAX) {
        fputs("count does not fit size_t\n", stderr);
        return 1;
    }

    const size_t count = (size_t)wire_count;
    const size_t stride = 24;
    const size_t header = 16;

    if (stride != 0 && count > SIZE_MAX / stride) {
        fputs("payload size overflow\n", stderr);
        return 1;
    }
    const size_t payload = count * stride;

    if (payload > SIZE_MAX - header) {
        fputs("total size overflow\n", stderr);
        return 1;
    }
    const size_t total = header + payload;

    printf("count=%" PRIu32 " total=%zu\n", wire_count, total);
    return 0;
}

Az sscanf nem tulajdonló hozzáférést kap a wire_count helyi objektumhoz, és a címet nem tárolja el. Az objektum tárolója és élettartama a main blokkjához kötött; a hívás alatt nem érvénytelenedik. A bemeneti sztringliterál statikus tárolási idejű. A program saját felismert hibakimenete üzenet és nem nulla kilépési státusz.

Választott, itt nem ellenőrzött labor-eszközlánc: GCC 14.2.0 vagy Clang 19.1.0, glibc 2.40, a fent megadott Linux-targettel. Fordítás külön laborlépésként:

gcc -std=c17 -O2 -Wall -Wextra -Wpedantic -Wconversion -Wsign-conversion sizes.c -o sizes-gcc
clang -std=c17 -O2 -Wall -Wextra -Wpedantic -Wconversion -Wsign-conversion sizes.c -o sizes-clang

A számtani levezetés szerint 42⋅24+16=102442\cdot24+16=1024. Ez várt eredmény, nem mérés. Az aritmetikai ellenőrzés mellett valódi szolgáltatásban külön erőforráskorlát is szükséges: a reprezentálható méret nem feltétlenül elfogadható méret.

4.2.5 Mérethelyes formátumok

A variadikus printf nem következteti ki az argumentum típusát a formátumszövegből. A formátum és az átadott típus össze nem illése UB-t okozhat.

Értéktípus Kiírás Beolvasás
size_t %zu %zu, size_t * argumentummal
ptrdiff_t %td %td, ptrdiff_t * argumentummal
uint32_t "%" PRIu32 "%" SCNu32, uint32_t * argumentummal
int64_t "%" PRId64 "%" SCNd64, int64_t * argumentummal

A PRI és SCN makrókat az <inttypes.h> biztosítja; az exact-width változatok az adott típus elérhetőségéhez kötődnek. A kiírási és beolvasási makrók nem felcserélhetők.

A SCN makró csak a típushelyes formátumot adja meg. C17-ben a célobjektumban nem ábrázolható numerikus scanf-eredmény UB lehet; a visszatérési érték ellenőrzése önmagában nem teszi biztonságossá a tetszőleges bemenetet. Ezért használ a példa ismert literált. Külső numerikus szöveghez célszerű például strtoumax-alapú feldolgozás, külön előjel-, tartomány-, végpozíció- és hibavizsgálattal; ennek részleteit a Formázás és megbízható szövegfeldolgozás tárgyalja.

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

4.2.7 Feladatok

4.2.7.1 Magyarázat: kivonás és értékadás

A laborprofilon a és b típusa uint16_t, értékük 3 és 5. Mi a különbség az int x = a - b; és a size_t y = a - b; között? Nevezd meg a kivonás és az értékadás konverzióit.

4.2.7.2 Kódolás: alkalmazási méretkorlát

Írj frame_fits(count, stride, header, limit) függvényt size_t paraméterekkel, amely túlcsordulás nélkül eldönti:

header+count⋅stride≤limit. header+count\cdot stride\le limit.

A nulla stride is legyen megengedett.

4.2.7.3 Diagnosztika: késői cast

A laborprofilon elemezd ezt az elkülönített, hibás C17-részletet:

int count = 100000000;
int stride = 24;
size_t bytes = (size_t)(count * stride);
printf("%u\n", bytes);

Keress két egymástól független hibát. UB esetére ne jósolj kimenetet.

4.2.8 Megoldások

4.2.8.1 A kivonás eredménye

Mindkét operandus int típusra promótálódik, ezért a kivonás eredménye int típusú -2. Az x értéke -2; a y inicializálásakor külön unsigned konverzió történik:

−2mod(𝚂𝙸𝚉𝙴_𝙼𝙰𝚇+1)=𝚂𝙸𝚉𝙴_𝙼𝙰𝚇−1. -2\bmod(\texttt{SIZE\_MAX}+1)=\texttt{SIZE\_MAX}-1.

Ellenőrzési feltétel: a levezetés külön kezelje az operátor és a célváltozó típusát.

4.2.8.2 Korlátvizsgáló függvény

C17-függvényrészlet; önálló programhoz hívó és main szükséges:

#include <stdbool.h>
#include <stddef.h>

bool frame_fits(size_t count, size_t stride,
                size_t header, size_t limit)
{
    if (header > limit) {
        return false;
    }

    return stride == 0 ||
           count <= (limit - header) / stride;
}

A kivonást az első ellenőrzés védi, az osztást a rövidzáras ||. Szorzást egyáltalán nem végzünk.

Ellenőrzési esetek: (42,24,16,1024) igaz, (43,24,16,1024) hamis; nulla stride mellett kizárólag a fejléc mérete számít.

4.2.8.3 A két független hiba

A szorzás int típusban történik. A matematikai eredmény 24000000002\,400\,000\,000, amely a profil INT_MAX értékénél nagyobb: ez UB, még a cast előtt.

Emellett %u helyett %zu szükséges a size_t kiírásához. A javítás sorrendje: előjel- és tartományvizsgálat, konverzió size_t típusra, szorzási korlátvizsgálat, majd szorzás és típushelyes kiírás. A warning hasznos bizonyíték lehet egy hibára, de hiánya nem bizonyítja a helyességet.

4.2.9 Összefoglaló

Egész kifejezéseknél először a promóciókat, majd a közös típust, végül az értékadás konverzióját vizsgáld. Az unsigned körbefordulás definiált, de méretszámításban veszélyes; a signed aritmetikai túlcsordulás UB. A cast nem ellenőrzés. Negatív értékeket unsigned konverzió előtt, összeadást és szorzást pedig a művelet végrehajtása előtt kell tartományvizsgálattal védeni.