A variadikus függvény (variadic function) változó számú argumentumot fogadhat. C-ben azonban a rugalmasságért a típusellenőrzés részleges elvesztésével fizetünk: a függvény nem kap automatikusan sem argumentumdarabszámot, sem futásidejű típusleírást. A hívónak és a hívottnak ezért külön szerződésben kell megállapodnia.
A legismertebb példa a printf: első argumentuma
egyszerre szöveg és a további argumentumokat leíró séma. Megértéséhez a
Függvényhívás, prototípus és paraméterátadás,
valamint a Promóciók, signedness és túlcsordulás
szabályaira építünk. Az alábbi nyelvi magyarázat ISO C17-re vonatkozik;
a fordítói attribútumot külön bővítésként kezeljük.
A prototípus végén álló ..., azaz ellipszis jelzi a
változó argumentumlistát:
/* C17 deklarációrészlet. */
int count_positive(int count, ...);C17-ben az ellipszist legalább egy névvel rendelkező paraméternek kell megelőznie. A rögzített paraméterekre a prototípus szerinti ellenőrzés és konverzió érvényes. Az ellipszishez tartozó argumentumokhoz viszont nincs deklarált céltípus.
A fenti API szerződése lehet:
count nemnegatív;count darab, promóció után
int típusú argumentum következik;count esetén az eredmény -1,
argumentumolvasás nélkül.A fordító a deklarációból nem tudja ellenőrizni a második feltételt.
A count csak programadat: nem köti automatikusan a nyelv a
tényleges argumentumlistához.
A variadikus függvény darabszám helyett használhat formátumsztringet,
lezáró jelzőértéket vagy külön típusleírókat. Mindegyik külső
protokoll, nem a ... által szolgáltatott
információ.
Az ellipszisnek átadott argumentumokra az alapértelmezett argumentumpromóciók (default argument promotions) vonatkoznak:
| Eredeti típus | Átadott típus |
|---|---|
float |
double |
_Bool, signed char,
short |
int |
char, unsigned char,
unsigned short |
int, ha minden értékük ábrázolható benne; különben
unsigned int |
int, unsigned int, long,
double |
Változatlan |
A char esetén tehát az implementáció által meghatározott
előjelességet és az értéktartományt is figyelembe kell venni.
Fontos, hogy az int nem változik
automatikusan double-lé attól, hogy a formátumban
%f szerepel. A formátumsztring nem vesz részt a nyelvi
argumentumkonverzióban.
/* C17 részlet; környező függvény és <stdio.h> szükséges. */
float rate = 1.5f;
signed char delta = -2;
printf("rate=%.1f delta=%d\n", rate, delta);A hívás lépései:
rate értéke float típusból
double típusra promoválódik.delta értéke signed char típusból
int típusra promoválódik.printf a %f alapján double,
a %d alapján int argumentumot vár.Gyakori formátumszerződések:
| Formátum | Elvárt argumentum |
|---|---|
%d |
int |
%u |
unsigned int |
%ld |
long |
%zu |
size_t |
%f |
double |
%Lf |
long double |
%s |
Karaktertömb elejére mutató pointer; ebben a fejezetben nullterminált sztringet követelünk meg |
%p |
void * |
A printf esetén a %lf ugyanúgy
double argumentumot kezel, mint a %f; ezt nem
szabad más formázási API-kra automatikusan általánosítani.
A %p esetén objektumpointert explicit
(void *) konverzióval adj át: az ellipszis önmagában nem
alakítja például az int * argumentumot void *
típusúvá. A *-gal megadott mezőszélesség és pontosság
külön-külön egy int argumentumot fogyaszt.
A <stdarg.h> biztosítja a változó argumentumlista
bejárását. A va_list implementációfüggő bejárási állapot,
nem hordozható „pointer a veremre”.
| Művelet | Jelentés és feltétel |
|---|---|
va_start(ap, last) |
Inicializálja a bejárást; last az utolsó név szerinti
paraméter |
va_arg(ap, T) |
Kiolvassa a következő argumentumot T típussal, és
továbblépteti az állapotot |
va_copy(dst, src) |
Önálló bejárási állapotot készít src aktuális
pozíciójáról |
va_end(ap) |
Lezárja az adott inicializált állapot használatát |
Minden va_start és va_copy művelethez a
megfelelő va_end tartozik, ugyanabban a függvényben. Ez
korai visszatérésnél is kötelező. A va_end nem szabadítja
fel az argumentumként átadott erőforrásokat.
C17-ben a va_start utolsó név szerinti paramétere nem
lehet register deklarációjú, tömb- vagy függvénytípusú,
illetve a saját promoválódott típusával inkompatibilis típusú. Ezért
például float, char vagy short
helyett célszerű int-et, illetve megfelelő
pointerparamétert használni.
A va_list másolására ne használj egyszerű értékadást
vagy memcpy-t: egyes implementációkon tömbtípus, máshol
összetettebb állapot. A hordozható művelet a va_copy.
Ha egy meghívott segédfüggvény va_arg-gal fogyasztja az
átadott listát, a hívó nem feltételezheti annak változatlan pozícióját.
Megőrzéshez előzetesen külön másolatot kell készíteni.
Az alábbi teljes C17 fordítási egység két szerződést mutat be. A
count_positive darabszám alapján olvas, a
mirror_log pedig ugyanazt a formázott üzenetet két streamre
írja.
A laborhoz választott profil: Linux x86-64 System V, GCC 14.2.0, glibc 2.40. Az attribútum nélküli mag ISO C17; az attribútum GCC/Clang-bővítés. A példa itt nem fordított vagy futtatott mérési eredmény.
/* variadic_demo.c */
#include <stdarg.h>
#include <stdio.h>
#if defined(__GNUC__) || defined(__clang__)
#define PRINTF_LIKE(fmt, args) \
__attribute__((format(printf, fmt, args)))
#else
#define PRINTF_LIKE(fmt, args)
#endif
static int count_positive(int count, ...)
{
if (count < 0) {
return -1;
}
va_list ap;
va_start(ap, count);
int result = 0;
for (int i = 0; i < count; ++i) {
int value = va_arg(ap, int);
if (value > 0) {
++result;
}
}
va_end(ap);
return result;
}
static int mirror_log(FILE *first, FILE *second,
const char *format, ...)
PRINTF_LIKE(3, 4);
static int mirror_log(FILE *first, FILE *second,
const char *format, ...)
{
va_list ap;
va_list copy;
va_start(ap, format);
va_copy(copy, ap);
int a = vfprintf(first, format, ap);
int b = vfprintf(second, format, copy);
va_end(copy);
va_end(ap);
return (a < 0 || b < 0) ? -1 : 0;
}
int main(void)
{
float rate = 1.5f;
signed char delta = -2;
int positives = count_positive(4, -3, 0, 7, 2);
int status = mirror_log(
stdout, stderr,
"rate=%.1f delta=%d positive=%d\n",
rate, delta, positives
);
return status < 0 ? 1 : 0;
}Fordítás a kiválasztott GCC-vel:
gcc -std=c17 -Wall -Wextra -Wpedantic -Wformat=2 \
-O0 -g variadic_demo.c -o variadic_demoA számlálás várt eredménye 2. A naplózó mindkét streamre
a rate=1.5 delta=-2 positive=2 sort próbálja kiírni. A két
stream megfigyelhető sorrendjére ebből nem következik garancia.
A másolatot az első fogyasztás előtt készítjük. A
vfprintf után az adott lista nem használható újra következő
bejárásként; a megfelelő va_end azonban szükséges.
A kölcsönzési szerződés:
| Erőforrás vagy tároló | Tulajdonos és élettartam | Érvénytelenítés, hibakimenet |
|---|---|---|
| A két stream | A hívó/környezet kezeli; a naplózó csak kölcsönzi őket | A hívás alatt nyitott, bájtos kimenetre használható streamek szükségesek |
| Formátumsztring | A hívó biztosítja; a példában statikus élettartamú sztringliterál | A hívás végéig érvényes és változatlan |
Esetleges %s adatok |
A hívó tulajdona; a naplózó nem tárolja el a pointereket | Felszabadítás vagy érvénytelen sztring szerződéssértés, nem megbízható hibakód |
Két va_list állapot |
A mirror_log kezeli |
Saját va_end zárja le mindkettőt |
A mirror_log -1 értéke valamelyik
vfprintf hibáját jelzi. Részleges kimenet már
létrejöhetett, visszagörgetés nincs. A siker sem jelent tartós tárolást
vagy későbbi pufferürítési hibák kizárását.
A va_arg(ap, T) nem típuskonverziót kér: azt állítja,
hogy a következő argumentum megfelelő típusú.
/* Szándékosan hibás C17 részletek; nem futtatandók. */
printf("%f\n", 1); /* int helyett double kellene */
printf("%ld\n", 1); /* int helyett long kellene */
count_positive(3, 10, 20); /* nincs harmadik olvasható érték */Ezek nem definiált viselkedéshez, UB-hez vezetnek.
Nincs garantált kimenet, és a látszólagos működés sem bizonyít
helyességet. Hasonlóan hibás a va_arg(ap, float), ha a hívó
float kifejezést adott át: az ténylegesen
double-ként érkezett.
Az ISO C17 va_arg-szabályának vannak szűk kivételei:
megfelelő signed/unsigned típuspárnál, ha az érték mindkettőben
ábrázolható, illetve void * és karaktertípusra mutató
pointer párosánál. Ezekből nem következik általános típusátjárhatóság,
és nem írják felül a printf saját formátumszerződését.
A fel nem használt argumentum önmagában nem UB: a
printf("%d\n", 1, 2) a második értéket figyelmen kívül
hagyja. A túl kevés argumentum viszont veszélyes, mert a formátum nem
létező adat kiolvasását kérheti.
A példabeli format(printf, 3, 4) attribútum jelentése: a
harmadik paraméter formátumsztring, az ellenőrzendő argumentumok a
negyediktől kezdődnek. Ez nem módosítja az átadást, csak diagnosztikai
információt ad a fordítónak. Nem ISO C17-képesség, és nem általános
futásidejű védelem.
Különösen fontos, hogy külső szöveg ne váljon formátummá. A
printf(input) helyett printf("%s", input)
szükséges, feltéve, hogy input érvényes, nullterminált
sztringre mutat. A sztring tárolójának a teljes hívás alatt élnie
kell.
Homogén adatokhoz rendszerint jobb a pointer–darabszám interfész:
/* C17 deklarációrészlet; <stddef.h> szükséges. */
size_t count_positive_array(const int *values, size_t count);Itt a fordító ellenőrzi az elemtípust, bár a tömbhatárt továbbra is a
szerződés rögzíti. A függvény kölcsönzi a tömböt, nem tárolja el és nem
szabadítja fel. count > 0 esetén legalább
count olvasható int szükséges;
count == 0 esetén megengedhető a null pointer.
Eltérő típusú adatokhoz névvel ellátott struktúra vagy típusjelölős
union-elemek tömbje használható. Ezek hosszabb
deklarációért cserébe ellenőrizhetőbb interfészt adnak. Variadikus API-t
akkor érdemes választani, ha a rugalmasság valóban indokolja a rejtett
szerződés költségét.
Magyarázó: Milyen típusként érkezik a példában
rate és delta? Miért nem javítaná ki a
%f formátum egy közvetlenül átadott 1
argumentum típusát?
Kódolási: Valósítsd meg a
count_positive_array függvényt. Üres bemenetre fogadjon el
null pointert, és ne dereferálja azt.
Diagnosztikai: Miért hibás a
mirror_log, ha a második vfprintf is
ap-t kap? Mi történik, ha a va_copy csak az
első vfprintf után kerülne? Melyik korábbi hibát várhatjuk
formátumellenőrzéstől, és melyiket nem?
A rate double, a delta
int típusként érkezik. A formátum feldolgozása a hívott
függvényben történik; az argumentumkonverzió már a hívás részeként
megtörtént. Az 1 ezért int marad.
Ellenőrzési feltétel: a javítás például
printf("%f\n", 1.0) legyen; ne pusztán a warning
elnyomása.
Önálló C17 fordítási egység:
/* positive_array.c */
#include <stddef.h>
size_t count_positive_array(const int *values, size_t count)
{
size_t result = 0;
for (size_t i = 0; i < count; ++i) {
if (values[i] > 0) {
++result;
}
}
return result;
}gcc -std=c17 -Wall -Wextra -Wpedantic \
-c positive_array.c -o positive_array.oBizonyításvázlat: minden iteráció előtt
result az első i elem pozitív értékeinek
száma, ezért
.
Az eredmény nem csordul túl. Üres bemenetnél a ciklustörzs nem fut.
Ellenőrzési feltétel: (NULL, 0)
eredménye 0; a {-3, 0, 7, 2} tömbé
2. A művelet
idejű, folytonos hozzáférésű, nem allokál és nem érvénytelenít
tárolót.
Az első vfprintf elfogyasztja a bejárási állapotot;
ugyanazzal nem indítható újra a lista. Utólagos másolása sem állítja
vissza a kezdetet. A javítás az előzetes va_copy, majd
mindkét állapot lezárása.
Ellenőrzési feltétel: minden inicializált lista
minden visszatérési úton kapjon va_end hívást. A literális
formátumhoz adott hibás típusra a fordító figyelmeztethet; a
count_positive(3, 10, 20) saját darabszámszerződését a
formátumellenőrzés nem ismeri.
A variadikus C API típus- és darabszámszerződését a programozó
biztosítja. A promóciókat az argumentum eredeti típusa, nem a formátum
vezérli. A va_list szabályos inicializálást,
va_copy alapú másolást és lezárást igényel. A fordítói
formátumellenőrzés hasznos védőháló, de ahol lehet, a típusos tömb- vagy
struktúrainterfész erősebb alapot ad.