2015-06-25

A sada malo o C makroa ...

Šta najviše smeta C++-ašima u C-u. Pa makroi, i onda su ih prvo nazvali štetnim i lošim i zamenili nekim drugim mehanizmima (na sličan način štetnim, po meni). Da ne ulazim sada u tu priču, pokušaću da pokažem neke stvari koje se mogu uraditi standardnim makroima u C-u. Ovog puta izbeći cu priču o x-makroima, koga zanima neka pogleda https://en.wikipedia.org/wiki/X_Macro.

U jednoj od brojnih razmena misljenja sa dragim Mrkijem (uvek nesto naučim posle bar dva minuta priče sa njim) došli smo do retoričkog pitanja: Kako razrešavas u C-u a->b->c->d->e, ali da vrati NULL ako je bilo koji od a, a->b, a->b->c, a->b->c->d baš NULL. Prva ideja sa nekom funkcijom koja bi ... pa ček kako ćes funkciji proslediti ime? Ne ide. Ajde da napišemo konkretno neki kod, pa da vidimo kako bi to moglo da se ulepša, generalizuje, makroizuje.

if
(
  a == NULL ||
  a->b == NULL ||
  a->b->c == NULL ||
  a->b->c->d == NULL
)
  result = NULL;
else
  result = a->b->c->d->e;
Prvo što primećujem je da možemo ovu pitalicu da zamenimo sa omiljenim tenarnim ?:, ali ajde pre toga da razvijem gomilu or-ova. Zašto, pa eto, iskustvo mi govori da to treba uraditi :).
if(a == NULL)
  result = NULL;
else if(a->b == NULL)
  result = NULL;
else if(a->b->c == NULL)
  result = NULL;
else if(a->b->c->d == NULL)
  result = NULL;
else
  result = a->b->c->d->e;
Sada možemo da iskoristomo ?: i dobijemo izraz, koji se može koristiti, pa i dodeliti u neki result:
(a == NULL ? NULL : (a->b == NULL ? NULL : (a->b->c == NULL ? NULL : (a->b->c->d == NULL ? NULL : a->b->c->d->e))))
Kako bi bilo lepo da ovo mozemo da pozovemo recimo sa N(a, b, c, d, e). Eh, steta sto C makroi nisu tako moćni. Ili možda jesu?

Šta primećujem, da ako bi uzeli prva dva argumenta (a i b), zatim proverili da li je a jednako NULL pa vratili ili NULL ili rekurzivni poziv (umesto prva dva argumenta imamo a->b i ostatak). Treba još da obezbedimo izlazak iz rekurzije, što je jednostavno, kada ima samo dva argumenta zaustavi se tu. Ali ono sto je teži deo posla, to je sto C makroi ne podržavaju rekurziju. Šta uraditi?

Ajde da napišemo nekoliko funkcija, dogovorimo se da svaka poziva prehodnu, nazovimo ih N_2, N_3, N_4, ... dokle tako, pa ajde da se dogovorimo i da kazemo da nikada neće biti vise od 16 promenljivih u ovom nizu. Da ispišemo te makroe, barem do N_5:

#define N_2(a, b) (a == NULL ? NULL : a->b)
#define N_3(a, b, c) (a == NULL ? NULL : N_2(a->b, c))
#define N_4(a, b, c, d) (a == NULL ? NULL : N_3(a->b, c, d))
#define N_5(a, b, c, d, e) (a == NULL ? NULL : N_4(a->b, c, d, e))
Odlično, probamo sa gcc -E (prepuštam vama da proverite, ja verujem sebi, za sada :) ). Radi! Dobro, može ovako da se ispiše do N_16, pa i do N_100 ako zatreba i eto. Nije baš praktično, moramo da pozovemo tačnu funkciju, i složeno je za pisanje. Pa ajde da optimizujemo nekako. Primetimo da sve posle trećeg argumenta samo ponavljamo, dakle super, moze moja omiljena forma: ellipsis. Ne znam da li ste primetili, ali ja baš volim ellipsis ...
#define N_2(a, b, ...) (a == NULL ? NULL : a->b)
#define N_3(a, b, ...) (a == NULL ? NULL : N_2(a->b, __VA_ARGS__))
#define N_4(a, b, ...) (a == NULL ? NULL : N_3(a->b, __VA_ARGS__))
#define N_5(a, b, ...) (a == NULL ? NULL : N_4(a->b, __VA_ARGS__))
Ah, kako me nervira ovo ponavljanje oblika (x == NULL ? NULL : y), zameniću ga makrom _N. O istom trošku dodaću i N_0 i N_1, neka se nadju, zatrebaće možda.
#define _N(x, y) (x == NULL ? NULL : y)
#define N_0(...) NULL
#define N_1(a, ...) a
#define N_2(a, b, ...) _N(a, a->b)
#define N_3(a, b, ...) _N(a, N_2(a->b, __VA_ARGS__))
#define N_4(a, b, ...) _N(a, N_3(a->b, __VA_ARGS__))
#define N_5(a, b, ...) _N(a, N_4(a->b, __VA_ARGS__))
Ovo već liči na nešto, kada bi mogli nekako da izračunamo broj argumenata zadatih makrou i pozovemo odgovarajući makro. Nešto oblika:
#define N(...) N_##NUMBER(__VA_ARGS__)(__VA_ARGS__)
Na žalost, ne postoji ništa što moze da izracuna broj argumenata, osim da to sami ne napišemo kao makro. A i tada, nece doci do zamene, pa ajde da ubacimo koji makro više (više ćete saznato o tome kada naučite kako makroi rade, i kada/kako vrše zamenu):
#define N__(n, ...) N_##n(__VA_ARGS__)
#define N_(n, ...) N__(n, __VA_ARGS__)
#define N(...) N_(NUMBER(__VA_ARGS__), __VA_ARGS__)
Sve je to lepo, ali i dalje nam nedostaje makro NUMBER koji računa koliko je bilo argumenata.

