C++ bez cholesterolu

Deklaratory

Z całej składni języka C najbardziej nie lubię składni deklaracji. Bjarne Stroustrup

Na zakończenie tej części, ponieważ poznaliśmy już wszystkie sposoby deklarowania wszystkiego co się da, wspomnę teraz o największej bolączce C++ (burzliwe dyskusje na ten temat prawdopodobnie trwają nadal, ale aktualny standard niczego jeszcze nie ulepszył w tej kwestii). Język C, jako język spartański, przyjął najprostszą możliwą postać deklaracji i w związku z tym jest ona pogmatwana na każdy możliwy sposób. Właściwie może nie byłby tak pogmatwana, gdyby nie operatory przedrostkowe, a właściwie tylko jeden - *. Problem w tym, że jest on w deklaratorach chyba najczęściej używany. Różni profesorzy, doktorzy i magistrzy na wyższych uczelniach uwielbiają gnębić studentów podając im deklarator i żądając objaśnienia, co taki deklarator deklaruje (jak sobie przypomnę koszmar lekcji języka polskiego z liceum i tłumaczenie "co autor miał na myśli", to mam trochę dziwnie analogiczne skojarzenia... pocieszać się można tylko tym, że kompilator to i tak zrozumie zawsze jednoznacznie). Ze swojej strony tylko osobiście podpowiem, że staram się zawsze wprowadzać definicje pośrednie, jeśli muszę już używać jakichś skomplikowanych typów, jednak zazwyczaj cały kłopot sprowadza się do problemów z odpowiednim umieszczeniem i opakowaniem nawiasami jednego operatora - *.

W deklaratorach występują operatory, podobne do takich, jakie się używa potem na obiektach tak deklarowanych typów. A więc, * do wskaźników, [] do tablic i () do funkcji (& do referencji tutaj jest wyjątkiem, ale wydaje mi się, że ten operator jest wystarczająco czytelny). W sumie nawet deklaratory nie są czymś skomplikowanym. Tzn. nie byłyby, gdyby nie *.

I wbrew pozorom, * jest jedynym operatorem, z którym są takie problemy; operatory przyrostkowe są całkowicie bezproblemowe, a referencje mają zbyt ograniczone możliwości. Praktycznie w deklaracji jakiejkolwiek referencji nie ma żadnego innego operatora oprócz `&' - tablic referencji tworzyć się nie da, a referencji do tablicy lub funkcji aż tak często się nie używa.

Postać deklaratora wygląda następująco:

<typ czołowy>
  <operatory przedrostkowe> nazwa
  <operatory przyrostkowe>;

Przy czym operatory [] i () to operatory przyrostkowe, a * i & - przedrostkowe. Proszę zgadnąć zatem, co takiego może deklarować taki deklarator, który dla dodatkowego zmylenia zapiszę tak, jak "profesjonalni" programiści C:

int *tab[20];

A więc - czym jest `tab'? Wskaźnikiem do tablicy 20 elementów typu int, czy tablicą o 20 elementach typu `int*'?

Odpowiem może tak: co się stanie, jeśli dodamy odpowiednio nawiasy:

int (*tab)[20];

Otóż ta właśnie deklaracja deklaruje wskaźnik do tablicy ... itd., podczas gdy tamten poprzedni typ oznaczał właśnie to drugie.

Oczywiście, że w int* tab[20]; lepiej wiadomo, o co chodzi. Niestety to tylko pozór. Gdyby bowiem chcieć wstawić w tą deklarację nawiasy tam, gdzie one powinny być, należałoby je umieścić w następujący sposób:

int (*tab[20]);

I ciekaw jestem, komu skojarzyłoby się to z tablicą 20 int*?

Ale to jeszcze nic. Przykładowo, mamy w bibliotece standardowej C taką funkcję `signal' (zostanie ona dalej przedstawiona). Służy ona do ustanowienia nowej funkcji obsługującej konkretny sygnał wysłany do programu, zwracając starą. Funkcja ta ma posiadać nagłówek: `void sighandler(int)'. Funkcja `signal' zatem przyjmuje i zwraca wskaźnik do niej. Oto jej deklaracja (również dla utrudnienia, napiszę ją tak, jak "profesjonalni" programiści to piszą, czyli bez wstawiania spacji wewnątrz nawiasów funkcji, jak to mam w zwyczaju):

