18.1 C23 nyelvi újdonságok és inkompatibilis változások

A C23-ra váltás nem egyszerűen új fordítókapcsoló bekapcsolása. Egyes korábbi fordítóbővítések szabványos nyelvi alakot kaptak, új kulcsszavak jelentek meg, néhány régi konstrukció pedig megváltozott vagy megszűnt. A migráció célja ezért nem az, hogy „leforduljon”, hanem hogy a program helyességi szerződése megmaradjon.

A Szabványok, dialektusok és hordozhatósági szerződés alapján külön kezeljük az ISO C17-et, az ISO C23-at és a fordítóbővítéseket. A változtatásokat a Záróreview: helyesség, diagnosztika és mérés módszerével kell ellenőrizni: rögzített környezetben, pozitív és negatív fordítási próbákkal.

18.1.1 Támogatásvizsgálat: nyelvi mód és tényleges képesség

A __STDC_VERSION__ C23-hoz tartozó értéke 202311L. Ez a választott nyelvi verzió jelzése, nem minden képesség hibátlan megvalósításának bizonyítéka. Egy régebbi fordító -std=c2x módja pedig nem automatikusan azonos a végleges C23-támogatással.

Az alábbiakhoz javasolt, itt nem futtatott vizsgálati profil: Linux, x86-64 System V ABI, LP64, GCC 15.1.0 és Clang 20.1.0, glibc 2.41. Ez vizsgálati célkörnyezet, nem teljes támogatási ígéret. A tényleges fordítókiadást, sysrootot és headereket külön rögzíteni kell.

A következő C23-kódrészleteket képességenként külön illeszd ebbe a teljes próbaprogramba:

/* probe.c: a behelyettesített részletek minimuma ISO C23. */
int main(void)
{
    /* Egy vizsgált részlet helye. */
    return 0;
}

Fordítási próbák:

gcc -std=c23 -Wall -Wextra -pedantic-errors -Werror=attributes \
    -fsyntax-only probe.c
clang -std=c23 -Wall -Wextra -pedantic-errors -Werror=attributes \
    -fsyntax-only probe.c

A C17-alternatívákat ugyanezekkel a kapcsolókkal, de -std=c17 módban vizsgáld. A részletekhez alapvetően nem kell libc-header; ahol mégis, azt külön jelezzük. A -fsyntax-only nyelvi és headerpróba, nem linkelési vagy futási teszt.

18.1.2 Logikai értékek és null pointer

ISO C23: a bool, true és false nyelvi elemek; használatukhoz nem szükséges <stdbool.h>. A true és false típusa bool.

/* C23-részlet; két külön képességpróbára is bontandó. */
bool enabled = true;
int *p = nullptr;
(void)enabled;
(void)p;

C17-alternatíva: <stdbool.h> mellett ugyanaz a logikaiérték-szintaxis használható, de makrókon keresztül: a bool a _Bool típushoz kapcsolódik, a true és false pedig egész konstansokra helyettesítődik. Header nélkül _Bool enabled = 1; írható.

A C23 nullptr előre definiált nullpointer-konstans, saját típussal. A típus nullptr_t neve a <stddef.h> headerből érhető el; ehhez külön headerpróba kell: a header beillesztése után fordítsd a nullptr_t n = nullptr; deklarációt. C17-alternatíva: például int *p = 0;.

A nullptr nem jelent foglalt tárolót, és nem teszi típusbiztonságossá a variadikus hívásokat. Ha egy ilyen interfész void * argumentumot vár, add át a megfelelő pointertípust, például (void *)nullptr.

Pointerhelyzet Tulajdonos és élettartam Érvénytelenítés, hibakimenet
p = nullptr Nincs hivatkozott objektum vagy birtokolt tároló Dereferálása nem definiált viselkedés
Dinamikus buffer pointere A felszabadításért felelős programrész a tulajdonos free és sikeres realloc érvényteleníti a régi hozzáféréseket

Migrációs törés lehet egy korábbi int nullptr; deklaráció is: C17-ben az azonosító megengedett, C23-ban már foglalt nyelvi elem.

18.1.3 Típuslevezetés és típuslekérdezés

ISO C23: az auto inicializálóból történő típuslevezetést (type inference) tesz lehetővé. Az egyszerű hordozható használat egyetlen deklarált objektumot és inicializálót tartalmaz.

/* C23-részlet. */
auto retries = 3u;
(void)retries;

A retries típusa unsigned int; ez fordításidejű típusválasztás, nem dinamikus típusosság. C17-alternatíva: unsigned int retries = 3u;. C17-ben az auto csak tárolásiosztály-jelölő, például auto int retries = 3;.

A typeof egy kifejezés vagy típusnév típusát adja meg, a typeof_unqual pedig annak minősítők nélküli, nem atomikus változatát:

