5.4 Effective type, aliasing és objektumreprezentáció

A memória olvashatósága nem pusztán cím- és lapvédelmi kérdés. Egy tartomány lehet a folyamat számára írható, megfelelő méretű és megfelelően igazított, miközben egy adott C típuson keresztüli hozzáférés mégis nem definiált viselkedést okoz. A fordító ugyanis nem csupán bájtokat lát: az objektumok típusára és a hozzáférések kapcsolatára is támaszkodik.

Az Inicializálás, indeterminate érték és reprezentáció rész alapján már különbséget teszünk érték és objektumreprezentáció között. A Deklarációk, typedef és típusminősítők pedig megadta a típusdeklarációk alapját. Most azt vizsgáljuk, milyen típusú kifejezéssel szabad egy objektum tárolt értékéhez hozzáférni.

A nyelvi szabályok alapja ebben a részben ISO C17. A konkrét gépi reprezentáció, illetve a fordítói optimalizálás ettől külön kezelendő.

5.4.1 Deklarált típus és effective type

Az effective type, azaz a hozzáférés megítéléséhez használt effektív típus nem azonos a pointer változó típusával. Egy cast megváltoztathatja egy pointerkifejezés típusát, de ettől a célobjektum típusa nem változik meg.

5.4.1.1 Deklarációval létrehozott objektumok

Ha az objektumnak van deklarált típusa, az effektív típusa a deklarált típusa.

/* ISO C17, szemléltető részlet. */
unsigned int counter = 12u;
unsigned char buffer[64];

A counter típusa unsigned int; a buffer karakterelemekből álló tömb. A tömb nem „típus nélküli nyers memória”, még akkor sem, ha kizárólag átmeneti tárolóként használjuk.

Ezért egy unsigned int reprezentációjának bemásolása a buffer elejére nem hoz létre ott deklarált unsigned int objektumot. A visszaolvasás helyes módszere egy megfelelő típusú célobjektumba végzett másolás, nem pedig a karaktertömb címének átalakítása és dereferálása. A megfelelő igazítás önmagában ezt nem javítaná ki.

5.4.1.2 Allokált tároló deklarált típus nélkül

A malloc által biztosított objektumnak nincs deklarált típusa. A C17 ezért a hozzáférés módjából határozza meg az effektív típust:

Az effektív típus tehát nem feltétlenül az allokáció teljes élettartamára szóló, megváltoztathatatlan címke. Másfelől a pointer átírása vagy castolása nem típusváltó művelet.

Tároló Van deklarált típusa? Mi határozza meg az effektív típust?
unsigned int x; Igen A deklaráció
unsigned char bytes[32]; Igen A deklaráció; másolástól sem lesz egész objektum
malloc eredménye Nincs A C17 hozzáférési és másolási szabályai
Deklarált objektumba végzett memcpy Igen A cél deklarált típusa változatlan

5.4.2 Lvalue-hozzáférés és strict aliasing

Az lvalue itt objektumot kijelölő kifejezés: például az x vagy a *p. Nem csak íráskor használunk lvalue-ot; a result = *p; kifejezésben a *p olvasás tárgya.

Aliasingról akkor beszélünk, amikor különböző hozzáférési utak ugyanazt az objektumot vagy egymást átfedő tárolót érik el. Ez önmagában nem hiba. Két int * teljesen szabályosan mutathat ugyanarra az int objektumra.

Az ISO C17 típuson alapuló hozzáférési szabálya, amelyet gyakran strict aliasing szabálynak neveznek, megengedi a tárolt érték elérését olyan lvalue-val, amelynek típusa:

Az összetett típusokra vonatkozó kivétel például lehetővé teszi, hogy a fordító számoljon egy struktúrára és annak egész típusú tagjára mutató pointer átfedésével. Nem jogosít fel arra, hogy egy önálló egész objektum köré képzeletbeli struktúrát olvassunk: a méret- és objektumhatárok továbbra is érvényesek.

Egy int objektum olvasása const int lvalue-val rendben van. A megfelelő unsigned int típus szintén külön megengedett eset. A float viszont nem válik kompatibilissé az int típussal attól, hogy egy platformon azonos méretűek.

A hozzáférés helyessége több feltétel együttes teljesülését követeli meg. Ha egy hozzáférés helyes, akkor mind az öt teljesül:

  1. a hozzáférés élő objektumot ér el;
  2. a hozzáférés az objektum határain belül marad;
  3. a cím megfelelően igazított;
  4. a hozzáférés lvalue-jának típusa megengedett (lásd a fenti listát);
  5. az olvasott érték érvényes reprezentáció.

