11.5 Numerikus és ritkább C könyvtári eszközök térképe

A standard fejléc kiválasztása nem pusztán névkeresés: egyben szerződésválasztás. Egy numerikus függvénynél számít az értelmezési tartomány és a hibajelzés módja; karakterfeldolgozásnál az aktuális locale és a konverziós állapot; diagnosztikánál pedig az, hogy a vizsgálat egyáltalán bekerül-e a programba.

Ez a térkép a lebegőpontos reprezentációra, a típusfüggő headeres eszközökre és a környezeti állapotra épít. Az alap ISO C17, hosted környezetben; az opcionális képességeket külön jelöljük. A fejléc megléte önmagában nem bizonyít például teljes IEC 60559 lebegőpontos működést vagy meghatározott Unicode-kódolást.

11.5.1 Numerikus függvények és lebegőpontos környezet

A <math.h> valós matematikai műveleteket, osztályozó makrókat és hibajelzési eszközöket ad. A függvénycsaládokban rendszerint a toldalék nélküli változat double, az f végű float, az l végű long double típussal dolgozik:

/* ISO C17; <math.h>; kifejezésrészletek. */
sqrt(9.0)
sqrtf(9.0f)
sqrtl(9.0L)

A típust nem szabad összekeverni a pontossággal: a long double nem minden implementációban ad több értékes számjegyet a double típusnál.

A tartományhiba (domain error) azt jelenti, hogy az argumentum kívül esik a függvény értelmezési tartományán; ilyen a valós sqrt(-1.0). Az eredménytartomány-hiba (range error) az eredmény reprezentálhatóságával kapcsolatos, például túlcsorduláskor. A math_errhandling jelzi, hogy az implementáció errno-alapú jelzést (MATH_ERRNO), lebegőpontos kivételjelzőket (MATH_ERREXCEPT), vagy mindkettőt használja. Az errno == 0 ezért önmagában nem általános sikerfeltétel.

11.5.1.1 Kidolgozott numerikus példa

A következő programban a matematikai előfeltételt futásidejű ellenőrzés biztosítja. Az assertion csak egy belső invariánst dokumentál.

/* numeric.c — ISO C17, hosted környezet */
#include <assert.h>
#include <float.h>
#include <inttypes.h>
#include <limits.h>
#include <math.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    const double squared = 9.0;

    if (!isfinite(squared) || squared < 0.0) {
        return EXIT_FAILURE;
    }

    const double root = sqrt(squared);
    assert(root >= 0.0);

    const uintmax_t samples = 42;
    const size_t object_bytes = sizeof root;

    if (printf("root=%.17g samples=%" PRIuMAX
               " sizeof=%zu CHAR_BIT=%d DBL_MANT_DIG=%d\n",
               root, samples, object_bytes,
               CHAR_BIT, DBL_MANT_DIG) < 0) {
        return EXIT_FAILURE;
    }

    return fflush(stdout) == 0 ? EXIT_SUCCESS : EXIT_FAILURE;
}

A fordítási parancs választott laborprofilja: GCC 14.2.0, glibc 2.40, Linux x86-64, System V ABI, LP64. Ez nem futtatással ellenőrzött eredményközlés.

gcc -std=c17 -Wall -Wextra -Wpedantic numeric.c -lm -o numeric

A -lm e toolchain linkelési konvenciója, nem ISO C-előírás. A matematikailag várt gyökérték 33; a kiírt típusjellemzőket a tényleges implementáció szolgáltatja. A sizeof egysége C bájt, amelynek bitszámát a CHAR_BIT adja meg.

Indoklás. A negatív, végtelen és NaN bemeneteket kizártuk. Nemnegatív, véges double érték négyzetgyöke nem okoz túlcsordulást; a példában a bemenet és az eredmény is pontosan reprezentálható. Az általános numerikus pontosság kérdését ettől még nem oldja meg egy assertion.

11.5.1.2 Komplex számok és típusgenerikus matematika

A <complex.h> komplex típusokhoz és műveletekhez tartozik. C17-ben a komplex támogatás opcionális; hiányát a __STDC_NO_COMPLEX__ makró jelezheti.

A <tgmath.h> típusgenerikus makrói az argumentum típusa alapján választanak matematikai függvényt. Ez nem C++-féle túlterhelés.

/* ISO C17; komplex támogatást igénylő részlet. */
#include <complex.h>
#include <tgmath.h>

