4.5 Kifejezések kiértékelése, sequencing és bitműveletek

Egy C-kifejezés helyességét nem dönti el, hogy a megfelelő operátorokat használtuk-e. Tudnunk kell azt is, melyik operandus milyen típusúvá alakul, mikor történnek meg a módosítások, és milyen sorrendet garantál a nyelv. Ezek a kérdések egy állapotjelző bitmező vagy egy számláló kezelésekor ugyanúgy előkerülnek, mint egy összetett függvényhívásban.

A Promóciók, signedness és túlcsordulás részben megismert konverziós szabályokra, valamint a Deklarációk, typedef és típusminősítők fogalmaira építünk. A nyelvi szabályok itt ISO C17-re vonatkoznak; a hasonló C++-szintaxisra nem szabad automatikusan átvinni őket.

4.5.1 Értékszámítás, mellékhatás és sequencing

Egy kifejezés kiértékelése kétféle tevékenységet tartalmazhat:

Az i++ értéke például i régi értéke; külön mellékhatása i növelése. A régi érték kiszámítása megelőzi ezt a módosítást, de ebből még nem következik sorrend a környező operandusokhoz képest.

A sequenced-before az egy szálon belüli kiértékelések aszimmetrikus, tranzitív sorrendi relációja. Ha AA sequenced-before BB, akkor AA kiértékelése megelőzi BB kiértékelését. Két további esetet különböztetünk meg:

Viszony Jelentés
Unsequenced Sem AA nem előzi meg garantáltan BB-t, sem fordítva.
Indeterminately sequenced Az egyik teljes kiértékelés megelőzi a másikat, de a nyelv nem rögzíti, melyik.

Az utóbbi tehát nem ugyanaz, mint a sorrendi kapcsolat hiánya.

ISO C17 – alapvető UB-szabály: ha egy skalár objektum módosítása unsequenced viszonyban áll ugyanannak az objektumnak egy másik módosításával vagy az értékét felhasználó értékszámítással, a viselkedés nem definiált.

Az operátorprecedencia csak a kifejezés csoportosítását határozza meg. Az a() + b() * c() alakból nem következik, hogy először b() fut le.

4.5.1.1 Függvényargumentumok és teljes kifejezések

Az alábbiak elkülönített tanítási részletek, nem futtatandó tesztek. Tegyük fel, hogy i és j különálló int objektumok, és nincs túlcsordulás.

Kifejezés C17 szerinti minősítés
i++ + j++ Definiált; az operandusok között nincs garantált sorrend.
i++ + i UB: a módosítás és az olvasás unsequenced.
consume(i++, i++) UB: ugyanaz az objektum két unsequenced módosítást kap.
consume(i++, i) UB: unsequenced módosítás és olvasás.
i = i++ + 1 UB C17-ben: a két módosítás között nincs szükséges sorrend.

A függvényargumentumok kiértékelése befejeződik a meghívott függvény törzsének megkezdése előtt, de ettől az argumentumok egymás közötti sorrendje még nincs rögzítve.

Biztonságos javítás, C17-részletként, egy megfelelően deklarált consume mellett:

int i = 3;
int first = i++;
int second = i;
consume(first, second);

A külön inicializálók teljes kifejezések (full expressions). Az előző teljes kifejezés értékszámításai és mellékhatásai megelőzik a következőét. Így a hívás argumentumértékei 3 és 4.

Fontos finomság: a consume(next(), next()) nem szükségképpen UB akkor sem, ha a két next() ugyanazt a számlálót módosítja. A két függvénytörzs végrehajtása egymáshoz képest indeterminately sequenced: nem fonódnak össze. Az argumentumokhoz rendelt értékek sorrendje ettől még nem specifikált. Feltétel természetesen, hogy a függvények önmagukban helyesek legyenek.

4.5.2 A rövidzár és a vesszőoperátor garanciái

ISO C17: a következő operátorok valódi sorrendi garanciát adnak.

