Nadużywanie preprocesora w C++

Wszyscy wiemy czym jest preprocesor Cpp i jak działa (dla niewtajemniczonych czym jest preprocesor, jakie oferuje funkcje i pułapki polecam jeden z rozdziałów Megatutoriala Xiona). Jest to narzędzie odziedziczone z C, operujące na tekście programu, a tym samym nie mające żadnego pojęcia o składni języka jaką przetwarza. Jest narzędziem, które używane nierozważnie może łatwo doprowadzić do klęski nasz program czy projekt. A mimo to wciąż tak wiele programistów piszących swoje aplikacje w C++ go kocha.

Jest też częstym nawykiem wyniesionym z C i nadużywany w C++, a przecież C++ ma wiele funkcjonalności, dzięki której możemy zrezygnować z jego korzystania. Oczywiście nie pozbędziemy się go w całości, ale wykorzystywanie go do definiowania stałych, makr i innych pseudofunkcji jest złym pomysłem!

Preprocesor powinien być wykorzystywany tylko i wyłącznie do włączania plików oraz sterowania kompilacją i opcjami kompilatora. Każde inne użycie jest pomyłką, która powinna zostać jak najszybciej naprawiona.

Korzystanie z preprocesora w C++ powinno być zaniechane.

Szlag mnie trafia, gdy widzę w kolejnym programie, czy projekcie używanie dyrektywy define preprocesora do definiowania stałych, rozwijanych funkcji lub próby wykorzystania makr do osiągnięcia imitacji generowania parametryzowanych obiektów lub funkcji:

#define SIZE        3
#define NAME		"dupa"
#define max(x,y)	(x > y ? x : y)

Do tego celu w języku C++ mamy stale obiekty poprzedzone specyfikatorem const i funkcje inline rozwijane w miejscu wywołania oraz wzorce:

const int size = 3;
const char* name = "dupa";
 
template<typename T>
inline T max(const T& x, const T& y) {
	return x > y ? x : y;
}

W przeciwieństwie do makr, mechanizmy te podlegają regułom języka, takim jak chociażby zasięg (widoczność) i kontrola typów. A co za tym idzie są bezpieczniejsze i wygodniejsze.

Jednym z głównych argumentów jakie zostaje podawane w odpowiedzi na pytanie „czemu używasz makr dla stałych, pada odpowiedź, że szybkość, bo przecież w wynikowym kodzie będą odpowiednie wartości a nie „zmienne”. Jest to totalną bzdurą.

Stale zadeklarowane jako const inicjowane znaną wartością, oraz wartością możliwą do wyliczenia i określenia przez kompilator w czasie kompilacji, zostaną zastąpione w wynikowym kodzie odpowiednimi wartościami. Więc gdzie tutaj brak szybkości i wydajności?

Programiści nadal kochają preprocesor i nagminnie go używają, a wygenerowane przez nich preprocesorowe potworki wprowadzają nie tylko zamieszanie i pogorszenie czytelności samego kodu, są beznadziejnym wyborem, świadczą o tym, ze autor chyba tak naprawdę nie zna języka w którym pisze swój program. Bo jeśli by znał to przecież by skorzystał z bardziej odpowiednich i dedykowanych rozwiązań.

Oczywiście zawsze istnieją wyjątki, gdzie skorzystanie z makr jest niezbędne i wygodne, aby otrzymać w wyniku zamierzony cel nie tracąc przy tym estetyki i czytelności kodu źródłowego.

Jedno z życzeń twórcy języka (Bjarne Stroustrup) jest związane z preprocesorem:

Chciałbym kiedyś dowiedzieć się, że preprocesor Cpp został usunięty. Jednak jedyny realny i odpowiedzialny sposób, który może do tego doprowadzić, polega na tym, aby najpierw sprawić, że Cpp stanie się zbędny, a następnie zachęcić ludzi do używania jego lepszych odpowiedników.