Ez szükséges feltételek összefoglalása, nem teljes helyességi bizonyítás: például az adatverseny tilalmát külön is teljesíteni kell. Maga a pointerkonverzió is problémás lehet, ha az eredmény nem megfelelően igazított.

5.4.3 Karakteres hozzáférés és reprezentációmásolás

ISO C17: egy objektum reprezentációja karaktertípusú lvalue-on keresztül hozzáférhető. C-ben ide tartozik a char, a signed char és az unsigned char; reprezentáció vizsgálatához általában az utóbbi a legegyértelműbb.

Az objektumreprezentáció mérete sizeof object C bájt. Egy C bájt CHAR_BIT bitből áll; a nyelvi szabály nem feltételez nyolcbites oktetteket.

A kivétel iránya lényeges:

Egy egész objektum reprezentációját szabad karakterként vizsgálni; egy karaktertömböt ettől még nem szabad tetszőleges egész objektumként olvasni.

A karakteres írás sem veszélytelen. Egy reprezentáció bájtjainak önkényes módosítása létrehozhat olyan bitmintát, amely a későbbi típusos olvasáskor nem érvényes.

5.4.3.1 Teljes példa: pillanatkép és típusos másolat

A következő program két eltérő esetet mutat:

  1. egy ismert, érvényes érték reprezentációját karaktertömbön át visszamásolja azonos típusú objektumba;
  2. deklarált típus nélküli allokált tárolóba másol egy típusos forrásobjektumból.

A programban szereplő add_u függvény restrict szerződését a következő rész bontja ki.

Fordítási profil: ISO C17, GNU bővítések nélkül. A helyi laborhoz választott profil Linux/x86-64 System V ABI, GCC 14.2.0 vagy Clang 19.1.0, glibc 2.40. A program nem használ konkrét típusméretet vagy bájtsorrendet. Az alábbi parancsok helyi ellenőrzésre szolgálnak; nem dokumentálnak itt elvégzett fordítást vagy futtatást.

/* representation.c */
#include <assert.h>
#include <stddef.h>
#include <stdlib.h>
#include <string.h>

/*
 * Szerzodes:
 * - n > 0 eseten dst es src legalabb n elo elemre mutat;
 * - dst elemei irhatok, src elemei olvashatok;
 * - a ket feldolgozott tartomany nem fed at;
 * - a fuggveny nem tartja meg a kolcsonzott pointereket.
 */
static void add_u(size_t n,
                  unsigned int *restrict dst,
                  const unsigned int *restrict src)
{
    for (size_t i = 0; i < n; ++i) {
        dst[i] += src[i];
    }
}

int main(void)
{
    unsigned int original = 0x1234u;
    unsigned char image[sizeof original];
    unsigned int restored;

    memcpy(image, &original, sizeof original);
    memcpy(&restored, image, sizeof restored);
    assert(restored == original);

    unsigned int *copy = malloc(sizeof *copy);
    if (copy == NULL) {
        return EXIT_FAILURE;
    }

    memcpy(copy, &original, sizeof *copy);
    assert(*copy == original);

    unsigned int destination[3] = {1u, 2u, 3u};
    unsigned int source[3] = {10u, 20u, 30u};

    add_u(3, destination, source);
    assert(destination[0] == 11u);
    assert(destination[1] == 22u);
    assert(destination[2] == 33u);

    free(copy);
    copy = NULL;
    return EXIT_SUCCESS;
}

A parancsokban a fordítónevek a fenti verziókat jelöljék:

gcc -std=c17 -O2 -Wall -Wextra -Wpedantic representation.c -o representation-gcc
clang -std=c17 -O2 -Wall -Wextra -Wpedantic representation.c -o representation-clang
Objektum vagy hozzáférés Tulajdonos és élettartam Invalidálás, hibakimenet
original, image, restored A main automatikus objektumai A blokk elhagyásakor megszűnnek
copy által elért tároló A felszabadításért a main felel; sikeres malloc-tól free-ig él free érvényteleníti; allokációs hiba esetén EXIT_FAILURE
source, destination A main automatikus tömbjei A blokk elhagyásakor megszűnnek
src, dst Az add_u kölcsönzött hozzáférései Csak a hívás alatt használatosak; szerződéssértésre nincs hibakód, UB következhet