Operátor Garancia
left && right left előbb értékelődik ki; right csak igaz bal operandus esetén.
left || right left előbb értékelődik ki; right csak hamis bal operandus esetén.
cond ? yes : no A feltétel megelőzi a kiválasztott ágat; csak az egyik ág értékelődik ki.
left, right A bal operandus kiértékelése és mellékhatásai megelőzik a jobbét.

A && és || eredménye int típusú, értéke 0 vagy 1.

Például unsigned int típusú num és den mellett:

/* C17-részlet; num és den már inicializált unsigned int objektumok. */
int large_ratio = den != 0u && num / den > 3u;

A rövidzár kizárja a nullával osztást. A & nem helyettesítheti a && operátort: nincs rövidzáras garanciája.

A vessző két szerepét külön kell választani:

/* C17-részlet; consume megfelelően deklarált függvény. */
int i = 3;
int result = (i++, i * 2);  /* Vesszőoperátor: result értéke 8. */
consume(i, result);        /* Az argumentumelválasztó vessző nem operátor. */

A sorrendet itt a vesszőoperátor biztosítja, nem önmagában a zárójel.

4.5.3 Bitműveletek: előbb a típust, aztán a biteket vizsgáld

A &, |, ^ és ~ bitenkénti műveleteket végeznek. Az egész promóciók ezekre is vonatkoznak: például egy unsigned char operandus ~ művelete gyakran int típusban történik, nem az eredeti keskeny típusban.

A shiftoperátoroknál mindkét operandus egész promóción megy át, de nem a szokásos aritmetikai konverziók határozzák meg a közös típust. Az eredmény típusa a promótált bal operandus típusa.

Legyen WW ennek bitbeli szélessége, előjeles típusnál az előjelbitet is beleszámítva, padding nélkül. Mindkét shift előfeltétele:

0≤k<W, 0 \le k < W,

ahol kk a jobb operandus értéke. Negatív vagy túl nagy eltolás UB.

ISO C17:

Egy 32 bites int/unsigned int profilban ezért a 1 << 31 és a 1u << 32 egyaránt UB, de más okból. Az első eredménye nem reprezentálható int-ként, a második eltolásszáma túl nagy.

A művelet utáni cast nem javítja meg a hibás shiftet:

/* HIBÁS C17-részlet, 32 bites unsigned int profilban: UB. */
uintmax_t bit = (uintmax_t)(1u << 40);

A megfelelően széles típust a shift előtt kell biztosítani.

4.5.3.1 Biztonságos maszk eltérő szélességekre

Az alsó nn bitet kijelölő maszk matematikai értéke 2n−12^n-1. A közvetlen (1 << n) - 1 azonban a teljes típusszélességnél hibás.

Használjuk inkább, n>0n>0 esetén:

t=2n−1,Mn=t|(t−1). t=2^{n-1},\qquad M_n=t\mathbin{|}(t-1).

Bizonyításvázlat: tt egyetlen beállított bitet tartalmaz az n−1n-1 pozíción; t−1t-1 az összes alatta levő bitet tartalmazza. Bitenkénti VAGY kapcsolatuk pontosan nn darab egyes.

A következő teljes program minimum C17-et igényel. A width logikai szélesség bitben, nem sizeof-érték. A támogatott tartomány 1–64 bit; a uintmax_t legalább 64 értékbitje elegendő. Nincs szükség opcionális uint64_t típusra.

/* masks.c */
#include <stdbool.h>
#include <stdint.h>
#include <inttypes.h>
#include <stdio.h>

#define MAX_FIELD_BITS 64u

_Static_assert(
    UINTMAX_MAX >= UINTMAX_C(0xffffffffffffffff),
    "at least 64 value bits are required"
);

struct mask_result {
    bool ok;
    uintmax_t value;
};

static struct mask_result low_mask(unsigned width, unsigned count)
{
    if (width == 0u || width > MAX_FIELD_BITS || count > width) {
        return (struct mask_result){false, UINTMAX_C(0)};
    }

    if (count == 0u) {
        return (struct mask_result){true, UINTMAX_C(0)};
    }

    uintmax_t top = UINTMAX_C(1) << (count - 1u);
    return (struct mask_result){true, top | (top - UINTMAX_C(1))};
}

int main(void)
{
    struct mask_result small = low_mask(8u, 4u);
    struct mask_result full = low_mask(64u, 64u);

    if (!small.ok || !full.ok) {
        return 1;
    }

    if (printf("%" PRIxMAX "\n%" PRIxMAX "\n",
               small.value, full.value) < 0) {
        return 2;
    }

    return 0;
}

A hibás kérés ok == false, value == 0 eredményt ad; a nulla hosszúságú maszk viszont sikeres kérés. A legnagyobb shift 63, ezért nincs szélességnyi eltolás.

A fordítási parancsokhoz választott laborprofil: Linux, x86-64 System V ABI, LP64, GCC 14.2.0 vagy Clang 19.1.7, glibc 2.40. A program nem támaszkodik e profil konkrét típusszélességeire. A parancsok feltételezik, hogy a megadott kiadások érhetők el az adott neveken; itt nem történt fordítás vagy futtatás.

gcc -std=c17 -Wall -Wextra -Wpedantic -Wconversion -O2 masks.c -o masks-gcc
clang -std=c17 -Wall -Wextra -Wpedantic -Wconversion -O2 masks.c -o masks-clang

A szabályokból levezetett, nem mért kimenet: f, majd ffffffffffffffff.

Maszkellenőrzésnél mindig ügyelj a precedenciára: a helyes alak (flags & mask) == 0. A flags & mask == 0 mást jelent, mert az egyenlőségvizsgálat köt erősebben.

4.5.4 sizeof, VLA és konstans kifejezések

ISO C17: a sizeof eredménye size_t, mértékegysége C bájt. Nem VLA típusú operandusa nem értékelődik ki: a sizeof(i++) ezért nem növeli i értékét, ha i például int.

A változó hosszúságú tömb, azaz variable length array (VLA) kivétel. Az alábbi C17-részlet VLA-támogatást igényel; C17-ben ez opcionális képesség:

#include <stddef.h>

void example(void)
{
    size_t n = 3;
    size_t bytes = sizeof(int[n++]);
    /* n értéke 4; bytes értéke 3 * sizeof(int). */
    (void)bytes;
}

Itt a méret meghatározásához ki kell értékelni a tömbhatárt. Ezzel szemben egy korábban létrehozott int a[n++] objektum sizeof a művelete nem értékeli újra a deklarációbeli n++ kifejezést: a tömb mérete a létrehozáskor rögzült. Egyes összetett, változó méretű típusokat tartalmazó esetekben a méretkifejezés kiértékelése el is maradhat, ha nem befolyásolná a sizeof eredményét. Méretkifejezésbe ezért ne rejts mellékhatást.

Az egész konstans kifejezés (integer constant expression, ICE) egész típusú, szigorúan korlátozott összetételű konstans kifejezés. Alkotóelemei lehetnek például egész-, karakter- és felsorolási konstansok, megfelelő sizeof és _Alignof kifejezések; közvetlen egészre castolt lebegőpontos konstans is megengedett. Kiértékelt függvényhívás vagy objektummódosítás nem lehet benne.

C17-ben egy const int objektum neve önmagában nem ICE, még akkor sem, ha az optimalizáló ismeri az értékét.

/* C17-részlet, fájlszinten is elhelyezhető. */
#define FIELD_BITS 5u
#define FIELD_SHIFT 9u

_Static_assert(FIELD_SHIFT < 64u, "invalid shift");
_Static_assert(FIELD_BITS > 0u &&
               FIELD_BITS <= 64u - FIELD_SHIFT,
               "field does not fit");

A _Static_assert egész konstans kifejezést követel; nulla értéknél fordítási diagnosztikát ír elő. Futásidejű bemenetet nem ellenőriz. A makró pedig szöveges helyettesítés: nem attól lesz konstans kifejezés, hogy nagybetűs a neve.

4.5.5 Feladatok

4.5.5.1 Sorrend és mellékhatás

Mindegyik esetet külön, int i = 3, j = 8; kezdőállapotból vizsgáld:

Add meg a definiált esetek eredményét és végállapotát, illetve az UB okát.

4.5.5.2 Diagnosztikai feladat

Miért hibás C17-ben az alábbi részlet? A második sor vizsgálatához tegyél fel 32 bites unsigned int típust.

/* HIBÁS tanítási részlet. */
const int bits = 32;
_Static_assert(bits == 32, "unexpected width");
uintmax_t mask = (uintmax_t)(1u << bits) - 1u;

Javítsd külön a fordítási idejű ellenőrzést és a maszkképzést.

4.5.5.3 Ellenőrzött mezőkinyerés

Egészítsd ki a teljes programot egy extract_field(x, shift, count) függvénnyel. A 64 bites logikai szó alsó bitjeitől számozva nyerjen ki mezőt. Utasítsa el a shift >= 64 és count > 64 - shift eseteket, és használja a meglévő hibaszerződést.

4.5.6 Megoldások

4.5.6.1 Sorrend és mellékhatás

A (i++, i) eredménye 4, végül i == 4: a vesszőoperátor sorrendet biztosít.

Az i++ + j++ eredménye 11, végül i == 4, j == 9. Külön objektumok módosulnak; az operandusok sorrendje nem befolyásolja ezt az eredményt.

A consume(i++, i) UB: i módosítása és értékének felhasználása unsequenced. Nincs garantált kimenete. Ellenőrzéskor a sorrendi relációt kell igazolni, nem egy megfigyelt futásra támaszkodni.

4.5.6.2 Diagnosztikai feladat

A bits objektum neve nem egész konstans kifejezés. Használható helyette például:

/* C17-részlet. */
enum { BITS = 32 };
_Static_assert(BITS == 32, "unexpected width");

A shift ettől még hibás maradna: 32 bites operandust 32-vel tolna. A szélesítés túl későn történik. A javítás a korábbi programban:

struct mask_result mask = low_mask(32u, 32u);

Ellenőrzési feltétel: mask.ok igaz, értéke matematikailag 232−12^{32}-1.

4.5.6.3 Ellenőrzött mezőkinyerés

Az alábbi C17-részlet a programban, a low_mask után helyezhető el:

static struct mask_result extract_field(
    uintmax_t x, unsigned shift, unsigned count)
{
    if (shift >= MAX_FIELD_BITS) {
        return (struct mask_result){false, UINTMAX_C(0)};
    }

    struct mask_result mask =
        low_mask(MAX_FIELD_BITS - shift, count);

    if (!mask.ok) {
        return mask;
    }

    return (struct mask_result){true, (x >> shift) & mask.value};
}

Előbb ellenőrizzük a shiftet, és csak utána számítjuk ki a fennmaradó szélességet. A jobbra tolás így mindig megengedett; a maszk eltávolítja a mező fölötti biteket.

Ellenőrzési esetek: extract_field(0xb0, 4, 4) értéke 0xb; a (shift=63, count=1) kérés megengedett, a (63, 2) és (64, 0) elutasítandó. A nulla hosszúságú, egyébként érvényes mező eredménye nulla.

4.5.7 Összefoglaló

A precedencia a csoportosítást, a sequencing a kiértékelések sorrendjét szabályozza. A külön teljes kifejezések és a rövidzáras operátorok világos sorrendi garanciákat adnak; a függvényargumentumok elválasztó vesszői nem.

Bitműveletnél mindig a promótált típust, az előjelet és az eltolásszám tartományát vizsgáld. A sizeof többnyire nem értékeli ki operandusát, de a VLA kivételt jelent. A _Static_assert fordítási idejű szerződést ellenőriz; a futásidejű bemenetekhez külön ellenőrzés szükséges.