10.3 static inline, _Generic és headeres eszköztár {#s048}

A headerbe írt segédfüggvény egyszerre nyelvi és fordításszervezési döntés. Nyelvi kérdés, hogy melyik fordítási egységhez tartozik a definíció, milyen kapcsolása van, és hogyan ellenőrizhetők az argumentumok. Ettől különálló optimalizálási kérdés, hogy a fordító létrehoz-e tényleges gépi függvényhívást.

A makrókról szóló rész megmutatta a szöveges helyettesítés veszélyeit; a függvényhívás tárgyalása pedig a típusos paraméterátadás előnyeit. Most ezeket kapcsoljuk össze: a static inline végzi a típusos műveletet, a _Generic pedig szükség esetén kiválasztja a megfelelő függvényt. A megoldás megértéséhez a nyelvi kapcsolás belső és külső változatának megkülönböztetésére is szükség lesz.

10.3.1 Az inline jelentése és a tényleges gépi beillesztés

ISO C17: az inline függvényspecifikátor. Nem új kapcsolási kategória, nem makróhelyettesítés, és nem kötelezi a fordítót arra, hogy a függvény törzsét a hívás helyére illessze.

A tényleges gépi beillesztés, azaz inlining, optimalizálói transzformáció. A fordító:

A döntést befolyásolhatja az optimalizálási profil, a függvénytörzs láthatósága, a kódméret és a célarchitektúra. Az inline tehát nem teljesítménygarancia. A hívási konvenció részleteit később, a hívási ABI tárgyalásakor vizsgáljuk.

10.3.1.1 C17 és GNU89: azonos íráskép, eltérő definíciós szabály

A következő táblázat fájlhatókörű függvénydefiníciókra vonatkozik. Feltételezzük, hogy ugyanabban a fordítási egységben nincs további, a helyzetet megváltoztató deklaráció.

Definíció alakja ISO C17 GNU89 inline-szemantika
static inline int f(int x) { ... } Belső kapcsolású függvény definíciója Szintén belső kapcsolású
inline int f(int x) { ... } Inline definíció; nem biztosít külső definíciót Külső definíciót biztosít
extern inline int f(int x) { ... } Külső definíciót biztosít Nem biztosítja a külön hívható külső definíciót

ISO C17: ha egy külső kapcsolású függvény összes fájlhatókörű deklarációja inline, és egyik sem tartalmaz extern specifikátort, akkor a definíciója inline definíció. Ez nem helyettesíti a programban szükséges külső definíciót. Ha a függvényt hívjuk, a teljes programnak biztosítania kell azt akkor is, ha egy adott optimalizált build látszólag nélküle is linkelhető.

Ezért veszélyes a headerben a puszta inline: optimalizálás nélkül előjöhet a hiányzó külső definíció miatti linkerhiba. Az extern inline vak hozzáadása sem általános javítás: C17-ben több fordítási egységben több külső definíciót eredményezhet.

GNU-fordítóbővítés: a GNU89-szabályokat ne azonosítsuk minden GNU nyelvi móddal. Például GCC-ben a -std=gnu17 önmagában nem jelent GNU89 inline-szemantikát; a -fgnu89-inline külön kapcsoló. A könyv példái explicit -std=c17 módot használnak, ilyen felülbírálás nélkül.

Külső kapcsolású inline definíciókra további C17-korlátozások is vonatkoznak: például nem hivatkozhatnak belső kapcsolású azonosítóra, és nem definiálhatnak módosítható statikus tárolási idejű objektumot. Egyszerű headeres segédfüggvényhez ezért általában a static inline a könnyebben indokolható választás.

10.3.2 Típusos headerhelper static inline használatával

A static itt belső kapcsolást ad. Minden beemelő fordítási egység saját függvénydefiníciót kap. Ez nem okoz azonos nevű külső szimbólumok közötti ütközést.

Készítsünk telítődő növelést számlálókhoz. A szerződés:

A példák ISO C17-et igényelnek. A parancsokhoz választható rögzített laborprofil: Linux x86-64 System V ABI, LP64, GCC 14.2.0 vagy Clang 19.1.7, glibc 2.40. A kód nem használ profilspecifikus típusszélességet. Az alábbiak fordítási és ellenőrzési utasítások, nem dokumentált futtatási eredmények.

A counter_util.h teljes tartalma:

#ifndef COUNTER_UTIL_H
#define COUNTER_UTIL_H

#include <limits.h>

static inline int counter_inc_int(int value)
{
    return value == INT_MAX ? INT_MAX : value + 1;
}

static inline long counter_inc_long(long value)
{
    return value == LONG_MAX ? LONG_MAX : value + 1L;
}

#define counter_inc(value) \
    _Generic(+(value),      \
        int: counter_inc_int, \
        long: counter_inc_long \
    )((value))

#endif

A main.c:

#include <assert.h>

#include "counter_util.h"

int main(void)
{
    int attempts = 3;

    int next = counter_inc(attempts++);
    assert(attempts == 4);
    assert(next == 4);

    assert(counter_inc(INT_MAX) == INT_MAX);
    assert(counter_inc(LONG_MAX) == LONG_MAX);
    assert(counter_inc(0L) == 1L);

    return 0;
}

Fordítás:

gcc -std=c17 -Wall -Wextra -Wpedantic -O0 main.c -o counter-gcc
clang -std=c17 -Wall -Wextra -Wpedantic -O2 main.c -o counter-clang

Az ellenőrzéshez a tanuló külön futtathatja a két programot. Elvárt tulajdonság, hogy az engedélyezett assert ellenőrzések teljesülnek.

Bizonyításvázlat a túlcsordulás kizárására. Jelölje MM az adott előjeles típus maximumát, xx pedig a paramétert. Ha x=Mx=M, a feltételes operátor nem értékeli ki az összeadást. Egyébként az egész értékek diszkrétsége miatt x≤M−1x\leq M-1, tehát x+1≤Mx+1\leq M. Az alsó korlátot a növelés nem sértheti meg. A művelet ezért minden érvényes bemeneti értékre definiált.

A műveleti költség mindkét ágon O(1)O(1), dinamikus allokáció és tárolóérvénytelenítés nélkül. A tényleges utasításszám ettől még fordító- és célfüggő.

10.3.2.1 A belső kapcsolás következményei

Két fordítási egységben a header azonos nevű static inline függvényei külön függvényeket jelölnek. Függvénycímüket ezért ne használjuk fordítási egységeken átívelő közös azonosítóként.

Ha ilyen függvénybe módosítható lokális static objektum kerül, annak állapota is külön példány lesz minden fordítási egységben. Ez meglepő hibát okozhat például „globális” hívásszámláló vagy cache megvalósításakor. Közös állapothoz vagy egyetlen közös függvényidentitáshoz inkább headerbeli deklaráció és egy .c fájlban elhelyezett definíció való.

10.3.3 Típusválasztás _Generic segítségével

ISO C17, C11 óta: a _Generic generikus választás (generic selection). Egy vezérlő kifejezés típusa alapján választ az asszociációk között:

/* Szintaxisvázlat, nem önálló program. */
_Generic(vezerlo_kifejezes,
    tipus_a: kifejezes_a,
    tipus_b: kifejezes_b,
    default: alapertelmezett_kifejezes)

A vezérlő kifejezés nem értékelődik ki. A kiválasztás típuskompatibilitáson alapul, nem az értéken vagy a típus nevének szövegén.

Az asszociációk típusai teljes, nem variábilisan módosított objektumtípusok lehetnek. Két kompatibilis típust nem sorolhatunk fel külön: például egy typedef név és az általa jelölt típus nem két külön választási lehetőség. Legfeljebb egy default adható meg. Ha nincs megfelelő típus és nincs default, az diagnózist igénylő korlátmegsértés.

A példánk lényegében ezt végzi:

/* Magyarázó részlet a fenti makró szerkezetéről. */
_Generic(+(value),
    int: counter_inc_int,
    long: counter_inc_long
)((value))

A kiválasztott kifejezés egy függvényjelölő; ezt követi a tényleges függvényhívás.

10.3.3.1 Miért nincs többszörös kiértékelés?

A counter_inc(attempts++) esetén:

  1. A vezérlő kifejezés +(attempts++), de nem értékelődik ki.
  2. Típusa alapján a választás counter_inc_int.
  3. A tényleges argumentumként szereplő attempts++ egyszer értékelődik ki.
  4. A függvény a növelés előtti érték másolatát kapja.

Az argumentum tehát kétszer szerepel a helyettesített szövegben, de csak egyszer kerül kiértékelésre. A szöveges előfordulások száma önmagában nem bizonyít kiértékelési hibát.

Az unáris + tudatos interfészdöntés: elvégzi az egész típusú promóciókat. A makró olyan aritmetikai kifejezést fogad el, amelynek típusa ezek után int vagy long. Így például a signed char is az int ághoz kerül; az unsigned int, a double és a pointer nem támogatott. A default hiánya itt védelmi eszköz.

A _Generic nem általános mechanizmus tetszőleges hibás kód elrejtésére. A ki nem választott asszociációkat is szintaktikailag és szemantikailag ellenőrizni kell. Ha például egy ágban (x)->field szerepel, attól az még hibás lehet, hogy az adott hívásnál másik ág választódna ki. A függvényjelölők kiválasztása ezt a gyakori tervezési csapdát elkerüli.

10.3.4 Makró vagy inline függvény: hol jelentkezik a költség?

A makró nem feltétlenül gyorsabb a függvénynél. Az optimalizáló egy látható függvénytörzsből gyakran ugyanazt a gépi kódot állíthatja elő. A valódi különbség elsősorban a szemantikai szerződés.

Tulajdonság Kifejezésmakró static inline függvény
Argumentumkezelés Szöveges helyettesítés Típusos paraméterátadás
Többszörös kiértékelés A makrótörzstől függ Argumentumonként egyszer
Tényleges hívás Nincs saját függvényhívása Elmaradhat vagy megmaradhat
Diagnosztika Helyettesítés után keletkezik A függvénytörzs külön ellenőrizhető
Header módosítása A fogyasztók újrafordítása szükséges Ugyanúgy szükséges

A függvény argumentumonkénti egyszeri kiértékelése nem rögzíti több argumentum egymáshoz viszonyított sorrendjét. Az f(i++, i++) C17-ben továbbra is hibás.

Mindkét headeres megoldás növelheti a fordítási munkát, a tényleges beillesztés pedig a gépi kód méretét és az utasításcache terhelését. Teljesítményállításhoz mérés szükséges, nem elegendő az inline szó jelenléte.

10.3.5 Könyvtári példa: tgmath.h

ISO C17: a <tgmath.h> típusgenerikus matematikai makrókat biztosít. Például a sqrt megfelelő argumentummal a sqrtf, sqrt vagy sqrtl műveletének felel meg. A könyvtár nem köteles ezt éppen _Generic használatával megvalósítani.

Önálló C17-példa, math_example.c:

#include <tgmath.h>

int main(void)
{
    float root = sqrt(9.0f);
    double power = pow(2.0f, 3);
    long double wide_root = sqrt(9.0L);

    (void)root;
    (void)power;
    (void)wide_root;
    return 0;
}

A pow(2.0f, 3) fontos részlet: a típusválasztásban az egész argumentum double választását eredményezi, ezért itt nem a powf műveletét kapjuk. Komplex argumentumoknál a megfelelő komplex művelet is kiválasztható, amennyiben az adott makróhoz létezik ilyen változat.

Linux/glibc profilban a matematikai könyvtár explicit linkelésével:

gcc -std=c17 -Wall -Wextra -Wpedantic -O2 \
    math_example.c -o math-example -lm

A -lm a megadott rendszer linkelési szokása, nem ISO C fordítási követelmény.

10.3.6 Feladatok

10.3.6.1 Magyarázó feladat: eltűnő linkerhiba

Egy header inline int twice(int x) { return x * 2; } definíciót tartalmaz, külső definíció sehol nincs. A hívások kis, túlcsordulást nem okozó értékeket adnak át. Miért nem bizonyítja a program helyességét, ha -O2 mellett linkelhető, -O0 mellett viszont nem?

10.3.6.2 Kódolási feladat: unsigned támogatás

Bővítsd a counter_inc interfészt unsigned int támogatással. A növelés UINT_MAX értéknél telítődjön. Ellenőrizd a nulla, a maximum előtti érték és a maximum esetét.

10.3.6.3 Diagnosztikai feladat: megismételt mellékhatás

Az alábbi hibás tervezésű makrónál induljon i értéke 3-ról:

/* Diagnosztikai példa: mellékhatásos argumentummal kerülendő. */
#define BAD_INC(x) ((x) == INT_MAX ? INT_MAX : (x) + 1)

/* Függvénytörzs részlete, <limits.h> szükséges. */
int i = 3;
int result = BAD_INC(i++);

Mennyi lesz i és result? UB történik-e? Mi változik a helyes counter_inc(i++) használatával?

10.3.7 Megoldások

10.3.7.1 A külső definíció hiánya

A puszta inline definíció C17-ben nem biztosít külső definíciót. Az optimalizáló eltüntetheti a gépi hívást, így elfedheti a linkelési tünetet, de nem javítja meg a programszerződést.

Headerhelper esetén javítás a static inline. Egyetlen közös külső függvényhez headerbeli prototípus és .c fájlbeli definíció használható. Ellenőrzési feltétel: mindkét optimalizálási profilban szabályos definíciós szerkezet és sikeres build.

10.3.7.2 Az unsigned ág

A headerbe kerülő C17-részlet:

static inline unsigned int counter_inc_uint(unsigned int value)
{
    return value == UINT_MAX ? UINT_MAX : value + 1u;
}

A generikus listához hozzáadandó:

/* Új asszociáció; a szomszédos elemeket vessző választja el. */
unsigned int: counter_inc_uint

Az ellenőrzések a main törzsében:

assert(counter_inc(0u) == 1u);
assert(counter_inc(UINT_MAX - 1u) == UINT_MAX);
assert(counter_inc(UINT_MAX) == UINT_MAX);

Az unsigned körbefordulás definiált lenne, de sértené a telítődő művelet szerződését. Ezért itt is szükséges a feltétel.

10.3.7.3 A makró mellékhatása

A feltétel először 3-at vizsgál, és i értékét 4-re növeli. A hamis ág újra kiértékeli az argumentumot: az értéke 4, majd i értéke 5 lesz. Az eredmény 4 + 1, tehát result == 5.

Ebben a konkrét esetben nincs UB: a feltételes operátor feltételének kiértékelése megelőzi a kiválasztott ágét. A hiba a kétszeri, nem kívánt mellékhatás.

A helyes változatnál i == 4 és result == 4. Ezek külön-külön assert állításokkal ellenőrizhetők.

10.3.8 Összefoglaló

A static inline típusos, belső kapcsolású headerhelper készítésére alkalmas; nem kényszeríti ki a gépi beillesztést. A külső kapcsolású C inline-szabályokat külön kell kezelni, különösen GNU89-kód átvételekor. A _Generic kiértékeletlen vezérlő kifejezéssel választ típust: függvényjelölők kiválasztásával egyszeri argumentumkiértékelésű interfész építhető. A <tgmath.h> ugyanezen típusgenerikus API-gondolat szabványos könyvtári példája.