Ja chciałbym tylko, aby programiści zaczęli używać tych lepszych odpowiedników jakie obecnie oferuje im język. Ciekawe czy kiedykolwiek doczekamy sie tego. Kiedy używanie dyrektywy define zostanie zniwelowane tylko do specyficznych przypadków i wspomagania kompilacji warunkowej.

11 przemyśleń nt. „Nadużywanie preprocesora w C++”

  1. A np takie makro?

    #define for(a,b,c) for(int (a)=(b);(a)<(c);(a)++)

    jaki jest lepszy pomysł?
    Czy może w ogóle takie tworki zmieniające składnie niektórych rzeczy są be?

  2. Takie wlasnie udziwnienia i „ulatwienia” sa najgorsze, szczegolnie jak sa nakladka na slowa zastrzezone, bo moga wprowadzac zamieszanie dla kogos kto bedzie staral sie zrozumiec kod ;)
    Nie wspominajac o tym, ze wszystkie Coding Style Guide zabraniaja uzywania preprocesora.

    Dla mnie lepszym pomyslem jest pozostawienie zwyklego for, a jak bardzo chcemy operowac na zakresie to std::for_each() lub poczekac na C++0x’owe for dla zakresow ;p

  3. Szlag mnie trafia, gdy widzę w kolejnym programie, czy projekcie używanie dyrektywy define preprocesora do definiowania stałych, rozwijanych funkcji […] preprocesora

    Hmm to co powiesz o wxWidgets? ;) programiści tej biblioteki raczej nie unikają stosowania #define w kodzie. ;)

  4. Tak, znam wxWidgets od srodka dosyc dobrze i to prawda, ze nie grzesza z preprocesorem ;)

    Aczkolwiek biblioteka ta powstawala w dawnych czasach, przed standardem i duzo pzozostalosci nadal trzymanych jest w kodzie. Na szczescie od jakiegos czasu jestesmy na dobrej drodze zmian.
    Makra mozemy tam podzielic na trzy grupy, rzeczy ktore nie da sie inaczej zrobic niz za pomoca makr, aby zachowac dotychczasowy wyglad (statyczne tablice eventow), makra bedace wrapperami na nowe rozwiazania (glownie na templatesy rozne),aby zachowac kompatybilnosc ze starzym kodem i rozne stare nalecialosci, ktorych nikt nie dotyka.

    Ja mam nadzieje, ze z czasem, wiecej „nowoczesnych” (obecnie to juz standardowych) mechanizmow jezyka bedzie wykorzystane, jak namespace, template, exceptions…

    Ostatanio dodano wsparcie dla „natywnego” RTTI, jakis czas temu caly system eventow oparto na wzorcach i dodano mozliwosc wspolpracy z boost.signals. Oby tak dalej ;)

  5. Cześć :)

    Na początku zaznaczę, że rozumiem Twoje ‚ale’ do preprocesora, i faktycznie w większości przypadków się z tobą zgodzę.

    Po za jednym ;>

    Mianowicie zacytowane przez ciebie makro ‚max’. Osobiście wychodzę z założenia, że wszystkich chwyty dozwolone, które zwiększają czytelność, upraszczają kod, i upraszczają pisanie (przy utopijnym założeniu, że programiści czytający kod są na dobrym poziomie). Biorąc pod uwagę te 3 punkty, dokonam porównania dwóch stylów ‚max’:

    #define max(x,y) (x < y ? x : y)
     
    template<typename T>
    inline T max(const T& x, const T& y) {
    	return x > y ? x : y;
    }

    Czytelność: szczerze to jakoś 33 znakowe makro mnie bardziej przekonuje niż 3-liniowy template
    Uproszczenie kodu: (chodzi o zastosowanie) w obu przypadkach wychodzi na to samo
    Uproszczenie pisania: (chodzi o tworzenie kodu) dla wprawnego programisty to bez różnicy, niemniej jednak 33 znaki wklepie się szybciej niż 3 linie

    Po za tym przy debugowaniu tego mogę sobie spokojnie preprocesor rozwinąć (gcc -E) i zobaczyć wtf. Template’y mi tego nie oferują (z tego co mi wiadomo).

    Niemniej jednak oczywiście zdaje sobie sprawę że przykład z funkcją max może być akurat na granicy tego co wygląda OK w makrach i tego co lepiej zrobić na template’ach.

    Z drugiej jednak strony, mój kod raczej nigdy nie służył do czytania ;DDD

  6. Czytelnosc, w sumie to zalezy dla kogo ;)

    Debugowanie nie rozni sie niczym od debugowania zwyklego kodu, ale wszelkie komunikaty bledow moga byc bardzo odstraszajace, choc coraz lepiej prezentuja je „nowe” kompilatory ;p

    No i jak pisalem, wzorce podlegaja regulom jezyka co ma zasadnicza zalete nad makrami, w wiekszosci przypadkow pozwala szybciej wychwycic bledy, co w przypadku makr mogloby dac o sobie znac dopiero w run-time. Aczkolwiek zalezy to od kodu.

    Co racja to racja, przyklad typowo pmodelowo-przykladowy, ludzie robia rozne dziwne rzeczy na makrach, zamiast uproscic sobie zycie i wykorzystac dedykowane do tego celu narzedzia jezyka.

    Odnosnie Twojego kodu, to o ile to mozliwe i nie bedzie problemem, to chcialbym wykorzytac niektore fragmenty do notek z „tej” serii. Trafilem na kilka fragmentów, ktore sa idealnym przykladem pisania w C++ stylem C, i sie idealnie nadaja do pokazania, jak minimalne wykorzystanie kilku podstawowych nozliwosci jezyka C++ wplyneloby pozytywnie nie tylko na sama prezentacje kodu i wyrazenia idei, czy zamaiarow autora, ale przede wszystkim uproscilo kod, polepszylo dzialanie, i pozwolilo uchronic sie przed niektorymi potencjalnymi bledami.

  7. const char* name = "dupa";

    No i to jest ten częsty błąd, który często popełniają ludzie preferujący const nad #define. Powyższa deklaracja zapobiega tylko przed zmianą testu na który wskazuje wskaźnik – nie zapobiega natomiast nad zmianą wskaźnika. I tak – poniższy kod jest oczywiście nielegalny:

    #include <iostream>
     
    const char* NAME = "dupa";
     
    int main () {
        NAME[2] = 't';
        std::cout << NAME << std::endl;
        return 0;
    }

    Ale co z tego, skoro programista może zrobić po prostu tak:

    #include <iostream>
     
    const char* NAME = "dupa";
     
    int main () {
        const char* NAME2 = "duta";
        NAME = NAME2;
        std::cout << NAME << std::endl;
        return 0;
    }

    i już nasza „stała” jest czymś innym.

    Jeżeli chcesz więc zadeklarować zmienną zawierającą łańcuch stałych, poprawna deklaracja wygląda tak:

    const char* const name = "dupa";

    Wtedy oba kody są nielegalne.

    Pozdrawiam;)

  8. W wiekszosci wypadkow wystarcza wskaznik na staly obiekt, sam czesto stosuje dla stalych ;)

    Aczkolwiek, jak sam pokazales, staly wskaznik na staly obiekt uchroni przed niektorymi dziwnymi „zabiegami” programistow korzystajacych z naszego kodu, ktore w wielu przypadkach sa poprostu „wypadkami” przy pracy.

  9. @GC: Wszystko ładnie i pięknie (i przejrzyście), ale co powiesz na to:

    int i = 5;
    cout << max(5, i++)

    gdzie max jest oczywiście twoim makrem? :)

  10. @Xion
    Mamy różnicę założeń. Ja założyłem, że programista jest kompetentny i wie jak działają makra, natomiast Ty założyłeś, że programista jest niekompetentny ;)

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *