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

  1. Dla niewtajemniczonych czym jest preprocesor, jakie oferuje funkcje i pułapki polecam jeden z rozdziałów Megatutoriala Xiona↩︎

  2. B. Stroustrup: “Projektowanie i rozwój języka C++”, Wydawnictwa Naukowo-Techniczne 1996 ↩︎

Komentarze (11)

matiit avatar
matiit
20091201-210542-matiit

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?

Malcom avatar
Malcom
20091201-211600-malcom

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’owe for dla zakresów ;)

lopik avatar
lopik
20091204-171840-lopik

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

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

Malcom avatar
Malcom
20091204-183653-malcom

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 ;)

Gynvael Coldwind avatar
Gynvael Coldwind
20091205-214656-gynvael-coldwind

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

Malcom avatar
Malcom
20091206-201830-malcom

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.

Gynvael Coldwind avatar
Gynvael Coldwind
20091206-224755-gynvael-coldwind

@Malcom
Sure, możesz użyć mojego kodu ;> Jeśli coś można w nim lepiej zrobić, to bardzo chętnie się tego nauczę ;>

matekm avatar
matekm
20100128-073637-matekm
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;)

Malcom avatar
Malcom
20100128-224535-malcom

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.

Xion avatar
Xion
20100310-012443-xion

@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? :)

Gynvael Coldwind avatar
Gynvael Coldwind
20100310-152830-gynvael-coldwind

@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

/dozwolony markdown/

/nie zostanie opublikowany/