Posts tagged ‘preprocessor’

if (false) { }

2010-05-22 16:49

Jeśli widzieliśmy już w swoim wystarczająco dużą ilość kodu w C lub C++, to istnieje pewna szansa, że natrafiliśmy na konstrukcję znaną jaką do-while(false). Występuje ona w dwóch wersjach. W pierwszej jest to właściwie goto w przebraniu – dzięki otoczeniu jakiegoś fragmentu kodu taką “pętlą” sprawiamy, że instrukcja break będzie powodowała skok tuż za nią. To pozwala na “wyjście” z danego fragmentu kodu np. wtedy, gdy wystąpi w nim jakiś błąd – oczywiście przy założeniu, że w środku nie ma kolejnej pętli lub switcha ;)
O drugiej wersji pisałem kilkanaście miesięcy temu. Jej zastosowaniem są makra preprocesora zawierająca kilka instrukcji; zamknięcie ich w do-while(false) zamiast w zwykły blok kodu ({ }) gwarantuje nam brak błędów składniowych niezależnie od miejsca użycia takiego makra.

Widać więc, że pętla z zawsze fałszywym warunkiem ma – mimo wszystko – jakieś zastosowanie… A co z analogicznym ifem? Czy if(false) ma jakieś sensowne zastosowanie poza ukazywaniem, że osoba używająca takiej konstrukcji do wyłączania kodu z kompilacji zapomniała o istnieniu dyrektywy #if, że o komentarzach nie wspomnę?…
Okazuje się, że jednak ma – przynajmniej mi dało się takowe zastosowanie znaleźć. Chcąc je zaprezentować, zacznę od takiego oto ciągu ifów:

  1. if (s == "foo") DoSth1(z);
  2. else if (s == "bar") DoSth2(x, y);
  3. else if (s == "baz") DoSth3();

który w założeniu może być oczywiście dowolnie długi i niekoniecznie musi też udawać switcha dla stringów. Rzecz w tym, aby każdy if miał podobną formę – na tyle podobną, że aż prosi się o jej zmakrowanie:

  1. #define C(val,code) else if (s == #val) { code; }

To by wyszło gdyby nie feler w postaci pierwszego ifa, który bynajmniej nie zaczyna się wcale od else‘a. Można jednak sprawdzić, żeby tak było. Jak?… Ano właśnie dzięki zawsze fałszywemu if(false):

  1. if (false) { }
  2. else if (s == "foo") DoSomething1(z);
  3. // ...

Robi on dokładnie nic, ale jednocześnie sprawia, że następujące dalej, prawdziwe ify mają dokładnie taką samą strukturę. Teraz już możemy je skrócić:

  1. #define C(val,code) else if (s==#val) {code;}
  2. if (false) { } C(foo,DoSth1(z)) C(bar,DoSth2(x,y)) C(baz,DoSth3())
  3. #undef C

Naturalnie dla trzech wariantów taka zabawa nie ma specjalnego sensu, ale dla dłuższej listy – czemu nie? Sam użyłem tej sztuczki parę razy, m.in. w parserze wyrażeń matematycznych, gdzie ciąg ifów wybierał wywołanie jednej z ponad 20 wbudowanych w niego funkcji. Nie jest to ładne – trzeba to przyznać – ale cóż, takie jest życie: czasami rozwiązania eleganckiego po prostu nie ma…

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

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

Nieoczekiwana zmiana składni

2008-09-25 8:54

