Variadické funkce
Variadické funkce jsou funkce s proměnným počtem parametrů. Variadické funkce mají povinné argumenty, těch je v každém zavolání stejný počet, a volitelné argumenty, jejichž počet se může mezi voláními může lišit. Každá variadická funkce musí mít alespoň jeden povinný argument.
Zjištění počtu a typu volitelných argumentů uvnitř funkce se neděje automaticky, řeší se konvencí. Existují v zásadě tři přístupy.
-
Předpokládáme pevný typ volitelných argumentů, v povinném argumentu předáváme jejich počet.
//hlavicka double sum_of_doubles(int count, ...); // hlavicka // volani sum_of_doubles(3, 1.2, 2.2, 3.3); // vysledek je 6.6 sum_of_doubles(2, 2.1, 3.3); // vysledek je 5.4 -
Předpokládáme pevný typ argumentů, ale poslední nepovinný argument má speciální hodnotu, například 0.
//hlavicka int sum_of_ints(int first, ...); // hlavicka // volani sum_of_ints(1, 2, 3, 0); // vysledek je 6 sum_of_ints(2, 2, 0); // vysledek je 4 -
Informaci o typech i počtu volitelných argumentů předáme pomocí povinných argumentů. Tento přístup používá například funkce
printf.
K nepovinným argumentům přistupujeme pomocí maker z stdarg.h.
- typ
va_listpro udržování nezpracovaných argumentů, va_startnastavíva_listna všechny volitelné argumenty,va_argvrátí hodnotu prvního argumentu veva_list, potřebuje znát typ, první argument odstraní zva_list.va_endukončí práci s nepovinnými argumenty.
Podrobnosti v následujících příkladech.
double sum_of_doubles(int n, ...)
{
va_list args;
double ret = 0;
va_start(args, n); // tady se musi predat va_list a posledni povinny argument
for (int i = 0; i < n; i += 1)
{
ret += va_arg(args, double); // tady se musi predat va_list a typ argumentu
}
va_end(args); // ukonceni prace s argumenty
return ret;
}
//
// nutno volat aspon dvema argumenty !!!
// jinak UB
//
int sum_of_ints(int first, ...)
{
va_list args;
int ret = first;
va_start(args, first);
while (1)
{
int val = va_arg(args, int);
if (!val) break;
ret += val;
}
va_end(args);
return ret;
}
void almost_printf(char *format, ...)
{
va_list args;
int index = 0;
va_start(args, format);
while(format[index])
{
switch(format[index])
{
case 'i':
printf("%i ", va_arg(args,int));
break;
case 'd':
printf("%f ", va_arg(args,double));
break;
}
index += 1;
}
va_end(args);
}
Při předávání variadických argumentů dochází k automatické konverzi typů:
float je konvertován na double, u celočíselných typů dochází k
celočíselné promoci.
Pointery na funkce
S pomocí pointerů na funkce lze v omezené míře pracovat s funkcemi
vyššího řádu.
Pointer na funkci sebou nese i typové informace: typ návratové hodnoty
a počet a typy argumentů. Zápis tohoto typu podobný jako zápis hlavičky
funkce bez jmen argumentů, pouze jméno funkce nahradíme (*).
int (*)(int*, int) // typ pointer na fci, ktera vraci int,
// jeji argumenty jsou typu int* a int.
Při definici proměnné se identifikátor píše dovnitř závorky za *.
int (*fce_ptr)(int*, int); // definice pointeru
Předchozí syntax je trochu nešikovná, proto se často používá
typedef.
typedef int (*p_fun)(int* , int); // typ se jmenuje p_fun
p_fun ptr; // definice promenne
nebo alternativně
typedef int fun_type(int *, int); // definujeme typ pro funkci, nikoliv pointer na funkci
fun_type* ptr;
S pointery na funkce nelze provádět aritmetiku, ani aplikovat operátory adresy a dereference (resp, tyto operátory nedělají nic). Můžeme ovšem funkci volat, syntax volání je přitom stejná jako u běžného volání funkce.
Podobně jako u polí, jméno funkce degeneruje na pointer na funkci.
typedef int p_fun(int* , int);
int array_sum(int *array, int n) {
int r = 0;
for (int i = 0; i < n; i += 1) {
r += array[i];
}
return r;
}
int array_product(int *array, int n) {
int r = 1;
for (int i = 0; i < n; i += 1) {
r *= array[i];
}
return r;
}
int main() {
int a[] = { 1, 2, 3, 4, 5 };
int n = sizeof(a)/sizeof(*a);
p_fun* reduce = array_sum; // reduce ukazuje na funkci array_sum
printf("%i\n", reduce(a, n)); // vytiskne 15, vola array_sum
reduce = array_product;
printf("%i\n", reduce(a, n)); // vytiskne 120, vola array_product
return 0;
}
S pointery na funkce lze pracovat jako s jinými typy v C. Lze tvořit jejich pole, předávat je do funkcí jako argumenty, mohou být návratovými hodnotami funkcí atd.
Pointer na funkci může být neplatný, v tom případě vede pokus o volání k nedefinovanému chování. Pointeru na funkci je možné přiřadit 0 a pointery na funkce lze porovnávat.
Jedno z využití pointerů na funkce je tvorba analogie generických funkcí známých z jiných jazyků. Ideou je, že funkci naprogramujeme jednou a ona poté pracuje pro více typů. Ukážeme si to na příkladu binárního vyhledávání.
typedef int compare_fn(void *, void *); // argumenty jsou void*, aby byla funkce obecna
int compare_int(void *a, void *b)
{
int *aa = a; // tuto funkci budeme volat pouze s int*,
int *bb = b; // muzeme bezpecne pretypovat
if (*aa < *bb) return -1;
if (*bb < *aa) return 1;
return 0;
}
int compare_long(void *a, void *b)
{
long* aa = a;
long* bb = b;
if (*aa < *bb) return -1;
if (*bb < *aa) return 1;
return 0;
}
//
// genericka funkce pro binarni vyhledavani
//
int binary_search(void *array, // pole, ve kterem vyhledavame
void *key, // ukazatel na hodnotu klice
size_t n, // pocet prvku pole
size_t element_size, // velikost jednoho prvku v bajtech
compare_fn* cmp) // funkce pro porovnani dvou prvku
{
unsigned char *bytes = array;
size_t lo = 0;
size_t hi = n; // polouzavreny interval [lo, hi)
while (lo < hi)
{
size_t mid = lo + (hi - lo) / 2;
int cmp_result = cmp(key, bytes + mid * element_size);
if (cmp_result == 0) { return (int)mid; }
else if (cmp_result > 0) { lo = mid + 1; }
else { hi = mid; }
}
return -1;
}
//
// pro jednotlive typy vytvorem funkce tvorici rozhrani
//
int binary_search_int(int* array, int key, size_t n)
{
return binary_search(array, &key, n, sizeof(int), compare_int);
}
int binary_search_long(long* array, long key, size_t n)
{
return binary_search(array, &key, n, sizeof(long), compare_long);
}
int main()
{
int array_int[20];
long array_long[20];
for(int i = 0; i < 20; i += 1)
{
array_int[i] = i;
array_long[i] = i;
}
int key_int_ok = 10;
int key_int_fail = 30;
long key_long_ok = 5;
long key_long_fail = -3;
printf("index of %i in array_int is %i\n",
key_int_ok,
binary_search_int(array_int, key_int_ok, 20));
printf("index of %i in array_int is %i\n",
key_int_fail,
binary_search_int(array_int, key_int_fail, 20));
printf("index of %li in array_long is %i\n",
key_long_ok,
binary_search_long(array_long, key_long_ok, 20));
printf("index of %li in array_long is %i\n",
key_long_fail,
binary_search_long(array_long, key_long_fail, 20));
return 0;
}
Úkoly
-
Navrhněte strukturu pro komplexní číslo a naprogramujte variadickou funkci pro výpočet sumy komplexních čísel využívající prvního přístupu k určení počtu volitelných argumentů.
-
Napište funkci
long double prumer(char* format, ...), která výpočítá aritmetický průměr ze zadaných hodnot různých datových typů. Typy předávaných hodnot jsou určeny pomocí parametruformat, který může tvořit libovolná posloupnost znaků odpovídající typům následujících parametrů -ipro typint,dpro typdoublealpro long double. -
Napište funkci
double* map(double (*fce)(double), double* input, int len), která na hodnoty poleinputaplikuje funkcifcea vrátí pole výsledných hodnot. Velikost poleinputparametremlen. -
Napište funkci
double akumulator(double (*fce)(double, double), double numbers[], int len), která zpracuje pomocí předané funkcefcehodnoty z polenumbers, jehož velikost je dána parametremlen. Můžete předpokládat, želenje vždy alespoň 2. -
S použitím příkladu z kapitoly vytvořte funkci pro binární vyhledávání zlomků.
-
Do následujícího kódu doprogramujte generickou funkci pro třídění a s její pomocí funkce pro třídění zlomků a
unsigned. (Můžete si doprogramovat libovolné pomocné funkce.) Je zakázáno použít třídicí funkci ze standardní knihovny.typedef struct { int numerator; int denominator; } Fraction; typedef int compare_fn(void*, void*); int compare_fraction(void *a, void *b) { // TODO } int compare_unsigned(void *a, void *b) { // TODO } void generic_sort(void *array, size_t n, size_t element_size, compare_fn* cmp) { // TODO } void fraction_sort(Fraction *array, int n) { // TODO } void unsigned_sort(unsigned int* array, int n) { // TODO } -
Naprogramujte vlastní verzi funkce
printf, která s podporou tiskových direktiv%i,%fa%z, kde poslední direktiva vede k tisku zlomku. Při řešení lze využít funkciprintf. -
Naprogramujte jednoduchou kalkulačku, která operaci vybírá indexem do pole pointerů na funkce, nikoliv pomocí
switchnebo řetězuif.typedef enum { OP_ADD, OP_SUB, OP_MUL, OP_DIV, OP_COUNT } Operation; double apply(Operation op, double a, double b);Funkci
applynapište tak, aby obsahovala poleOP_COUNTpointerů na funkce a operaci provedla jediným zavoláním. Ošetřete dělení nulou. Porovnejte výsledný kód s variantou napsanou pomocíswitcha rozmyslete si, kdy je která přehlednější. -
Naprogramujte funkci
int filter(int *input, int n, int *output, int (*predicate)(int));která do pole
outputzkopíruje ty prvkyinput, pro kterépredicatevrátí nenulovou hodnotu, a vrátí jejich počet. Otestujte ji s predikáty pro sudost, prvočíselnost a zápornost. -
Naprogramujte funkci
typedef double binary_op(double, double); binary_op *get_operation(char symbol);která pro znak
'+','-','*'a'/'vrátí pointer na odpovídající funkci a pro jiný znak vrátí 0. Volající musí návratovou hodnotu zkontrolovat, než ji zavolá.
Řešení úkolů
Úkol 1
Navrhněte strukturu pro komplexní číslo a naprogramujte variadickou funkci pro výpočet sumy komplexních čísel využívající prvního přístupu k určení počtu volitelných argumentů.
typedef struct
{
double re;
double im;
} Complex;
//
// prvni pristup: pocet volitelnych argumentu je v povinnem argumentu
//
Complex complex_sum(int count, ...)
{
Complex ret = { 0, 0 };
va_list args;
va_start(args, count);
for (int i = 0; i < count; i += 1)
{
Complex c = va_arg(args, Complex);
ret.re += c.re;
ret.im += c.im;
}
va_end(args);
return ret;
}
Volání využívá složených literálů:
Complex s = complex_sum(3, (Complex){1,2}, (Complex){3,-1}, (Complex){0.5,0.5});
// s je 4.5 + 1.5i
Strukturu lze variadickou funkcí předat, protože se na ni nevztahují
automatické konverze — do va_arg se předá přesně ten typ, který jsme
poslali. U typů menších než int nebo u float by to takto nefungovalo,
viz konec sekce o variadických funkcích.
Kdyby uživatel předal jiný počet argumentů, než slibuje count, šlo by
o nedefinované chování. Variadická funkce nemá jak si to ověřit.
Úkol 2
Napište funkci long double prumer(char *format, ...), kde i značí
int, d značí double a l značí long double.
long double prumer(char *format, ...)
{
long double sum = 0;
int n = 0;
va_list args;
va_start(args, format);
for (int i = 0; format[i]; i += 1)
{
switch (format[i])
{
case 'i': sum += (long double)va_arg(args, int); n += 1; break;
case 'd': sum += (long double)va_arg(args, double); n += 1; break;
case 'l': sum += va_arg(args, long double); n += 1; break;
default: break; // neznamy znak ignorujeme
}
}
va_end(args);
return n ? sum / n : 0;
}
// prumer("idl", 2, 3.0, (long double)4.0) -> 3.0
Typ ve va_arg musí přesně odpovídat tomu, co bylo předáno. Kdybychom
u 'l' napsali va_arg(args, double), přečetli bychom nesprávný počet
bajtů a všechny další argumenty by se rozsypaly.
Při volání je literál 4.0 typu double, nikoliv long double.
Pro direktivu 'l' je potřeba psát 4.0L nebo přetypovat.
Úkol 3
Napište funkci double *map(double (*fce)(double), double *input, int len).
double *map(double (*fce)(double), double *input, int len)
{
double *out = malloc((size_t)len * sizeof(double));
assert(out);
for (int i = 0; i < len; i += 1) { out[i] = fce(input[i]); }
return out;
}
// pouziti
static double twice(double x) { return 2 * x; }
double in[] = { 1, 2, 3 };
double *out = map(twice, in, 3); // { 2, 4, 6 }
free(out); // uvolnuje volajici!
Zápis double (*fce)(double) je pointer na funkci, která bere i vrací
double. Závorky kolem *fce jsou nutné — double *fce(double) by byla
deklarace funkce vracející double*.
Při volání se píše jenom jméno funkce, bez &: jméno funkce degeneruje
na pointer stejně jako jméno pole.
Funkce alokuje nové pole, takže ho volající musí uvolnit. Alternativou by bylo nechat cílové pole předat jako parametr.
Úkol 4
Napište funkci double akumulator(double (*fce)(double, double), double numbers[], int len).
double akumulator(double (*fce)(double, double), double numbers[], int len)
{
double acc = numbers[0];
for (int i = 1; i < len; i += 1) { acc = fce(acc, numbers[i]); }
return acc;
}
static double add(double a, double b) { return a + b; }
static double maxd(double a, double b) { return a > b ? a : b; }
double nums[] = { 3, 1, 4, 1, 5 };
akumulator(add, nums, 5); // 14
akumulator(maxd, nums, 5); // 5
Jde o klasickou operaci fold: první prvek je počáteční hodnota a funkce
se postupně aplikuje na dosavadní výsledek a další prvek.
Právě proto zadání říká, že len je aspoň 2 — pro prázdné pole bychom
neměli co vrátit a museli bychom počáteční hodnotu dostat jako další
parametr.
Úkol 5
S použitím příkladu z kapitoly vytvořte funkci pro binární vyhledávání zlomků.
Generická funkce binary_search z výkladu zůstává beze změny. Stačí
dodat porovnávací funkci a obal:
int compare_fraction(void *a, void *b)
{
Fraction *x = a;
Fraction *y = b;
//
// x/xd < y/yd prave kdyz x*yd < y*xd (kladne jmenovatele)
// nasobime v long, aby soucin nepretekl
//
long left = (long)x->numerator * y->denominator;
long right = (long)y->numerator * x->denominator;
return (left > right) - (left < right);
}
int binary_search_fraction(Fraction *array, Fraction key, size_t n)
{
return binary_search(array, &key, n, sizeof(Fraction), compare_fraction);
}
Zlomky nelze porovnávat po položkách; musíme je převést na společného
jmenovatele. Křížové násobení je přesnější než převod na double,
který by u velkých čísel ztrácel přesnost.
Výraz (left > right) - (left < right) vrací −1, 0 nebo 1 bez větvení.
Zápis return left - right; by byl chybný: rozdíl může přetéct.
Přetypování na long před násobením je podstatné.
(long)(x->numerator* y->denominator) by nepomohlo — součin by se spočítal v int
a přetekl by ještě před konverzí.
Klíč se předává adresou (&key), protože binary_search pracuje
s void*. Parametr key je lokální kopie, takže je to bezpečné.
Úkol 6
Doprogramujte generickou funkci pro třídění a s její pomocí funkce
pro třídění zlomků a unsigned.
void generic_sort(void *array, size_t n, size_t element_size, compare_fn *cmp)
{
unsigned char *bytes = array;
//
// jeden prvek si musime umet odlozit, velikost zname az za behu
//
unsigned char *tmp = malloc(element_size);
assert(tmp);
//
// insertion sort
//
for (size_t i = 1; i < n; i += 1)
{
memcpy(tmp, bytes + i * element_size, element_size);
size_t j = i;
while (j > 0 && cmp(bytes + (j-1) * element_size, tmp) > 0)
{
memcpy(bytes + j * element_size, bytes + (j-1) * element_size, element_size);
j -= 1;
}
memcpy(bytes + j * element_size, tmp, element_size);
}
free(tmp);
}
int compare_unsigned(void *a, void *b)
{
unsigned int x = *(unsigned int*)a;
unsigned int y = *(unsigned int*)b;
return (x > y) - (x < y);
}
void fraction_sort(Fraction *array, int n)
{
generic_sort(array, (size_t)n, sizeof(Fraction), compare_fraction);
}
void unsigned_sort(unsigned int *array, int n)
{
generic_sort(array, (size_t)n, sizeof(unsigned int), compare_unsigned);
}
Protože velikost prvku známe až za běhu, nemůžeme si odkládací proměnnou
deklarovat — musíme ji alokovat. Prohazování prvků probíhá přes memcpy
nad bajty.
U compare_unsigned nelze psát return x - y;. Odčítání dvou
unsigned int je modulární, takže pro x < y vyjde obrovské kladné
číslo a třídění by bylo úplně špatně.
Poznámka k typu compare_fn. V zadání je definován jako typ funkce,
nikoliv pointeru:
typedef int compare_fn(void*, void*); // typ funkce
compare_fn *cmp; // proto se pise hvezdicka
Ve výkladu se objevuje i varianta typedef int (*compare_fn)(void*, void*),
kde už je pointer součástí typu a píše se pak jenom compare_fn cmp.
Obojí je správně, jenom je potřeba vědět, který zápis zrovna používáme.
Úkol 7
Naprogramujte vlastní verzi printf s podporou direktiv %i, %f
a %z, kde poslední vede k tisku zlomku.
typedef struct { int numerator; int denominator; } Fraction;
void my_printf(char *format, ...)
{
va_list args;
va_start(args, format);
for (int i = 0; format[i]; i += 1)
{
if (format[i] != '%') { putchar(format[i]); continue; }
i += 1; // preskocime '%'
switch (format[i])
{
case 'i':
printf("%i", va_arg(args, int));
break;
case 'f':
printf("%f", va_arg(args, double));
break;
case 'z':
{
Fraction f = va_arg(args, Fraction);
printf("%i/%i", f.numerator, f.denominator);
break;
}
case '%':
putchar('%');
break;
case 0:
//
// format koncil znakem '%', vratime se
// aby vnejsi cyklus korektne skoncil
//
i -= 1;
break;
default:
putchar('%');
putchar(format[i]);
break;
}
}
va_end(args);
}
Direktivy %i a %f jenom předáme skutečnému printf, jak zadání
dovoluje. Zajímavé je %z, kde si strukturu vytáhneme a naformátujeme
sami.
Případ case 0 je snadné přehlédnout: pokud řetězec končí osamoceným
%, zvýšili jsme i za konec řetězce a bez ošetření bychom četli mimo
pole.
Větev pro %f je dobrou ilustrací automatických konverzí: i kdyby
volající předal float, přijde do funkce jako double, takže
va_arg(args, double) je správně a va_arg(args, float) by bylo špatně.
Úkol 8
Naprogramujte kalkulačku, která operaci vybírá indexem do pole pointerů
na funkce, nikoliv pomocí switch nebo řetězu if.
static double op_add(double a, double b) { return a + b; }
static double op_sub(double a, double b) { return a - b; }
static double op_mul(double a, double b) { return a * b; }
static double op_div(double a, double b) { return b == 0.0 ? 0.0 : a / b; }
double apply(Operation op, double a, double b)
{
//
// poradi prvku MUSI odpovidat poradi konstant ve vyctu
//
static double (*table[OP_COUNT])(double, double) = { op_add, op_sub, op_mul, op_div };
assert(op >= 0 && op < OP_COUNT);
return table[op](a, b);
}
Zápis double (*table[OP_COUNT])(double, double) se čte zevnitř ven:
table je pole OP_COUNT pointerů na funkci beroucí dva double
a vracející double. Závorky kolem *table[...] jsou nutné.
Pole je static, aby se neinicializovalo znovu při každém volání.
Jeho velikost je OP_COUNT, takže po přidání nové operace do výčtu
překladač ohlásí, že v inicializátoru chybí prvek.
Dělení nulou ošetřujeme uvnitř op_div. Celočíselné dělení nulou je
nedefinované chování; u double bychom sice dostali inf, ale vracet
nulu je tu čitelnější.
Kontrola assert je podstatná: op je typu Operation, ale nic
nebrání někomu přiřadit do něj (Operation)99 a index mimo pole by byl
nedefinované chování.
Varianta se switch by byla zhruba stejně dlouhá a pro čtyři operace
možná i čitelnější. Tabulka vyhrává tehdy, když je operací hodně, když
se sestavuje za běhu, nebo když se má dát rozšířit bez zásahu
do apply.
Úkol 9
Naprogramujte funkci filter s predikátem předaným jako pointer
na funkci.
int filter(int *input, int n, int *output, int (*predicate)(int))
{
int count = 0;
for (int i = 0; i < n; i += 1)
{
if (predicate(input[i]))
{
output[count] = input[i];
count += 1;
}
}
return count;
}
Predikáty pro otestování:
static int is_even(int x) { return x % 2 == 0; }
static int is_negative(int x) { return x < 0; }
static int is_prime(int x)
{
if (x < 2) { return 0; }
for (int d = 2; d * d <= x; d += 1) { if (x % d == 0) { return 0; } }
return 1;
}
Index i prochází vstup, count
ukazuje na první volné místo ve výstupu — a protože se zvyšuje jenom
při úspěchu, jsou výsledky ve výstupním poli za sebou bez mezer.
Zápis if (predicate(...)) stačí; porovnávat s nulou není potřeba,
protože zadání mluví o „nenulové hodnotě“, což je přesně to, co if
testuje.
Alokaci výstupního pole necháváme na volajícím, takže funkce sama
nealokuje ani neuvolňuje. Volající musí zajistit, že se do output
vejde až n prvků.
Úkol 10
Naprogramujte funkci get_operation, která vrací pointer na funkci.
typedef double binary_op(double, double);
binary_op *get_operation(char symbol)
{
switch (symbol)
{
case '+': return op_add;
case '-': return op_sub;
case '*': return op_mul;
case '/': return op_div;
default: return 0;
}
}
Použití — návratovou hodnotu je nutné zkontrolovat, než ji zavoláme:
binary_op *f = get_operation('*');
if (f) { printf("%f\n", f(6, 7)); } // vytiskne 42.000000
else { fprintf(stderr, "neznamy operator\n"); }
Všimněte si, jak se typedef vyplatil. Bez něj by hlavička vypadala
double (*get_operation(char symbol))(double, double);
což je zápis, který se čte velmi obtížně: get_operation je funkce
beroucí char a vracející pointer na funkci dvou double vracející
double. S typedef je z toho čitelné binary_op *get_operation(char).
Zde je binary_op typ funkce, nikoliv pointeru — proto se
u návratového typu píše hvězdička. Kapitola ukazuje i druhou možnost,
kde je pointer součástí typu:
typedef double (*binary_op_ptr)(double, double);
binary_op_ptr get_operation(char symbol); // uz bez hvezdicky
Obojí je správně, jen je potřeba vědět, kterou variantu zrovna používáme.
Vracet 0 pro neznámý znak je tatáž konvence jako u neplatného pointeru
na data: nulový pointer na funkci je vždy neplatný a odpovídá nepravdě,
takže se dá testovat přímo v if. Zavolat ho by bylo nedefinované
chování — proto ta kontrola.