Helyességi bizonyításvázlat. Az első másolás pontosan sizeof original C bájtot ment el. Az image közben nem módosul. A második másolás ugyanannyi bájtot helyez egy azonos típusú, élő célobjektumba, ezért az eredeti, érvényes érték reprezentációját állítja vissza. A padding esetleges jelenléte ezt nem akadályozza.

A sikeres malloc elegendő méretű és az unsigned int számára megfelelően igazított tárolót ad. A memcpy típusos forrása miatt az allokált cél effektív típusa a következő olvasásra unsigned int. A *copy még a felszabadítás előtt történik.

5.4.3.2 Amit a memcpy nem garantál

A memcpy reprezentációt másol, nem numerikus értéket konvertál. Eltérő típusok közötti másolásnál külön kell igazolni, hogy:

Átfedéshez memmove szükséges, de az sem old meg típus- vagy reprezentációs hibát.

Az egyenlő méret önmagában kevés. Egy egész bitmintája nem feltétlenül érvényes lebegőpontos vagy pointerreprezentáció. Külső adatból származó bájtok közvetlen bemásolása ezért nem helyettesíti a protokoll szerinti dekódolást.

Azonos értékű objektumok paddingje eltérhet, ezért a memcmp általában nem értékegyenlőség-vizsgálat. A memóriabeli pillanatkép pedig nem automatikusan hordozható fájlformátum.

5.4.4 Union type punning C-ben

A type punning ugyanazon reprezentáció más típus szerinti értelmezése. C-ben a union erre külön nyelvi lehetőséget ad.

/* ISO C17, feltetelesen hasznalhato szemlelteto reszlet. */
union numeric_view {
    float f;
    unsigned int u;
};

union numeric_view value = {.f = 1.0f};
unsigned int bits = value.u;

ISO C17: ha nem azt a uniontagot olvassuk, amelyen keresztül utoljára tároltunk, az érték reprezentációjának megfelelő részét az olvasott tag típusának reprezentációjaként értelmezzük újra. Ez csapdareprezentációhoz is vezethet. Ha az olvasott tag nagyobb, a korábban írt tagon túli bájtok értéke nem specifikált.

A részlet használatához ezért dokumentált reprezentációs feltételek szükségesek. Nem állítjuk, hogy az 1.0f valamely univerzális egész bitmintát eredményez. Az unsigned int szélessége és a float formátuma sincs itt rögzítve.

A unionos forma nem azonos egy önálló float objektum unsigned int *-en keresztüli olvasásával. A union puszta deklarálása máshol sem tesz szabályossá tetszőleges pointeres type punningot.

C/C++ határ: ezt a C-s lehetőséget nem szabad automatikusan C++-ra átvinni; ott az aktív uniontag és az objektumélettartam szabályait külön kell alkalmazni.

Recept C17 szerinti minősítés
Objektum olvasása unsigned char-ként Megengedett reprezentációs hozzáférés
Egész objektum olvasása nem kompatibilis lebegőpontos lvalue-val Nem megengedett
Ismert érvényes reprezentáció visszamásolása azonos típusba Megfelelő méret és élettartam mellett helyes
Karaktertömb castolása típusos pointerré, majd olvasás A cast nem létesít megfelelő típusú objektumot
Másik uniontag olvasása C-s reprezentáció-újraértelmezés, reprezentációs korlátokkal

5.4.5 restrict: külön szerződés az aliasingról

A restrict nem azt jelenti, hogy két pointer címe soha nem lehet azonos. Hozzáférési szerződés, amelynek jelentése a blokk végrehajtásához és a ténylegesen elért objektumokhoz kötődik.

Az itt vizsgált egyszerű esetben: ha egy objektumot egy restrict pointerből származó hozzáféréssel elérünk, és azt a blokk végrehajtása során módosítjuk is, akkor az objektum összes érintett hozzáférésének ugyanahhoz a korlátozott hozzáférési útvonalhoz kell tartoznia. A pointer másolatai és belőle képzett elemcímek lehetnek ilyen származtatott utak. A részletes szabály a mutatott típus const minősítését is korlátozza, ha a hozzáfért objektum módosul.

Az add_u függvényben a dst[i] olvasódik és módosul. Ha ugyanaz az elem a src útvonalon is olvasódik, sérül a szerződés. Az API ezért egymást nem átfedő feldolgozott tartományokat követel.

A csak olvasott, sehol nem módosított objektumok aliasolását a restrict önmagában nem tiltja.

