Egy mérési esemény tartalmazhat számlálót, terhelési arányt vagy hibainformációt. Ha egyszerre pontosan egy változat értelmes, a három külön mező helyett közös tárolót használhatunk. A megtakarítás azonban csak akkor biztonságos, ha azt is egyértelműen nyilvántartjuk, melyik változatot tároltuk.
A C-ben ezt rendszerint egy union és egy külön
változatazonosító együttese, a címkézett unió
(tagged union, más néven discriminated union)
valósítja meg. A kapcsolatot a nyelv nem ellenőrzi automatikusan: az
invariáns a program szerződésének része.
Az alábbiak ISO C17 szabályokra épülnek. A Padding, alignment és offsetof fejezetből a méret–igazítás kapcsolatát, az Effective type, aliasing és objektumreprezentáció részből az érték és reprezentáció különbségét használjuk.
A struct tagjai külön, deklarációs sorrendjüknek
megfelelő tárolási területet kapnak. A union tagjainak
tárolója ezzel szemben átfed: minden tag ugyanott kezdődik.
C17 deklarációs részlet:
union measurement {
unsigned long count;
double load;
};A union measurement mérete elegendő bármelyik tag
tárolásához, igazítása pedig lehetővé teszi bármelyik tag megfelelő
elhelyezését. Mérete legalább a legnagyobb tag mérete, de kitöltés miatt
annál nagyobb is lehet:
Itt az uniótípus, annak valamely tagtípusa; a méretek C bájtban értendők. A union mérete tehát nem a tagméretek összege.
A tagválasztást az inicializálás vagy a tagba történő értékadás határozza meg:
/* C17, önálló programba illeszthető részlet. */
union measurement m = { .count = 120UL };
m.load = 0.75;A második utasítás után a load tag értékét tároljuk. Nem
két független változónk van: a korábbi count nem marad meg
külön.
Az aktív tag kifejezést itt a legutóbb eltárolt változat rövid neveként használjuk. Ez nem jelenti a C++ objektumélettartam-szabályainak átvételét. C17-ben másik tag olvasásának saját reprezentációs szabályai vannak, amelyeket később külön tárgyalunk.
Fontos következmény: a union önmagában nem tartalmaz futásidejű információt arról, melyik tagba írtunk. A debugger sem feltétlenül tudja ezt kikövetkeztetni.
A felsorolási típus, az enum, nevesített egész
konstansokat kapcsol össze:
enum sample_kind {
SAMPLE_COUNT,
SAMPLE_LOAD,
SAMPLE_FAILURE
};ISO C17: explicit értékadás nélkül az első
felsorolási konstans értéke nulla, a következőké eggyel nagyobb. A
felsorolási konstansok típusa int, értéküknek pedig
int-tel ábrázolhatónak kell lenniük.
Ettől külön kérdés magának az enum sample_kind típusnak
a reprezentációja. Egy char, előjeles vagy előjel nélküli
egész típussal kompatibilis; a megfelelő típus kiválasztása
implementáció által meghatározott. Nem következik tehát, hogy minden
enum mérete négy C bájt.
Az enum ráadásul nem zárt, automatikusan ellenőrzött értékhalmaz. Egy
enumobjektum tárolhat a nevesített konstansoktól eltérő, a kompatibilis
típussal ábrázolható értéket is. Emiatt a switch
default ága hasznos védelmi határ.
Ez azonban nem teszi biztonságossá inicializálatlan vagy érvénytelen
reprezentációjú objektum olvasását. A default csak olyan
értéket kezelhet, amelynek kiértékelése már önmagában is érvényes.
A C23 rögzített alaptípusú enumlehetőségeit itt nem használjuk; azok nem részei a C17 szerződésnek.
A címke és a tároló közötti kapcsolatot minden változatra külön rögzítsük:
| Címke | Tárolt uniontag | Invariáns |
|---|---|---|
SAMPLE_COUNT |
count |
Bármely unsigned long érték elfogadható |
SAMPLE_LOAD |
load |
; NaN és végtelen nem megengedett |
SAMPLE_FAILURE |
failure |
code > 0; retryable logikai érték |
Az invariáns része az is, hogy a címke valóban az eltárolt uniontagot azonosítja. A címke átírása önmagában nem konvertálja az adatot.
A típus csak beágyazott értékeket tartalmaz: nincs dinamikus memória, erőforrásátadás vagy kölcsönzött sztring. Ezért a struktúra értékmásolása teljes, önálló másolatot eredményez.
A következő kód sample.c néven fordítható.
Minimumkövetelménye hosted ISO C17. Javasolt, de itt nem fordítással
ellenőrzött laborprofil: GCC 14.2.0 vagy Clang 19.1.7, Linux x86-64
System V ABI, LP64, glibc 2.40, alapértelmezett
adattípus-elrendezéssel.
#include <stdbool.h>
#include <stdio.h>
enum sample_kind {
SAMPLE_COUNT,
SAMPLE_LOAD,
SAMPLE_FAILURE
};
struct failure_info {
int code;
bool retryable;
};
union sample_payload {
unsigned long count;
double load;
struct failure_info failure;
};
struct sample {
enum sample_kind kind;
union sample_payload data;
};
static struct sample sample_count(unsigned long count)
{
return (struct sample) {
.kind = SAMPLE_COUNT,
.data = { .count = count }
};
}
static bool sample_load(double load, struct sample *out)
{
if (out == NULL || !(load >= 0.0 && load <= 1.0)) {
return false;
}
*out = (struct sample) {
.kind = SAMPLE_LOAD,
.data = { .load = load }
};
return true;
}
static bool sample_failure(int code, bool retryable,
struct sample *out)
{
if (out == NULL || code <= 0) {
return false;
}
*out = (struct sample) {
.kind = SAMPLE_FAILURE,
.data = {
.failure = {
.code = code,
.retryable = retryable
}
}
};
return true;
}
static bool sample_print(struct sample s)
{
switch (s.kind) {
case SAMPLE_COUNT:
return printf("count=%lu\n", s.data.count) >= 0;
case SAMPLE_LOAD:
return printf("load=%.2f\n", s.data.load) >= 0;
case SAMPLE_FAILURE:
return printf("failure=%d retryable=%d\n",
s.data.failure.code,
s.data.failure.retryable) >= 0;
default:
return false;
}
}
int main(void)
{
struct sample a = sample_count(42UL);
struct sample b = sample_count(0UL);
struct sample c = sample_count(0UL);
if (!sample_load(0.75, &b) ||
!sample_failure(5, true, &c)) {
return 1;
}
if (!sample_print(a) ||
!sample_print(b) ||
!sample_print(c)) {
return 1;
}
return 0;
}Fordítási parancsok a megfelelő kiadáshoz rendelt fordítóparancsokkal:
gcc-14 -std=c17 -Wall -Wextra -Wpedantic -O2 -fno-fast-math sample.c -o sample-gcc
clang-19 -std=c17 -Wall -Wextra -Wpedantic -O2 -fno-fast-math sample.c -o sample-clangA parancsnevek önmagukban nem garantálják a pontos javítóverziót; azt a laborban külön rögzíteni kell. A példához nem tartozik mért futási eredmény.
| Szereplő | Tulajdonlás és élettartam | Módosítás vagy hibakimenet |
|---|---|---|
a, b, c |
A main helyi objektumai; saját beágyazott
tárolóval |
Élettartamuk a blokk elhagyásáig tart |
out |
Nem tulajdonló hozzáférés a hívó írható struct sample
objektumához |
Nem marad meg a hívás után |
| Sikeres konstrukció | A célobjektum teljes értékét lecseréli | A korábbi változat szerinti értelmezés többé nem használható |
| Sikertelen konstrukció | Nem ír a célobjektumba | false; a cél korábbi értéke változatlan |
sample_print |
Értékmásolatot kap | Ismeretlen címkénél vagy azonnal jelzett kimeneti hibánál
false |
Nem null out esetén előfeltétel, hogy ténylegesen élő,
megfelelően igazított, írható struct sample objektumra
mutasson. A nullvizsgálat dangling pointert nem ismer fel.
Invariánsmegőrzési bizonyításvázlat. A konstruktor
először ellenőrzi a bemenetet. Hibánál nem történik értékadás. Sikernél
az összetett literál egyszerre írja le a helyes címkét és az ahhoz
tartozó tagot, majd a struktúraértékadás ezt az értéket másolja a célba.
A load feltétel a NaN-t is elutasítja, mert az
összehasonlítások nem lesznek igazak. Ez a bizonyítás szabványos
lebegőpontos szemantikát feltételez; ezért nem használunk
fast-math módot.
Az „egyszerre” itt forráskódbeli értékkonstrukciót jelent, nem szálak közötti atomikusságot.
A fenti x86-64 System V LP64 laborprofilhoz, rövidített enum és
packing nélkül, a következő elrendezéssel számolunk. Feltételezett
méret/igazítás párok: enum és int 4/4,
unsigned long és double 8/8,
_Bool 1/1.
| Elem | Offset a közvetlen befoglalóban | Méret | Igazítás |
|---|---|---|---|
failure_info.code |
0 | 4 | 4 |
failure_info.retryable |
4 | 1 | 1 |
struct failure_info |
— | 8 | 4 |
sample_payload bármely tagja |
0 | 8 | tagfüggő |
union sample_payload |
— | 8 | 8 |
sample.kind |
0 | 4 | 4 |
sample.data |
8 | 8 | 8 |
struct sample |
— | 16 | 8 |
Minden adat C bájtban értendő. A táblázat profilhoz kötött levezetés,
nem mérési jegyzőkönyv és nem általános C-garancia. A helyi ellenőrzés
eszköze a sizeof, _Alignof és
offsetof, az előző részben tanult módon.
ISO C17: ha nem a legutóbb eltárolt tagot olvassuk, a megfelelő objektumreprezentációt az olvasott tag típusának reprezentációjaként értelmezzük. Ezt nevezik unionon keresztüli type punningnak.
Szándékosan nem hordozható, elemzésre szolgáló C17 részlet:
union bits {
unsigned int integer;
float real;
};
union bits b = { .integer = 1u };
float x = b.real; /* Nem azonos ezzel: (float)b.integer */Ez nem numerikus egész–lebegőpontos konverzió. Az eredmény az ábrázolásoktól, méretektől és bájtsorrendtől függ. Ha az új tag nagyobb, a korábbi tárolás által nem meghatározott bájtok is érintettek lehetnek. Az előálló reprezentáció csapdareprezentáció (trap representation) is lehet; ilyen érték olvasása nem definiált viselkedést okozhat.
Két gyakori szélsőséges állítás ezért egyaránt hibás:
Az azonos sizeof sem bizonyítja a reprezentációk
kompatibilitását. A memcpy segíthet elkerülni bizonyos
aliasing- és igazítási hibákat, de önmagában nem garantál érvényes
célreprezentációt. Ezeket a C-szabályokat C++-ra sem szabad változtatás
nélkül átvinni.
Külön C17-garancia vonatkozik a közös kezdősorozatra: ha egy union struktúratagjai megfelelő kezdeti tagjaikban kompatibilis típusúak — bitmezőknél azonos szélességgel —, a közös kezdet vizsgálható a másik struktúratagon keresztül is, ahol a teljes uniontípus deklarációja látható. Ez szűk kivétel, nem általános engedély a struktúrák tetszőleges részének átértelmezésére.
Címkézett értéktípusnál egyszerűbb szabályt alkalmazunk: mindig a címkéhez tartozó tagot olvassuk.
A bitmező (bit-field) egész típusú struktúra- vagy uniontag, amelyhez bitszélességet adunk meg. Folyamaton belüli jelzőkhöz kényelmes lehet:
/* C17 deklarációs részlet; a fizikai elrendezés ABI-függő. */
struct local_flags {
unsigned int ready : 1;
unsigned int mode : 3;
unsigned int : 2;
unsigned int : 0;
unsigned int retries : 4;
signed int delta : 4;
_Bool enabled : 1;
};ISO C17: a szélesség nemnegatív egész
konstanskifejezés; nem haladhatja meg az alapul szolgáló típusban
rendelkezésre álló bitszámot. A biztosan használható típusok
_Bool, signed int és
unsigned int; más típusok megengedése implementáció által
meghatározott. A _Bool bitmező legfeljebb egy bit széles
lehet.
Az egyszerű int bitmező előjelessége implementáció által
meghatározott, ezért azt érdemes explicit megadni. Egy
bites unsigned int bitmező értéktartománya:
Így a mode tartománya 0–7. A
signed int delta : 4 pontos negatív tartományánál C17-ben a
signed reprezentációt is figyelembe kell venni; a −8–7 tartomány kettes
komplemens profilt feltételez.
A névtelen, nem nulla szélességű mező elérhetetlen kitöltési helyet foglal. A nulla szélességű mező csak névtelen lehet: megtiltja, hogy további bitmező kerüljön az előző bitmezőt tartalmazó allokációs egységbe. Nem jelent hordozható „következő négybájtos címre igazítást”.
A fizikai elrendezés több külön döntést tartalmaz:
| Tulajdonság | C17 szerinti korlát |
|---|---|
| Magas vagy alacsony helyiérték felől történő kiosztás | Implementáció által meghatározott |
| Nem elegendő hely esetén egységhatár átlépése | Implementáció által meghatározott |
| Az allokációs egység igazítása | Nem specifikált |
| Konkrét elrendezés fordítók közötti egyezése | Az ABI és fordítási opciók szerződésétől függ |
A bitmezőnek nem képezhető címe, és közvetlenül sizeof
sem alkalmazható rá. Bitmezőre hordozható offsetof-alapú
offsetet sem kaphatunk.
Ezért a bitmezős struktúra nem hordozható hálózati formátum. A bitkiosztás nem vezethető le pusztán a processzor bájtsorrendjéből. Wire protokollhoz explicit oktettsorrend, maszkok és eltolások szükségesek; ezt a Byte order és biztonságos szerializálás rész folytatja.
A címke átírása uniontag-értékadás nélkül csak inkonzisztens objektumot hoz létre. Javítása a teljes új változat konstrukciója.
A default ág nem általános unionvalidátor. Felismerhet
ismeretlen címkét, de nem bizonyítja, hogy egy ismert címke mellett
valóban a megfelelő tagot tárolták.
A „nálam két bájt” megfigyelés nem bitmező-protokollszerződés. Más ABI, packing vagy fordítóopció megváltoztathatja az elrendezést.
Végül az értékmásolás csak a jelenlegi, beágyazott adatokat tartalmazó típusnál jelent teljesen független értéket. Pointertag hozzáadása után külön tulajdonlási szerződésre lenne szükség.
Egy SAMPLE_COUNT érték címkéjét
SAMPLE_LOAD-ra írjuk, a unionhoz nem nyúlunk. Megőrzi-e ez
az invariánst? Megjavítaná-e egy default ág?
Írj sample_is_retryable(struct sample s) függvényt. Csak
újrapróbálható hibaváltozat esetén adjon igaz értéket, és más
változatnál ne olvassa a failure tagot.
Egy protokoll két oktettjéből az első felső három bitje
típusazonosító, a fennmaradó tizenhárom bit pedig hossz, magas
helyiértékű oktettel előre. A bemenet A0 01.
Miért nem megfelelő dekóder egy három- és tizenhárom bites
unsigned int bitmezőt tartalmazó struktúra? Add meg a
helyes mezőértékeket explicit bitműveletekkel.
Az invariáns sérül: a címke és a legutóbb eltárolt tag eltér. A
default nem segít, mert a címke ismert. A
sample_load konstruktor használata helyreállítja a
kapcsolatot, és az értéktartományt is ellenőrzi.
Ellenőrzési feltétel: minden változatváltás után együtt legyen helyes a címke, a választott tag és annak tartalmi invariánsa.
C17 részlet, a teljes példa típusdeklarációi után:
static bool sample_is_retryable(struct sample s)
{
return s.kind == SAMPLE_FAILURE && s.data.failure.retryable;
}A && rövidzáras: a jobb oldalt csak igaz bal
oldal esetén értékeli ki. Előfeltétel a típus invariánsát teljesítő
bemenet.
Ellenőrzési feltétel: számlálóra és terhelésre hamis; hibára a
retryable értékével egyező eredmény.
A bitmezők sorrendje, allokációs egysége és kitöltése nem a protokoll szabályait követi. Még a struktúra kétoktettes mérete sem garantált.
Nyolcbites oktettprofilban, helyi tömbbel:
/* C17, CHAR_BIT == 8 előfeltétellel; elemzési részlet. */
unsigned char bytes[2] = { 0xA0u, 0x01u };
unsigned int type = (bytes[0] >> 5) & 0x07u;
unsigned int length =
((unsigned int)(bytes[0] & 0x1Fu) << 8) |
(unsigned int)bytes[1];A tároló a helyi bytes tömb; nincs megtartott
kölcsönzött hozzáférés. A felső három bit 101, ezért
type == 5. Az alsó öt bit nulla, így
length == 1. Ezek levezetett ellenőrzési értékek, nem
futtatási eredmények.
A union közös tárolót ad, nem automatikus változatkezelést. Biztonságos értéktípushoz explicit címke, változatonkénti invariáns és címkevezérelt hozzáférés szükséges. A C17 union type punning reprezentációt értelmez át, ezért trap- és hordozhatósági korlátai vannak. A bitmezők logikai szélességet fejeznek ki, de fizikai elrendezésükből nem következik hordozható wire formátum.