Ideja je jednostavna, ako iza __VA_ARGS__ navedemo redom brojeve, pa uzmemo neki na fiksnoj poziciji i dobićemo duzinu. Naravno treba namestiti da lepo radi.

#define NUMBER_(x5, x4, x3, x2, x1, n, ...) n
#define NUMBER(...) NUMBER_(__VA_ARGS__, 5, 4, 3, 2, 1)
Ovo računa broj argumenata samo do duzine 5, ali eto, lako se da produžiti. Ne radi za prazan ulaz, ali i to se može srediti vezivanjem __VA_ARGS__ za prethodni argument. Na žalost to nije deo standarda, ali naravno gcc to guta, nadam se da će uskoro i to da prihvate u standardu. Naime x, ##__VA_ARGS__ će se razviti u samo x (nema zareza) ako je __VA_ARGS__ prazno, inace normalno se radi konkatenacija.
#define NUMBER_(x, x5, x4, x3, x2, x1, n, ...) n
#define NUMBER(...) NUMBER_(x, ##__VA_ARGS__, 5, 4, 3, 2, 1, 0)
Ha, imamo sve, sada možemo da spojimo, i da proširimo do recimo 16:
#define NUMBER_(x, x16, x15, x14, x13, x12, x11, x10, x9, x8, x7, x6, x5, x4, x3, x2, x1, n, ...) n
#define NUMBER(...) NUMBER_(x, ##__VA_ARGS__, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0)
#define _N(x, y) (x == NULL ? NULL : y)
#define N_0(...) NULL
#define N_1(a, ...) a
#define N_2(a, b, ...) _N(a, a->b)
#define N_3(a, b, ...) _N(a, N_2(a->b, __VA_ARGS__))
#define N_4(a, b, ...) _N(a, N_3(a->b, __VA_ARGS__))
#define N_5(a, b, ...) _N(a, N_4(a->b, __VA_ARGS__))
#define N_6(a, b, ...) _N(a, N_5(a->b, __VA_ARGS__))
#define N_7(a, b, ...) _N(a, N_6(a->b, __VA_ARGS__))
#define N_8(a, b, ...) _N(a, N_7(a->b, __VA_ARGS__))
#define N_9(a, b, ...) _N(a, N_8(a->b, __VA_ARGS__))
#define N_10(a, b, ...) _N(a, N_9(a->b, __VA_ARGS__))
#define N_11(a, b, ...) _N(a, N_10(a->b, __VA_ARGS__))
#define N_12(a, b, ...) _N(a, N_11(a->b, __VA_ARGS__))
#define N_13(a, b, ...) _N(a, N_12(a->b, __VA_ARGS__))
#define N_14(a, b, ...) _N(a, N_13(a->b, __VA_ARGS__))
#define N_15(a, b, ...) _N(a, N_14(a->b, __VA_ARGS__))
#define N_16(a, b, ...) _N(a, N_15(a->b, __VA_ARGS__))
#define N__(n, ...) N_##n(__VA_ARGS__)
#define N_(n, ...) N__(n, __VA_ARGS__)
#define N(...) N_(NUMBER(__VA_ARGS__), __VA_ARGS__)
Ah, a pa zato sam definisao N_0 i N_1 ... sada je i meni jasno :). Ostaje da sami proverite da li radi i da krenete da ozbiljno razmi[ljate o makroima u C-u.

P.S. Ako ste dokoni, uradite na slican nacin makro MAP(f, ...) koji na niz argumenata primenjuje (makro) f().