/* Függvénytörzsben: */
double complex z = 1.0 + 2.0 * I;
double complex root = sqrt(z); /* A megfelelő csqrt-változat. */
double magnitude = cabs(root);

Itt a sqrt komplex műveletet választ. Ha ugyanaz a makró egész argumentumot kap, a választásnál az egész típus rendszerint double-nak számít. Az „azonos függvénynév” tehát nem jelent azonos eredménytípust vagy költséget.

11.5.1.3 A <fenv.h> szerepe

A lebegőpontos környezet a támogatott kerekítési módokat és a lebegőpontos kivételjelzőket foglalja magába. A jelző nem C++-kivétel, és nem szükségképpen operációsrendszer-szignál.

A jelzők tartósan beállítva maradhatnak, amíg nem töröljük őket. Ezért egy művelet vizsgálatának sorrendje:

  1. a releváns jelzők törlése, a feclearexcept eredményének ellenőrzésével;
  2. a vizsgált művelet;
  3. a jelzők lekérdezése a fetestexcept segítségével.

Például a fetestexcept(FE_INVALID) != 0 beállított érvénytelenművelet-jelzőt keres, ha a FE_INVALID támogatott és definiált. Törlés nélkül egy korábbi művelet hibáját is láthatjuk.

Ha az aritmetika környezetfüggő működésére támaszkodunk, a #pragma STDC FENV_ACCESS ON fordítói támogatását külön ellenőrizni kell. A fejléc jelenléte ezt nem bizonyítja; a környezetet figyelmen kívül hagyó optimalizálási módok, például bizonyos -ffast-math beállítások, alkalmatlanok ilyen vizsgálathoz.

11.5.2 Assertion és üzemi hibakezelés

A <assert.h> assert makrója belső programinvariánsok diagnosztikájára szolgál. Aktív állapotban a hamis feltétel diagnosztikát és rendellenes programleállást vált ki.

Ha az NDEBUG definiált a fejléc feldolgozásakor, az assertion feltétele nem értékelődik ki. Az NDEBUG=0 is definiált makró, tehát szintén kikapcsolja az assertionöket.

/* Hibás tervezési példa; a művelet eltűnhet a release buildből. */
assert(++count < limit);

A javítás nem pusztán az inkrementálás áthelyezése. Előbb a művelet saját előfeltételeit — például a túlcsordulás kizárását — kell biztosítani, utána külön végrehajtani, végül mellékhatásmentesen ellenőrizni az invariánst.

Felhasználói bemenetet, allokációs hibát vagy fájlmegnyitási hibát nem kezelünk kizárólag assertionnel. Ezek a program üzemi hibaszerződésének részei, release buildben is.

11.5.3 Locale, széles és többbájtos karakterek

A „széles karakter” (wide character) nem azonos azzal, hogy „Unicode-kódpont”. A wchar_t szélessége és a hozzá kapcsolódó karakterkódolás implementációfüggő.

Fejléc Feladat és minimális példa Fontos feltétel
<locale.h> setlocale(LC_CTYPE, "") Környezetből választ locale-t; sikertelenségkor null pointert ad
<wchar.h> mbrtowc(&wc, "A", 1, &state) Állapottartó többbájtos dekódolás
<wctype.h> iswalpha((wint_t)wc) Locale-függő karakterosztályozás
<uchar.h> mbrtoc16(&unit, "A", 1, &state16) char16_t kódegységet állít elő

A táblázat kifejezésrészleteiben wc egy wchar_t, unit egy char16_t, a két állapot pedig külön, nullával inicializált mbstate_t objektum. A szükséges deklaráció például:

/* ISO C17; <wchar.h>; függvénytörzs-részlet. */
mbstate_t state = {0};
wchar_t wc;
size_t consumed = mbrtowc(&wc, "A", 1, &state);

A hívó tulajdonolja a kimeneti és állapotobjektumot; a függvény csak a hívás idejére kölcsönzi a címüket. Az állapotot ugyanazon konverziós folyamat folytatásához meg kell őrizni. Az objektum élettartamának vége érvényteleníti a rá mutató pointert; az állapot felülírása pedig elveszítheti a dekódolás folytatásához szükséges információt.

A mbrtowc eredménye nem egyszerű siker/hiba érték: (size_t)-1 érvénytelen sorozatot, (size_t)-2 hiányos sorozatot, nulla nullkaraktert jelez. A mbrtoc16 ezenfelül (size_t)-3 eredménnyel is visszatérhet, amikor korábban feldolgozott bemenetből további kódegységet ad ki.

