Kawałek kodu w makrze

2009-09-01 19:52

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 returnem musimy coś jeszcze zrobić – np.:

  1. bool SprobujCosObliczyc(const data& parametry, int* const wynik)
  2. {
  3.     if (parametryNieSaDobre) { *wynik = 0; return false; }
  4.     /* ... */
  5.     if (cosObliczylismyAleJestZle) { *wynik = 0; return false; }
  6.     /* ... */
  7.     if (tutajCosSiePopsulo) { *wynik = 0; return false; }
  8.     /* ... */
  9.     if (juzPrawieKoniecAleNiestetyLipa) { *wynik = 0; return false; }
  10.     /* ... */
  11.     return true;
  12. }

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:

  1. #define RETURN { *wynik = 0; return false; }

definiowane przed i #undefowane 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 ifie:

  1. if (wyjsc) RETURN; else { /* ... */ }

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ę:

  1. #define RETURN do { *wynik = 0; return false; } while (0)

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.

Tags: , ,
Author: Xion, posted under Programming »


12 comments for post “Kawałek kodu w makrze”.
  1. lukaszw:
    September 1st, 2009 o 21:18

    #define RETURN return((*wynik=0)!=0)

    !=0 dla uniknięcia warning’a :P

  2. Xion:
    September 1st, 2009 o 23:49

    W tym specyficznym przypadku tak. W ogólności niekoniecznie ;]

  3. yarpen:
    September 2nd, 2009 o 9:59

    Generalnie jezeli odczuwa sie pokuse uzycia makra, to nalezy przemyslec design. W tym przypadku wystarczy RAII. (ew. *wynik = 0 na samym poczatku f-kcji).

  4. Malcom:
    September 2nd, 2009 o 17:29

    A co z potepianym goto? Tutaj chyba dobrze pasowaloby.

  5. Aithne:
    September 2nd, 2009 o 21:23

    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ę.

  6. Xion:
    September 3rd, 2009 o 1:10

    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.

  7. Aithne:
    September 3rd, 2009 o 13:01

    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.

  8. Reg:
    September 6th, 2009 o 18:46

    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.

  9. Syriusz:
    September 8th, 2009 o 22:25

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

  10. siso:
    September 29th, 2009 o 1:49

    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.

  11. RV:
    October 6th, 2009 o 1:33

    A co z potepianym goto? Tutaj chyba dobrze pasowaloby.

  12. Groshu:
    October 13th, 2009 o 23:12

    wzorzec pojedynczego wyjścia z funkcji w tym przypadku służył by głównie spowanlianiu kodu :p

Comments are disabled.
 


© 2017 Karol Kuczmarski "Xion". Layout by Urszulka. Powered by WordPress with QuickLaTeX.com.