Nadużywanie preprocesora w C++
• tech • 542 słowa • 3 minuty czytania
Wszyscy wiemy czym jest preprocesor Cpp i jak działa1. Jest to narzędzie odziedziczone z języka C, operujące na tekście kodu programu, a tym samym nie mające żadnego pojęcia o składni i regułach języka jaki przetwarza. Jest narzędziem, które używane nierozważnie może z łatwością doprowadzić do klęski nasz program. A mimo to wciąż tak wiele programistów piszących swoje aplikacje w C++ go kocha.
Nagminne używanie preprocesora jest też częstym nawykiem wyniesionym z C i nadużywanym w C++. A przecież C++ ma wiele funkcjonalności, dzięki którym 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. Poza wyjątkami, 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 template
:
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ą ścisłym 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 często 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”. Oczywiście jest to totalną bzdurą.
W C++ stałe 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. Zatem o jaki brak szybkości i wydajności tutaj chodzi?
Programiści nadal kochają preprocesor i nagminnie go używają, a wygenerowane przez nich preprocesorowe potworki wprowadzają zamieszanie i pogorszenie czytelności samego kodu. Nierzadko są też beznadziejnym wyborem, świadczącym o tym, że sam autor chyba tak naprawdę nie zna języka w którym pisze swój program. Bo jeśli byłoby inaczej to przecież skorzystałby z bardziej cywilizowanych, odpowiednich i dedykowanych rozwiązań.
Oczywiście zawsze istnieją wyjątki, gdzie skorzystanie z makr jest niezbędne i wygodne do osiągniecia zamierzonego celu, 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 preprocesorem2:
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 się tego doczekam. Może kiedyś używanie dyrektywy define
przez programistów zostanie zniwelowane tylko do specyficznych przypadków i wspomagania kompilacji warunkowej.
Przypisy
-
Dla niewtajemniczonych czym jest preprocesor, jakie oferuje funkcje i pułapki polecam jeden z rozdziałów Megatutoriala Xiona. ↩︎
-
B. Stroustrup: “Projektowanie i rozwój języka C++”, Wydawnictwa Naukowo-Techniczne 1996 ↩︎
Komentarze (11)
A np takie makro?
jaki jest lepszy pomysł?
Czy może w ogóle takie tworki zmieniające składnie niektórych rzeczy są be?
Takie właśnie udziwnienia i “ułatwienia” są najgorsze, szczególnie jak są nakładką na słowa zastrzeżone, bo mogą wprowadzać zamieszanie dla kogoś kto będzie starał się zrozumieć kod. Nie wspominając o tym, że wszystkie Coding Style Guide zabraniają używania preprocesora.
Dla mnie lepszym pomysłem jest pozostawienie zwykłego
for
, a jak bardzo chcemy operować na zakresie to lepiej już użyćstd::for_each()
lub poczekać na C++0x’owefor
dla zakresów ;)Hmm to co powiesz o wxWidgets? ;) programiści tej biblioteki raczej nie unikają stosowania
#define
w kodzie. ;)Tak, znam wxWidgets od środka dosyć dobrze i to prawda, że nie grzeszą z preprocesorem ;)
Aczkolwiek biblioteka ta powstawała w dawnych czasach, przed standardem i dużo pozostałości nadal trzymanych jest w kodzie. Na szczęście od jakiegoś czasu jesteśmy na dobrej drodze zmian.
Makra możemy tam podzielić na trzy grupy, rzeczy które nie da się inaczej zrobić niż za pomocą makr, aby zachować dotychczasowy wygląd (statyczne tablice eventów), makra będące wrapperami na nowe rozwiązania (głownie na różne templatesy), aby zachować kompatybilność ze starszym kodem i różne stare naleciałości, których nikt nie dotyka.
Ja mam nadzieje, że z czasem, więcej “nowoczesnych” (obecnie to już standardowych) mechanizmów języka będzie wykorzystane, jak namespace, template, exceptions… Ostatnio dodano wsparcie dla “natywnego” RTTI, jakiś czas temu cały system eventów oparto na wzorcach i dodano możliwość współpracy z
boost.signals
. Oby tak dalej ;)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ówmax
: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
Czytelność, w sumie to zależy dla kogo ;)
Debugowanie nie różni się niczym od debugowania zwykłego kodu, ale wszelkie komunikaty błędów mogą być bardzo odstraszające, choć coraz lepiej prezentują je “nowe” kompilatory ;)
No i jak pisałem, wzorce podlegają regułom języka co ma zasadniczą zaletę nad makrami, bo w większości przypadków pozwala to szybciej wychwycić błędy, co przy makrach mogłoby dać o sobie znać dopiero w run-time. Aczkolwiek to zawsze zależy od kodu.
Co racja to racja, przykład typowo modelowo-przykładowy. Ludzie robią różne dziwne rzeczy na makrach, zamiast uprościć sobie życie i wykorzystać dedykowane do tego celu narzędzia języka.
Odnośnie Twojego kodu, to o ile to możliwe i nie będzie problemem, to chciałbym wykorzystać niektóre fragmenty do notek z “tej” serii. Trafiłem na kilka fragmentów, które są idealnym przykładem pisania w C++ stylem C i się idealnie nadają do pokazania, jak minimalne wykorzystanie kilku podstawowych możliwości języka C++ wpłynęłoby pozytywnie nie tylko na samą prezentację kodu i wyrażenia w nim idei, czy zamiarów autora, ale przede wszystkim uprościło kod, polepszyło działanie i pozwoliło uchronić się przed niektórymi potencjalnymi błędami.
@Malcom
Sure, możesz użyć mojego kodu ;> Jeśli coś można w nim lepiej zrobić, to bardzo chętnie się tego nauczę ;>
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:Ale co z tego, skoro programista może zrobić po prostu tak:
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:
Wtedy oba kody są nielegalne.
Pozdrawiam;)
W większości wypadków wystarcza wskaźnik na stały obiekt, sam często stosuję dla stałych ;)
Aczkolwiek, jak sam pokazałeś, stały wskaźnik na stały obiekt uchroni przed niektórymi dziwnymi “zabiegami” programistów korzystających z naszego kodu, które w wielu przypadkach są po prostu “wypadkami” przy pracy.
@GC:
Wszystko ładnie i pięknie (i przejrzyście), ale co powiesz na to:
gdzie max jest oczywiście twoim makrem? :)
@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 ;)