Optimalizálási következmény: a fordító a típus- és restrict szerződésekre támaszkodva tarthat értékeket regiszterben, átrendezhet műveleteket vagy vektorizálhat ciklusokat. A megsértett szerződés nem „lassabb kódot”, hanem UB-t jelent.

A GCC/Clang -fno-strict-aliasing kapcsolója implementációspecifikus optimalizálási beállítás, nem C17-es helyességi bizonyítás. Nem javítja ki a dangling pointert, a hibás igazítást vagy a restrict megsértését. A warningok és sanitizerek hiánya sem bizonyítja e szerződések teljesülését.

5.4.6 Feladatok

5.4.6.1 Fogalmi osztályozás

Egy élő, inicializált int x objektumot const int, unsigned int, float, illetve unsigned char lvalue-on keresztül szeretnénk olvasni. Melyik hozzáférést engedi a típusszabály? A pointerképzés egyéb feltételeit tekintsd teljesítettnek.

5.4.6.2 Biztonságos pillanatkép készítése

Írj C17-részletet, amely egy double objektum érvényes értékét karaktertömbbe menti, majd egy másik double objektumba visszaállítja. Ne feltételezz konkrét méretet vagy reprezentációt.

5.4.6.3 Optimalizálásfüggő hiba diagnózisa

Egy fejlesztő az add_u(3, values, values) hívást használja egy legalább háromelemű, inicializált unsigned int tömb elemeinek megduplázására. Optimalizálás nélkül a várt értékeket látja. Miért hibás a szerződés? Adj olyan javítást, amely megőrzi az eredeti add_u nem átfedő tartományokra szóló interfészét.

5.4.7 Megoldások

5.4.7.1 A hozzáférések minősítése

A const int kompatibilis típus minősített változata, ezért megengedett. Az unsigned int a megfelelő előjel nélküli típus, ezért szintén megengedett. A float nem tartozik a kivételekhez. Az unsigned char reprezentációs hozzáférésként megengedett, de egyetlen ilyen olvasás nem az egész int numerikus értékét adja.

Ellenőrzési feltétel: a típusszabály teljesülése mellett az objektum él, inicializált, és a hozzáférés a határain belül marad.

5.4.7.2 A pillanatkép megvalósítása

/* ISO C17-reszlet; a forditasi egyseghez: #include <string.h> */
double source = 1.25;
unsigned char snapshot[sizeof source];
double destination;

memcpy(snapshot, &source, sizeof source);
memcpy(&destination, snapshot, sizeof destination);

A három objektum ugyanazon blokk automatikus objektuma; a memcpy csak a hívások idejére kap kölcsönzött hozzáférést. Nincs dinamikus allokáció vagy hibakód. A blokk elhagyása lezárja az élettartamukat.

Ellenőrzési feltétel: a pillanatkép nem módosul a két másolás között, minden objektum él, és a másolások tartományai nem fednek át. Ebben a példában destination == source teljesül. Ez nem általános lebegőpontos egyenlőségteszt minden lehetséges értékre, például NaN-ra.

5.4.7.3 A restrict-hiba javítása

A hívás ugyanazokat az objektumokat módosítja a dst, és olvassa a független src útvonalon. Ez pozitív elemszámnál szerződéssértés. Az optimalizálás nélküli megfigyelés nem teszi helyessé.

A megduplázás külön, egyetlen hozzáférési utat használó művelet lehet:

/* ISO C17-reszlet: values elo, irhato, legalabb haromelemu tomb. */
for (size_t i = 0; i < 3; ++i) {
    values[i] += values[i];
}

A tömb tulajdonosa a hívó; a ciklus nem hosszabbítja meg az élettartamát és nem tart meg pointert. Az add_u változatlanul megmarad nem átfedő tartományokhoz.

Ellenőrzési feltétel: legalább három inicializált elem legyen írható. Az előjel nélküli összeadás túlcsordulása definiált moduloaritmetika; ha ez alkalmazási szinten nem elfogadható, külön tartományellenőrzés szükséges.

5.4.8 Rövid összefoglaló

A tároló címe és mérete nem elegendő a típusos hozzáférés igazolásához. A deklarált típus, az allokált objektum effektív típusa és az lvalue típusa együtt határozza meg, mely hozzáférések megengedettek.

A karakteres hozzáférés reprezentációt vizsgál; a memcpy reprezentációt másol; egyik sem általános értékkonverzió. A unionos type punning C-specifikus feltételekhez kötött. A restrict pedig külön hozzáférési ígéret: megsértése ugyanolyan helyességi hiba, mint egy tiltott típuson keresztüli olvasás.