Jeśli staramy się pisać kod zgodnie z dobrymi praktykami programowania w C/C++, to niezbyt często korzystamy z dyrektywy #define
. W większości przypadków ogranicza się to zresztą do zdefiniowania jakiegoś symbolu służącego do kompilacji warunkowej (np. WIN32_LEAN_AND_MEAN
). Od czasu do czasu bywa jednak tak, że chcemy użyć #define
w jego – teoretycznie – głównym zastosowaniu, czyli do zastępowania jednego kawałka kodu innym.
Uzasadnione przypadki takiego postępowania (do których nie należą np. makra udające funkcje, czyli sławetne #define min
/max
) to tylko takie sytuacje, w których bez #define
rzeczywiście musielibyśmy wiele razy pisać (prawie) to samo. Moim ulubionym przykładem czegoś takiego jest wychodzenie z funkcji w wielu miejscach, gdzie przed każdym return
em musimy coś jeszcze zrobić – np.:
Zwyczajne ustawienie *wynik = 0;
na początku funkcji będzie nieakceptowalne, jeśli chcemy, by w przypadku rzucenia wyjątku w którymś z /*... */
podana do funkcji zmienna pozostała bez zmian.
Prostym rozwiązaniem wydaje się więc makro:
definiowane przed i #undef
owane tuż po funkcji. Pomysł byłby dobry, gdyby nie to, że w tej postaci makro to powodowałoby błędy składniowe. Kiedy? Chociażby w takim if
ie:
Tutaj bowiem kompilator nie znalazłby instrukcji if
pasującej do else
, co jest naturalnie błędem. Powodem tego jest średnik po wystąpieniu makra, generujący pustą instrukcję i sygnalizujący (przedwczesny) koniec klauzuli if
.
Jak ominąć ten problem? Odpowiedź jest może zaskakująca: instrukcje trzeba zamknąć w… pętlę:
Jak nietrudno zauważyć, taka pętla wykona się dokładnie raz (i co więcej, kompilator o tym doskonale wie, zatem może ją zoptymalizować – tj. wyrzucić wszystko poza zawartością). Działanie jest więc takie same. Różnica polega na tym, że teraz takie makro może być używane w dowolnym miejscu bez żadnych niespodziewanych efektów.
#define RETURN return((*wynik=0)!=0)
!=0 dla uniknięcia warning’a :P
W tym specyficznym przypadku tak. W ogólności niekoniecznie ;]
Generalnie jezeli odczuwa sie pokuse uzycia makra, to nalezy przemyslec design. W tym przypadku wystarczy RAII. (ew. *wynik = 0 na samym poczatku f-kcji).
A co z potepianym goto? Tutaj chyba dobrze pasowaloby.
Zamiast potępianego goto użyłeś więc jeszcze bardziej potępianych makr. Super. Tak to jest, jak się wbije do głowy, że goto jest zawsze złe. A kombinowanie z RAII to w tym przypadku Little Boy na muchę.
Eee… Czy wszyscy umyślnie ignorują ten mały skrót “np.”? Tak, to był tylko przykład, w którym *można* chcieć użyć makr do prostego copy-paste fragmentów kodu – ale oczywiście nie trzeba. Chciałem tylko pokazać, jak wtedy zrobić to poprawnie, i tyle.
Co nie zmienia faktu, że dla takiego podstawiania najczęściej można użyć goto – przynajmniej się nie można nadziać na jakąś literówkę w makrze i zastanawiać się, czemu mamy milion błędów.
Ale dziwna kombinacja :) Ciekawa, ale to kolejny dowód na to jak pokrętny jest C++. Moim zdaniem lepiej po prostu użycia takich makr obejmować w swoim kodzie w nawiasy klamrowe, tak jak if { MAKRO; } else …, albo świadomie nie pisać tego średnika po użyciu makra.
Hm, ja staram się stosować wzorzec pojedynczego wyjścia z funkcji (nie pamiętam ja się to dokładnie nazywało). I chyba miałby zastosowanie w powyższym przykładzie:
{
bool result = true;
result = parametryNieSaDobre;
if(result)
{
/* … */
result = cosObliczylismyAleJestZle;
}
if(result)
{
/* … */
result = tutajCosSiePopsulo;
}
if(result)
{
/* … */
result = juzPrawieKoniecAleNiestetyLipa;
}
if(!result)
*wynik = 0;
return result;
}
Jeżeli chcemy, by w obliczu wyjątku przekazana zmienna pozostała bez zmian, a w pozostałych przypadkach chcemy zwrócić z funkcji wiele wartości, można ją przekazać do funkcji przez, hmmm… wartość (zamiast przez wskaźnik), zaś zwracać z funkcji krotkę (tuple) z odpowiednio ustawionymi składowymi.
A co z potepianym goto? Tutaj chyba dobrze pasowaloby.
wzorzec pojedynczego wyjścia z funkcji w tym przypadku służył by głównie spowanlianiu kodu :p