C++ bez cholesterolu

Preprocesor

"Chiałbym się kiedyś dowiedzieć, że preprocesor został usunięty. Jednak jedyny realny i odpowiedzialny sposób, który może do tego doprowadzić, polega na tym, żeby najpierw sprawić, że stanie się zbędny, po czym zachęcić ludzi do używania jego lepszych odpowiedników." Bjarne Stroustrup

Wiadomości podstawowe

Ja osobiście jestem sceptyczny co do życzeń Stroustrupa w kwestii preprocesora i wydaje mi się że ze zrozumiałych względów pozostanie on w C++ po wsze czasy (wyobraziłem sobie w tym momencie, jak Stroustrup leży na łożu śmierci i nagle ktoś podbiega do niego i mówi: "Mistrzu! Preprocesor został usunięty z C++!", na co Stroustrup wypowiada swoje ostatnie słowa: "Uff, dożyłem..."; no dobra, ale starczy tych żartów :). Oczywiście zdanie Stroustrupa o preprocesorze jest całkowicie zrozumiałe, a tym bardziej zrozumiałe dla kogoś, kto widział całe mnóstwo kodu napisanego z mocnym wykorzystaniem preprocesora.

Preprocesor omawiam na samym początku z tego względu, iż jest to oddzielny program, dokonujący wstępnej obróbki tekstu źródłowego programu przed jego kompilacją. Poza tym nie da się praktycznie dzisiaj znaleźć programu, który nie zaczynałby się od dyrektywy preprocesora (no, chyba że zaczyna się komentarzem :*). Preprocesor umożliwia wykonywanie takich rzeczy, jak tworzenie makrodefinicji, wstawianie innych plików, kompilację warunkową, oraz parę innych mniej istotnych rzeczy. W tym rozdziale zawarłem wszystkie informacje na temat preprocesora, niemniej do następnych rozdziałów będzie potrzebna praktycznie jedynie znajomość dyrektywy #include.

Zwracam uwagę, że preprocesor istnieje w sposób oderwany od języka i jest to jakby całkowicie niezależny język. Właśnie z tego powodu należy w miare możliwości unikać jego stosowania polegając raczej na właściwościach samego C++.

Dyrektywa #include

Dyrektywa ta nakazuje wstawić plik nagłówkowy (w zasadzie to czy on jest taki znów nagłówkowy niewiele preprocesor obchodzi). Plik, podany jako argument, umieszczamy, w zależności od jego lokalizacji, w nawiasach trójkątnych <> lub podwójnym cudzysłowie "". Plik podany w <> jest wyszukiwany w katalogu, który jest zarejestrowany przez kompilator jako katalog z plikami nagłówkowymi. Kompilatory pod windows mają to ustawione w konfiguracji, dla UNIX-a (np. g++) będzie to na pewno /usr/include i jeszcze dodatkowy dla nagłówków C++ (w przypadku gcc od wersji 3 będzie to /usr/[byc może local/]include/c++/[wersja]). Plik podany w "" jest szukany w bieżącym katalogu (w przypadku nieznalezienia oczywiście jest szukany w katalogach zarejestrowanych). Przykładowo:

#include <stdlib.h>

wstawia plik stdlib.h znajdujący się w katalogu z nagłówkami, a

#include "main.h"
int main()

wstawia plik main.h znajdujący się w bieżącym katalogu (ale jeśli nie zostanie znaleziony w bieżącym katalogu, jest nadal poszukiwany w katalogach z plikami nagłówkowymi!). W poprzednim rozdziale przykład zawierał dyrektywę:

#include <iostream>