/* C23-részlet; a két typeof-alak külön is próbálandó. */
const int source = 4;
typeof(source) preserved = 5;
typeof_unqual(source) writable = 6;
writable = 7;
(void)preserved;
(void)writable;

A preserved típusa const int, a writable típusa int. C17-alternatíva: ezek explicit kiírása; általános, szabványos typeof operátor nincs.

A GNU-dialektusban korábban elfogadott typeof vagy __auto_type nem bizonyít ISO C17-támogatást. A fenti szigorú fordítási próba éppen ezt választja el a szabványos C23-használattól. A typeof operandusáról változó hosszúságú tömbtípusok környezetében nem szabad általánosan feltételezni, hogy biztosan nem értékelődik ki.

18.1.4 constexpr: C-ben objektum, nem C++-szerű függvény

ISO C23: a constexpr konstans objektum definiálására használható; nem vezet be C++-szerű constexpr függvényeket.

/* C23-részlet. */
constexpr int capacity = 8;
int slots[capacity];
_Static_assert(capacity == 8, "unexpected capacity");
(void)slots;

Ebben az egész típusú példában a capacity konstans kifejezésben használható, ezért a slots nem változó hosszúságú tömb. A _Static_assert szándékosan a C17-ben is létező alak: így a próba a constexpr képességet vizsgálja.

C17-alternatíva: enum { capacity = 8 }; vagy makró. A const int capacity = 8; C17-ben önmagában nem teszi a nevet egész konstans kifejezésben használhatóvá.

Összehasonlításként ez önálló ISO C++20 program, könyvtári függőség nélkül:

// constexpr_demo.cpp
constexpr int twice(int value)
{
    return 2 * value;
}

static_assert(twice(4) == 8);

int main()
{
    return 0;
}
g++ -std=c++20 -Wall -Wextra -pedantic-errors \
    -fsyntax-only constexpr_demo.cpp
clang++ -std=c++20 -Wall -Wextra -pedantic-errors \
    -fsyntax-only constexpr_demo.cpp

A függvényes alak C23-ba másolva nem válik érvényes C-vé. Az azonos kulcsszó eltérő nyelvi képességet jelöl.

18.1.5 Egész típusok, enumok és literálok

ISO C23: a _BitInt(N) bitszélességet megadó egész típus. Előjeles alakban N≥2N\geq2, előjel nélkül N≥1N\geq1. Az előjeles alak szélességébe az előjelbit is beleszámít. A támogatott felső korlátot a <limits.h> BITINT_MAXWIDTH makrójával, majd konkrét fordítási próbával kell ellenőrizni.

/* C23-részlet; a típust a literáloktól külön vizsgálja. */
_BitInt(10) signed_value = 300;
unsigned _BitInt(9) unsigned_value = 300;
(void)signed_value;
(void)unsigned_value;

Az előjeles, NN bites tartomány:

−2N−1≤x≤2N−1−1. -2^{N-1}\leq x\leq 2^{N-1}-1.

Ezért 300 előjeles tárolásához 10 bit kell: 28−1=255<3002^8-1=255<300, míg 29−1=511≥3002^9-1=511\geq300. Előjel nélkül 9 bit elegendő.

Bizonyításvázlat: kettes komplemensben az előjelbit súlya −2N−1-2^{N-1}, az alsó bitek maximális összege 2N−1−12^{N-1}-1. Ebből adódnak a határok.

C17-alternatíva: megfelelő tartományú hagyományos egész típus, szükség esetén több szóból felépített reprezentáció. A uintN_t típusok csak elérhetőségük ellenőrzése után használhatók, és nem helyettesítenek tetszőleges bitszélességet.

A kapcsolódó C23-újdonságok:

Képesség és külön fordítandó próba C17-alternatíva
int x = 0b1010; – bináris literál int x = 0xA;
int x = 1'024; – számjegyelválasztó int x = 1024;
auto x = 300wb; – előjeles _BitInt literál Megfelelő hagyományos típus és literál
auto x = 300uwb; – előjel nélküli változat Megfelelő előjel nélküli típus
enum opcode : unsigned char { OP_DATA = 1 }; Hagyományos enum, vagy unsigned char typedef és külön konstansok

A literálpróbák végére kerüljön (void)x;. A wb és uwb a szükséges legkisebb megfelelő bitszélességet választja.

A rögzített mögöttes típusú enum (fixed underlying enum) értékeinek reprezentálhatónak kell lenniük a megadott típusban. Az unsigned char azonban nem általánosan „8 bites”: ezt CHAR_BIT határozza meg. A bitszélességből a _BitInt tárolómérete, paddingje és ABI-beli átadása sem következik; ezek külön implementációs vizsgálatok.

18.1.6 Attribútumok és static_assert

ISO C23: szabványos attribútumalak például a [[maybe_unused]] és a [[nodiscard]].