A __STDC_UTF_16__ és __STDC_UTF_32__ makrók a megfelelő kódolási garanciákat jelzik. A típusnevekből önmagukban ne következtess teljes Unicode-feldolgozásra.

A setlocale visszatérési sztringjét az implementáció kezeli: nem szabad felszabadítani vagy módosítani, és későbbi setlocale hívás felülírhatja. A locale váltása megosztott környezeti állapotot érint; részletes szövegkezelési hátterét a kódolásról szóló rész tárgyalja.

11.5.4 Szignálok és nem lokális vezérlés

A <signal.h> az ISO C szignálmodelljét adja. Minimális jelzőobjektum például:

/* ISO C17; fájlszintű deklaráció. */
#include <signal.h>
static volatile sig_atomic_t stop_requested = 0;

Ez nem általános szálközi szinkronizációs eszköz. Egy aszinkron szignálkezelőből megengedett műveletek köre erősen korlátozott; a printf vagy a tetszőleges cleanup meghívását nem teszi biztonságossá ez a deklaráció. Az ISO C és a POSIX szignálszerződéseit a dedikált szignálfejezet választja szét.

A <setjmp.h> setjmp/longjmp párosa mentett végrehajtási környezethez ugrik vissza:

/* ISO C17; <setjmp.h>; függvénytörzs-részlet. */
jmp_buf env;

if (setjmp(env) == 0) {
    longjmp(env, 1);
}

Az első visszatérés nulla; az ugrás utáni visszatérés itt egy. A setjmp hívást tartalmazó függvénynek még aktívnak kell lennie. A setjmp után módosított, nem volatile, automatikus lokális változók értéke az ugrás után indeterminate lehet.

A vezérlésugrás nem futtat automatikus erőforrás-cleanupot. Ezért nem egyszerű helyettesítője a hibavisszatérésnek; lásd a nem lokális vezérlés életciklus-korlátait.

11.5.5 Típushatárok és hordozható referenciaértékek

Fejléc Mire használjuk? Gyakori tévedés
<limits.h> CHAR_BIT, INT_MAX, LONG_MAX A long nem mindenhol 64 bites
<float.h> FLT_RADIX, DBL_MANT_DIG, DBL_MAX A DBL_MIN a legkisebb pozitív normál érték, nem a legnegatívabb
<stddef.h> size_t, ptrdiff_t, offsetof, max_align_t A sizeof nem szükségképpen oktettekben mér
<inttypes.h> PRIuMAX, PRId64, strtoimax A %ld nem általános helyettesítője a típusspecifikus formátumnak

A DBL_EPSILON az 11 és a következő nagyobb reprezentálható double érték különbségét jellemzi; nem univerzális numerikus tolerancia.

Az exact-width típusok, például az int64_t, opcionálisak. A hozzájuk tartozó formázó makrókra csak a megfelelő típus rendelkezésre állásakor támaszkodhatunk. A fenti program ezért használja a hordozható uintmax_t–PRIuMAX párost. Az offsetof(T, member) eredménye C bájtban értendő; bit-fieldre nem alkalmazható.

11.5.6 A C17 fejléc-térkép és az opcionális részek

A C17 29 standard fejlécnevének teljes térképe:

Terület Fejlécek
Numerika és környezet <math.h>, <complex.h>, <tgmath.h>, <fenv.h>
Típusjellemzők <limits.h>, <float.h>, <stddef.h>, <stdint.h>, <inttypes.h>
Karakterek és locale <ctype.h>, <locale.h>, <wchar.h>, <wctype.h>, <uchar.h>
I/O, memória, idő, hibák <stdio.h>, <stdlib.h>, <string.h>, <time.h>, <errno.h>
Diagnosztika és vezérlés <assert.h>, <signal.h>, <setjmp.h>
Nyelvi segédeszközök <stdarg.h>, <stdbool.h>, <stdalign.h>, <stdnoreturn.h>, <iso646.h>
Konkurencia <stdatomic.h>, <threads.h>

A táblázat leltár, nem minden környezetre érvényes támogatási garancia. A komplex támogatás mellett az atomikus és szálkönyvtári támogatás is lehet opcionális; a kapcsolódó jelzések __STDC_NO_ATOMICS__ és __STDC_NO_THREADS__. Freestanding implementációban a könyvtári minimum szűkebb.