Dyrektywa ta nakazuje wczytać deklaracje przydatne do operacji wejścia-wyjścia na powłoce, która uruchamia program (w tym przedstawione `cout', `cin' i specjalne dla nich operatory << i >>).

Tu jedno małe przypomnienie. Niektórzy pewno pamiętają, że przed standardem ISO C++ używało się <iostream.h>. Jest to nadal możliwe, ale jest to tylko pozostawione dla wstecznej zgodności (oczywiście tylko na niektórych kompilatorach i też tylko do pewnej wersji). Aktualnie używa się nagłówków standardowych bez .h, ale jednocześnie są pewne istotne różnice, które omówię później (związane z tym właśnie using namespace std;).

Dyrektywa #define

Dyrektywa ta pozwala tworzyć makrodefinicje. Pozwala ona zastąpić DOWOLNY ciąg znaków (również pusty) identyfikatorem. Może służyć np. do definiowania stałych:

#define przyciaganie_ziemskie 9.81

Taka makrodefinicja może również posiadać argumenty, np.

#define ctg( x ) 1/tan( x )

Zaznaczam z góry, że znajomość tej dyrektywy raczej nie będzie Ci potrzebna; najwyżej do tego, żeby wiedzieć, co to jest. Jest to uniwersalne dosyć narzędzie, ale bardzo niebezpieczne i dające mnóstwo okazji do popełniania błędów. Kompilator nie stwierdzi błędu w definicji makra (jeśli np. zdefiniujesz jako deasygnat makra jakąś konstrukcję, która jest niepoprawna składniowo w C++), a ewentualnie dopiero w miejscu, gdzie zostało ono użyte (kompilator nie widzi makr ani ich używania; preprocesor jest właśnie od tego, zeby je usuwać). W przypadku zastępowania nimi wyrażeń arytmetyczno-logicznych należy używać dla pewności jak najwięcej nawiasów, oraz - o czym też wielu zapomina - NIE WOLNO używać żadnych operatorów modyfikujących na zmiennych przekazywanych jako parametry makra (tzn. jako parametry makra należy podawać wartości lub zmienne raczej, niż wyrażenia). Preprocesor w C++ jest właściwie takim czymś "na boku" i jakby zupełnie osobnym językiem programowania. Toteż jednym też z typowych błędów jest zakończenie tej dyrektywy średnikiem. Należy pamiętać, że wszystko, co zostanie podane po nazwie (jeśli makro posiada parametry to po zamknięciu nawiasu) jest tekstem, którym w źródle podanym następnie do kompilatora to makro zostanie zastąpione.

I przypominam jeszcze raz: jak każdy makroprocesor, preprocesor języka C++ działa w całkowitym oderwaniu od języka C++, w związku z czym, tak niewinnie wyglądające wywołanie:

print_value( x );

może być wywołaniem funkcji, zdefiniowanej zgodnie z regułami języka C++, ale może być również makrem preprocesora, które zrealizowano z argumentem x. Być może jest to:

#define print_value( x ) cout << #x " = " << x << endl

Co po rozwinięciu tak, jak podano wyżej, zostanie zamienione na:

cout << "x" " = " << x << endl;

Dlatego właśnie niepisaną zasadą jest nazywanie makr preprocesora dużymi literami. W niektórych kompilatorach można nawet w nazwach używać znaku $ (moim skromnym zdaniem, od początku z preprocesorem byłby mniejszy problem, gdyby w nazwach makr preprocesora użycie znaku $ w odpowiednim miejscu było wymuszone).

Dodatkowo, oczywiście, nie każdy ciąg znaków można przekazać jako argument dla preprocesora. Jeśli bowiem podajemy kilka argumentów, to oddzielamy je przecinkiem. Skoro tak, to znaczy, że jeśli chcielibyśmy skonstruować np. jakieś makro, którego argumentem będzie lista argumentów funkcji, a ta niestety jest rozdzielana przecinkami, to nie można już jej podać jako argumentu, jeśli miałby to być jeden argument makra.

Błąd w makrodefinicji może wystąpić jedynie wtedy, jeżeli ponownie użyto tego samego identyfikatora (ale jeśli deasygnat makra, z dokładnością do niebiałych znaków, jest tak sam, to nie jest to błąd - kompilator co najwyżej może wystosować ostrzeżenie). Czasem jednak istnieje potrzeba, aby zmienić definicję makra - należy użyć wtedy dyrektywy #undef, która usuwa definicję makra (można go wtedy również zdefiniować ponownie). Możemy również dać pustą makrodefinicję, jako sygnalizator (przyjmie ona wtedy zawartość 1):

#define USE_ALL

W definicjach dyrektyw, jeśli używamy argumentów przydają się jeszcze dodatkowe znaczenia znaku `#'. Podwójny oznacza sklejenie dwóch nazw:

#define MY( name ) JanB##name

Jeśli więc zadeklarujemy funkcję o nazwie

int MY( Func )( int );

W efekcie będzie to oznaczało:

int JanBFunc( int );

Nie można tutaj użyć po prostu `JanBname', gdyż preprocesor potraktowałby to jako osobny symbol. Z kolei, pojedynczy znak `#' przed argumentem oznacza ujęcie go w "". Na przykład:

#define CHAN( type ) CHANNEL_##type, #type

Jeśli zrealizujemy to przez CHAN( CHAT ), to uzyskamy w efekcie:

CHANNEL_CHAT, "CHAT"

Dyrektywa #define jest bardzo fajna, aczkolwiek gorąco polecam ograniczanie jej używania do WYŁĄCZNIE niezbędnego minimum (jeśli cokolwiek da się zrobić inaczej, niż dyrektywą #define i tak samo czytelnie, to tak zrób). Deklaracje stałych oraz funkcji rozwijalnych można w C++ zrealizować dużo bezpieczniejszymi metodami, o których będzie mowa później (przy omawianiu deklaracji obiektów, funkcji, a później przy wzorcach).

Przykładem tego, co nie może być zrealizowane bez wsparcia preprocesora, jest wspomniane już sklejanie identyfikatorów i ujmowanie identyfikatora w "królicze uszy" (co w połączeniu z automatycznym sklejaniem napisów, o czym będzie później, daje całkiem niewąskie możliwości). Dodatkowo istnieją też makra standardowe, których istnienie (i specyfikacja wartości) są gwarantowane w każdej kompilacji; są to np. makra __FILE__ i __LINE__, których np. używa się po to, żeby wstawić jakąś instrukcję diagnostyczną, wskazującą miejsce w kodzie źródłowym. Jeśli jednak, mimo wszystko, chcemy robić takie rzeczy, to należy robić to dwustopniowo: najpierw stworzyc normalną C++-ową funkcję, która będzie przyjmowała wszystkie interesujące ją argumenty, w tym te, które są osiągalne tylko z preprocesora (np. wspomniane __FILE__), a dopiero do tego zrobić makro preprocesora, które przekaże tej funkcji zarówno otrzymane parametry (np. dodatkowy tekst dla diagnostyki), jak i swoje (np. __FILE__). W żadnym wypadku nie należy tworzyć złożonych konstrukcji jako deasygnatów makra, nie należy też pozwalać, by parametr makra wystąpił w jego deasygnacie więcej, niż raz.

Kompilacja warunkowa

Podstawową dyrektywą warunkową jest #if. Jako argument przyjmuje ona warunek, który ma być spełniony (pamiętaj jednak, że interpretuje go preprocesor, a nie kompilator, nie możesz w nich zatem używać obiektów, których preprocesor nie widzi - np. zmiennych). Najczęściej jednak do kompilacji warunkowej stosuje się makrowartowniki. Są to puste makra, definiowane w plikach nagłówkowych, dla np. zabezpieczenia przed kilkakrotnym wstawianiem tego samego pliku. Sprawdzenia tego dokonujemy dyrektywami #ifdef i #ifndef, czasem używa się też #if i funkcji preprocesora defined():

#ifndef __STDLIB_H
lub
#if !defined( __STDLIB_H )
i dalej:
#define __STDLIB_H
... (tutaj deklaracje)
#endif

Jeżeli makrodefinicja __STDLIB_H została już wcześniej zdefiniowana, oznacza to, że plik już był wcześniej wstawiany i zawarte w nim deklaracje są pomijane. Jako dyrektywy warunkowe stosuje się też #else i #elif. Nie omawiam ich, gdyż uważam, że ich znaczenie jest oczywiste (a jeżeli nie, to po omówieniu instrukcji takowe się stanie).

Inne dyrektywy

Rzadziej znacznie są w programach używane dyrektywy takie, jak:

  1. #error - powoduje wyrzucenie błędu kompilacji z podanym jako argument komunikatem (używane tylko wespół z dyrektywami warunkowymi),
  2. #pragma - zmienia ustawienia kompilatora (użycie tej dyrektywy zależy wyłącznie od implementacji)
  3. #line - udaje, że następna linia jest inną linią z innego pliku

Dyrektywa nieznana (inna, niż wymienione ciągi znaków) jest błędem (przynajmniej wg. standardu tylko takie dyrektywy istnieją, bo gcc ma jeszcze np. taką dyrektywę `#include_next'). Dyrektywa pusta (składająca się tylko ze znaku `#') jest dopuszczalna i nie daje żadnego efektu.