void (*signal(int sig, void (*handler)(int)))(int);

No i jak? Podoba się? Tak jest (nawiasem mówiąc) podana ta deklaracja we sławetnej książce Kernighan & Ritche "Język ANSI C" (tzw. "biblii" programistów C). Nikt normalny przecież (a więc również twórcy bibliotek do GNU C) nie wpisałby czegoś takiego do kodu (od samego patrzenia na to można dostać świra :*). Tam deklaracja tej funkcji zawiera pewną deklarację pośrednią (tu pokazałem trochę uproszczoną):

typedef void (*__sighandler_t)(int);

I potem:

__sighandler_t signal( int, __sighandler_t );

I to już jest proste i zrozumiałe (__sighandler_t jest zresztą dalej w tym pliku nagłówkowym używane do jeszcze wielu innych deklaracji).

Gdyby kogoś interesowało, jak wstawiłem tamtą poprzednią deklarację funkcji `signal' do tego tekstu, to wyjaśniam, że napisałem to z pamięci, aczkolwiek nie od początku do końca. Nie skleiłem tego również z przedstawionych deklaracji pośrednich (szczerze powiedziawszy, miałbym z tym problemy, poza tym znalazłem lepsze rozwiązanie). Zanim jednak przedstawię szczegóły, przedstawię źródło całej koncepcji.

Bjarne Stroustrup we wspomnianej już książce wspomina, że pojawiła się koncepcja poprawy konstrukcji deklaratorów, gdzie zamiast operatora przedrostkowego `*' używałoby się przyrostkowego `->' (niestety nie dopracował tego pomysłu i w końcu nie znalazł się on w propozycji do standardu). Choć obowiązkowość tego "typu czołowego" nadal wprowadzałaby pewne zamieszanie, ale deklarator byłby jeszcze jakoś możliwy do odczytania. Na przykład wspomniana funkcja miałaby taki nagłówek:

void signal( int sig, void handler->( int ) )->( int );

Radykalna propozycja, na której się oparłem, polegała dodatkowo na tym, że cały zlepek operatorów przyrostkowych (czyli stojących "za" nazwą) można przenieść przed typ czołowy, ale tylko wtedy, jeśli deklarator zawiera nazwę (bo nie musi; ta pierwsza deklaracja funkcji `signal' nie musiała mieć np. nazwy argumentu `handler'; mogłoby pozostać samo (*)). Deklaracja taka wyglądałaby wtedy w następujący sposób:

->( int )void signal( int sig, ->( int )void handler );

Czytałoby się to (poczynając od funkcji i kończąc na typie zwracanym): "Funkcja signal, przyjmująca argument `sig' typu `int' oraz argument `handler' typu `wskaźnik do funkcji przyjmującej `int' i zwracającej `void' ', zwracająca wskaźnik do funkcji przyjmującej argument `int' i zwracającej `void'.

To i tak jeszcze mało. Tablica wskaźników do funkcji np. wyglądałaby tak:

[20]->( int )void tab;

Ewentualnie nic nie stoi na przeszkodzie, żeby umieszczać operatory część tu część tam (przy przenoszeniu należy pamiętać, że operatory muszą być ustawione w tej samej kolejności!):

->( int )void tab[20];

Czasem, gdyby zaszła potrzeba stworzenia takiej skomplikowanej deklaracji, zawsze się można posłużyć taką. Konwersja na postać akceptowalną przez kompilator jest banalnie prosta - należy najpierw umieścić wszystkie operatory przyrostkowe tam, gdzie "były", czy gdzie powinny być:

void tab[20]->( int );

po czym zamienić wszystkie `->' na `*' w ten sposób: w miejscu, gdzie jest `->' wstawić `)', po czem po tej operacji dodać `(*' przed nazwą. Jeśli wiesz już również, jak konwertować w drugą stronę, to takie zamieszane deklaracje przestaną być jakimkolwiek problemem.