Jedną ze szczególnych cech jest C++ jest obecność różnych “nadmiarowych” właściwości, które pojawiły się w języku właściwie przypadkiem. Bardziej znanym przykładem jest chociażby metaprogramowanie za pomocą szablonów, czego pierwszym przykładem jest słynny kod generujący liczby pierwsze na wyjściu kompilatora. Z początku było to nic innego, jak niezamierzony skutek uboczny procesu rozwijania szablonów, acz teraz zostało to oczywiście świadomie rozwinięte.
Podobnie jest na przykład z preprocesorem, jako że C i C++ są jednymi z niewielu języków, które taki mechanizm posiadają. Na początku służył głównie do deklarowania “stałych”, potem także makr naśladujących funkcje… A przecież można go też użyć do niemal całkowitej zmiany składni języka – tak, by zaczął on udawać inny. Przykładowo, za pomocą kilku poniższych #define‘ów:
#include
#include
#define IF if(
#define THEN )
#define BEGIN {
#define END }
#define WRITELN std::cout<< #define READLN(x) std::getline(std::cin,x)[/cpp] możemy sprawić, że w C++ będzie pisało się prawie jak w Pascalu: [cpp]WRITELN (“Podaj hasło:”); READLN (pass); IF pass == “strasznietajnehaslo” THEN BEGIN WRITELN (“OK”); END;[/cpp] Nie wszystkie niuanse składni innego języka da się oczywiście zasymulować przy pomocy makr, ale w praktyce udaje się bardzo wiele. Mimo to polecam jednak programowanie w C++ przy pomocy C++, a nie Pascala czy Basica ;)

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

Debugowanie preprocesora

2008-06-18 22:48

Preprocesor w C++ to może i przestarzała, ale i całkiem fajna zabawka. Przynajmniej czasami – mimo dość ubogiej logiki – pozwala ona załatać choć niektóre braki języka. A niekiedy pozwala też na pewne sztuczki, jakie ciężko byłoby osiągnąć inaczej. W końcu, można go też używać do tego, w czym jest najlepszy: do automatycznego wklejania powtarzających się fragmentów kodu w wielu miejscach.
Ponieważ jednak preprocesor działa na kodzie jak na tekście, to rezultaty mogą być czasami zadziwiające – zwłaszcza dla kompilatora. Może on nam wówczas pokazać błąd w linijce, która jest całkowicie poprawna… Ale tylko z dokładnością do zawartego w niej makra, które po rozwinięciu generuje błąd składniowy lub semantyczny. Weźmy chociażby chyba najprostszy przypadek tego typu:

  1. #define COUNT 10;
  2. int buf[COUNT]; // błąd!

W nim łatwo o wykrycie pomyłki, lecz w rzeczywistych sytuacjach może to być zadanie nie lada. Zwłaszcza wtedy, gdy użyte makro jest skomplikowane, a my przecież nie możemy przełączyć preprocesor w tryb pracy krokowej i śledzić, jak jest ono rozwijane.

Możemy jednak przynajmniej podejrzeć ostateczne rezultaty. Wprawdzie domyślne każdy plik źródłowy po przetworzeniu przez preprocesor trafia od razu do kompilatora, jednak zachowanie to można łatwo zmienić. Możliwe jest na przykład zrzucenie wstępnie przetworzonego źródła do pliku, który to możemy potem przejrzeć i zobaczyć, w co ostatecznie nasze makra się zmieniły.
W Visual C++ takie zachowanie ustawia się we właściwościach projektu, na zakładce Configuration Properties > C/C++ > Preprocessor. Tam możemy wybrać, czy chcemy, by rezultaty działania preprocesora na plikach .cpp były zapisywane do osobnych plików (z rozszerzeniem .i). Wachlarz opcji umożliwia też określenie, czy mają być do nich dołączane numery linii lub komentarze.

Opcje preprocesora w Visual C++

Włączenie tych opcji bywa przydatne, jeśli dostajemy na linijkach kodu z użyciem makr dostajemy komunikaty o błędach kompilatora, które są zupełnie “z innej bajki”. Ponadto w ten sposób można też zobaczyć, jakie fragmenty kodu są kierowane do kompilacji za pomocą dyrektyw typu #ifdef, co też bywa przydatne.
Pamiętajmy jednak, że wygenerowane w ten sposób pliki .i są prawie zawsze bardzo, bardzo duże – rzędu kilku megabajtów – gdyż zawierają treść wszystkich dołączonych nagłówków. Dlatego, ze względu na szybkość budowania projektu, nie powinniśmy ich generować poza przypadkami debugowania preprocesora.

Tags: ,
Author: Xion, posted under Applications, Programming » Comments Off on Debugowanie preprocesora
 


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