11.5.6.1 Annex K: névleges bounds-checking nem jelent automatikus biztonságot

Az Annex K opcionális, bounds-checking interfészeket ad, például memcpy_s és strcpy_s nevekkel. Nem önálló fejléc.

Támogatását a __STDC_LIB_EXT1__ jelzi; használati igényét a releváns fejlécek előtt definiált, 1 értékű __STDC_WANT_LIB_EXT1__ makróval kell megadni. A definíció önmagában nem teremti meg a hiányzó libc-támogatást.

Ezek az interfészek sem tudják általában ellenőrizni, hogy a hívó által közölt pufferméret megfelel-e a tényleges objektumnak. Futásidejű szerződéssértéskor constraint handler is szerepet kap, így a hibakezelés nem feltétlenül egyszerű visszatérési kód. Egy gyártói _s nevű függvény nem szükségképpen Annex K-kompatibilis. Hordozható API-t ezért ne építs ellenőrizetlen elérhetőségükre.

11.5.7 Feladatok

  1. Magyarázat. Miért hibás általános állítás, hogy a DBL_EPSILON minden összehasonlításhoz megfelelő tolerancia, illetve hogy a wchar_t Unicode-kódpontot tárol?

  2. Kódolás. A numerikus program samples változója legyen módosítható. Növeld eggyel túlcsordulás nélkül, majd assertionnel ellenőrizd, hogy pozitív. Az üzemi működés -DNDEBUG mellett se változzon.

  3. Diagnosztika. Egy C17 build feltétel nélkül használ komplex számokat és memcpy_s-t. Numerikus hibát kizárólag errno alapján keres, miközben -ffast-math kapcsolóval fordít. Milyen különálló ellenőrzések hiányoznak?

11.5.8 Megoldások

11.5.8.1 Magyarázó feladat

A DBL_EPSILON az egység körüli reprezentációt jellemzi. A megfelelő tolerancia függ a nagyságrendtől, a számítás hibájától és az alkalmazás követelményeitől. A wchar_t jelentése implementáció- és locale-függő; egy kódegység ráadásul nem szükségképpen teljes kódpont.

Ellenőrzési feltétel: a válasz különítse el a reprezentációt az alkalmazási pontosságtól, illetve a típust a kódolástól.

11.5.8.2 Kódolási feladat

Az eredeti deklaráció legyen uintmax_t samples = 42;, majd következzen:

/* A numeric.c main függvényébe illesztendő C17-részlet. */
if (samples == UINTMAX_MAX) {
    return EXIT_FAILURE;
}
++samples;
assert(samples > 0);

Az unsigned aritmetika körbefordulása definiált lenne, de megsértené a számláló szerződését. A guard ezt előzi meg.

gcc -std=c17 -Wall -Wextra -Wpedantic -DNDEBUG \
    numeric.c -lm -o numeric-release

Ellenőrzési feltétel: mindkét buildben a számláló várt értéke 43; UINTMAX_MAX kezdőértéknél mindkettő hibával térjen vissza.

11.5.8.3 Diagnosztikai feladat

Külön kell vizsgálni a komplex támogatást, az Annex K implementációs és deklarációs feltételeit, valamint a math_errhandling jelzését. A lebegőpontos környezetre támaszkodó kódhoz a fordító támogatását és optimalizálási beállításait is ellenőrizni kell.

Ellenőrzési feltétel: a konfiguráció vagy igazolja a szükséges képességeket, vagy világosan elutasítja az adott buildet; nem helyettesíti a hiányzó szerződést puszta függvénynév-egyezéssel.

11.5.9 Továbbtanulás és összefoglalás

Elsődleges továbbtanulási kiindulópont a WG14 dokumentumgyűjteménye és a GNU C Library kézikönyve. Ezek itt kutatási kiindulópontok, nem konkrét C17-szakaszok ellenőrzött bizonyítékai. A szabványverziót és a használt libc dokumentációjának verzióját külön kell azonosítani.

A fejlécválasztás helyes sorrendje: feladat, nyelvi szerződés, opcionális képesség, implementációs támogatás, hibakezelés. Az assertion nem üzemi validáció; a széles karakter nem automatikusan Unicode; a numerikus hibajelzés nem kizárólag errno; a standard név pedig nem jelenti minden környezetben ugyanazt a rendelkezésre állást.