/* C23-részlet. */
[[maybe_unused]] int reserved = 0;
[[nodiscard]] int check_configuration(void);
static_assert(sizeof(char) == 1);

A nodiscard az eredmény figyelmen kívül hagyásának diagnosztizálását támogatja, nem futásidejű ellenőrzés. A static_assert kulcsszó, és az üzenet elhagyható.

C17-alternatíva: _Static_assert(feltetel, "uzenet");; a static_assert név <assert.h> mellett makróként használható, két argumentummal. A bemutatott attribútumoknak nincs általános ISO C17-megfelelőjük: a fordítóspecifikus helyettesítés külön bővítésszerződés.

Attribútumonként ellenőrizd a __has_c_attribute operátor elérhetőségét és például a __has_c_attribute(nodiscard) eredményét. Az elfogadott szintaxis önmagában kevés: az ismeretlen attribútum figyelmen kívül hagyható. Ezért kell a figyelmeztetést hibává emelő próba; a static_assert alakjait pedig az attribútumoktól külön fordítsd.

18.1.7 Inkompatibilis változások és migrációs hibák

Függvénydeklarációk. C17-ben az int ping(); nem közli a paraméterek számát és típusát. C23-ban nulla paramétert jelent, mint az int ping(void);. A régi K&R-függvénydefiníciók megszűntek. Mindkét verzióhoz jó migrációs cél az explicit prototípus.

Negatív próbaként a ping(1) hívásnak C23-ban diagnosztikát kell kiváltania. C17-beli elfogadása nem bizonyít helyes hívást. Az implicit int és az előzetes deklaráció nélküli függvényhívás már C17-ben sem megengedett; ezek nem új C23-szigorítások.

Kettes komplemens. C23 előírja az előjeles egész típusok kettes komplemens reprezentációját. C17 több reprezentációt megengedett. Ettől az előjeles túlcsordulás továbbra is UB; a migráció nem jogosít fel körbeforduló előjeles aritmetikára. Ezt fordítási siker vagy próbaértékek nem bizonyítják biztonságosnak.

Nulla méretű realloc. A realloc(p, 0) C17-ben implementáció által meghatározott kezelésű; C23-ban nem definiált viselkedés. Nem helyes futtatással „felderíteni”, hogyan működik.

A mindkét verzióban megfelelő javítás: ha a kért méret nulla, free(p) után állítsd a tulajdonosi pointert nullára. Egyébként ideiglenes pointerrel hívd a realloc-ot. Nem nulla méretű sikertelen hívásnál a régi foglalás megmarad; siker esetén a korábbi kölcsönzött hozzáférések érvényüket vesztik, akkor is, ha a cím numerikusan változatlan. A pontos minta a realloc és dinamikusan növekvő buffer részben szerepel.

18.1.8 Feladatok

  1. Egy typeof-ot használó fájl lefordul -std=gnu17 módban. Mit bizonyít ez, és milyen további próbák szükségesek?
  2. Írj C23-ban fordításidejű, 16 értékű capacity objektumot és vele rögzített méretű tömböt. Add meg a C17-alternatívát.
  3. Auditálj egy C17-modult, amely int flush(); deklarációt, K&R-definíciót és realloc(buffer, requested) hívást használ. A requested lehet nulla. Milyen javítások és ellenőrzések kellenek?

18.1.9 Megoldások

  1. Csak az adott GNU-dialektus támogatása bizonyított. Külön próbáld -std=c17 -pedantic-errors és -std=c23 -pedantic-errors mellett, mindkét rögzített fordítóval. Az ellenőrzési feltétel a dialektusfüggő és a szabványos elfogadás elkülönítése.

  2. C23-ban constexpr int capacity = 16; int data[capacity];. C17-ben enum { capacity = 16 }; int data[capacity];. Adj hozzá _Static_assert(capacity == 16, "capacity"); ellenőrzést. Mindkét változatnak a saját nyelvi módjában kell fordulnia; C17-ben egy puszta const int nem egyenértékű helyettesítés.

  3. Írj int flush(void); deklarációt, és alakítsd a K&R-definíciót típusos paraméterlistájúvá. A nulla méretet külön felszabadítási ág kezelje. Ellenőrizd a nulla, a nem nulla sikeres és az injektált allokációs hibás esetet; továbbá azt, hogy az érvénytelenített kölcsönzött pointereket senki sem használja. A hibás nulla méretű hívást ne futtasd C23-as tesztként.

18.1.10 Összefoglaló

A C23-váltás képességenkénti döntés. Minden új konstrukcióhoz tartozzon C17-alternatíva, explicit nyelvi mód és elkülönített támogatáspróba. Különösen vizsgáld az új kulcsszavakkal ütköző neveket, a prototípus nélküli deklarációkat és a nulla méretű realloc-ot. A modernebb szintaxis nem helyettesíti az ownership-, élettartam- és túlcsordulási szerződést.