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.
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ó:
inline függvényt is megvalósíthat tényleges
hívással;inline nélkül definiált függvényt is
beilleszthet;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.
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.
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:
int vagy
long;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))
#endifA 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-clangAz 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 az adott előjeles típus maximumát, pedig a paramétert. Ha , a feltételes operátor nem értékeli ki az összeadást. Egyébként az egész értékek diszkrétsége miatt , tehát . 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 , 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ő.
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ó.
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.
A counter_inc(attempts++) esetén:
+(attempts++), de nem értékelődik
ki.counter_inc_int.attempts++ egyszer
értékelődik ki.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.
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.
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 -lmA -lm a megadott rendszer linkelési szokása, nem ISO C
fordítási követelmény.
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?
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.
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?
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.
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_uintAz 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.
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.
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.