Archive for Programming

Konstruktory bezparametrowe

2008-06-28 12:12

W C++ konstruktor domyślny jest generowany automatycznie, jeśli w klasie nie zostanie zdefiniowany żaden inny. W przeciwnym wypadku nie jst on tworzony, co może prowadzić do sytuacji, gdy klasa posiada jedynie takie konstruktory, które wymagają podania parametrów. Jest ona niekorzystna przynajmniej z dwóch powodów:

  1. Gdy dziedziczymy po takiej klasie, musimy jakoś zapewnić, że w klasie pochodnej zostanie wywołany konstruktor klasy bazowej. Zwykle robi się to za pomocą listy inicjalizacyjnej:
    1. Derived(int param1, int param2) : Base(param1, param2) { /*... */ }

    Kiedy jednak zmieni się postać konstruktora klasy bazowej, wówczas modyfikacja będzie musiała dotyczyć wszystkich bezpośrednich potomków tej klasy. W nowej wersji standardu C++ ma być aczkolwiek wprowadzone nowe zadanie dla słowa kluczowego using, które pozwoli na automatyczne utworzenie konstruktorów “przekaźnikowych” – takich, których jedyną rolą jest wywołanie konstuktorów bazowych z takimi samymi parametrami jak te otrzymane w konstruktorze pochodnym. Jeśli jednak przy okazji chcemy coś zmienić czy pominąć, to nie ma rady: trzeba całą tę “sztafetę” zakodować ręcznie.

  2. Jeszcze gorzej jest, kiedy klasa bez konstruktora domyślnego jest wirtualną klasą bazową. Wówczas o jej prawidłowej inicjalizacji muszą pamiętać nie tylko klasy bezpośrednio pochodne, ale w ogóle wszystkie potomne! Dokładnie tak: nawet w piątym czy dziesiątym pokoleniu hierarchii o korzeniu w wirtualnej klasie bazowej, klasy pochodne muszą w swoich konstruktorach wywoływać konstruktory owego korzenia. To oczywiście sprawia, że każda zmiana musi być rozpropagowana na całe to drzewo dziedziczenia (a właściwie graf, bo pewnie zawiera on cykle :>).

Dlatego też klasy będące przede wszystkim klasami bazowymi w dziedziczeniu dobrze jest wyposażać w konstruktory bezparametrowe. Może to aczkolwiek wymagać wyróżnienia w obiekcie stanów Zainicjowany-Niezainicjowany, które powinny być sprawdzane w jego metodach i w razie potrzeby odpowiednio sygnalizowane.

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

Bo to zła instrukcja była…

2008-06-24 21:56

Ostatnio na forum Warsztatu wyrosła dyskusja wokół niepozornej instrukcji break. Zaczęło się od zasłyszanej opinii, że korzystanie z niej jest objawem złego stylu programowania i że jest generalnie niezalecane. Argumentem na rzecz takiego twierdzenia jest to, że break jest podobny w swoim działaniu do etykiet i “wyklętej” instrukcji goto:

  1. while (WarunekPetli())
  2. {
  3.     if (CosSieStalo()) goto ZaPetla;   // prawie jak break
  4. }
  5. ZaPetla:
  6.     // ...

W sumie wyszła z tego całkiem długa polemika, która ujawniła kilka ciekawych faktów. Po pierwsze: przejrzystość kodu jest w całkiem dużym stopniu kwestią subiektywną i o ile jednemu może przeszkadzać brak (lub niewystarczająca ilość) komentarzy, to dla drugiego o wiele ważniejsze może być używanie lub nieużywanie określonych konstrukcji językowych. Po drugie: oprócz wspomnianego już goto czy wywołanego we wspomnianym wątku breaka, do instrukcji potencjalnie niepożądanych swobodnie można zaliczyć bardzo wiele innych rozwiązań – zależnie od upodobań, poczucia ‘słuszności’, bieżącej fazy księżyca i pewnie mnóstwa jeszcze innych kryteriów. I tak dostało się chociażby pętlom “nieskończonym” (typu while(true) lub for(;;)), wspomniano coś o wyjątkach, a sam napomknąłem o instrukcji continue lub “przedwczesnym” return:

  1. void Funkcja()
  2. {
  3.     if (/* warunek wyjścia */) return;
  4.     // reszta instrukcji
  5. }

Teoretycznie można by wszystkie te konstrukcje usunąć w imię ideologicznej czystości programowania strukturalnego, w którym każdy blok ma dokładnie jedno wejście i wyjście. Ale tu na szczęście wkracza wniosek trzeci: języki programowania używane w praktyce nie są żadnymi doskonałymi modelami, nad którymi należy się zachwycać. To narzędzia i jako takie powinny być przede wszystkim wygodne w użytkowaniu.
Więc nie warto drzeć kotów o sens stosowania break czy nawet goto (która to instrukcja też ma swoje zastosowania) i wydawać ogólne sądy na temat ich ‘nieprofesjonalności’. Bo nawet jeśli ta czy inna instrukcja rzekomo jest zła, to korzystający z niej kod nadal może być dobry, czytelny i łatwy do modyfikacji… Łącznie z refaktoringiem, który tego czy innego breaka może przecież usunąć ;P

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

Znaki białe i bielsze

2008-06-22 22:23

W językach programowania i opisu (np. HTML lub XML) ważną rolę odgrywają tzw. białe znaki (whitespaces). Nie są one nigdy wypisywane ani drukowane jako symbole, ale zajmują miejsce na ekranie i czasem powodują dodatkowe efekty (jak na przykład przejście do nowego wiersza w przypadku znaku końca linii – line feed). W wielu przypadkach parsery tekstowe ignorują większą ilość tego typu znaków, pozwalając dzięki temu na dowolne formatowanie kodu. Ale czasami jest wręcz odwrotnie: istnieje mianowicie ezoteryczny język programowania – zwany oryginalnie Whitespace – w którym to właśnie znaki niedrukowane (a dokładniej spacja, tabulator i znak końca wiersza) są jedynymi, które są interpretowane jako kod.

Jakie jednak znaki należy uważać za niedrukowane?… Sprawa nie jest taka prosta, gdyż, podobnie jak rozróżnienie liter wielkich i małych, zależy ona od ustawień lokalnych języka. Większość języków programowania dysponuje aczkolwiek odpowiednią metodą sprawdzenia. W C++ jest to na przykład standardowa funkcja isspace. Ponieważ jednak w grę wchodzą ustawienia kulturowe, rezultaty zależą od języka systemu operacyjnego (przynajmniej teoretycznie).
W Unicode mamy na szczęście standardowo zdefiniowane, które znaki są traktowane jako białe. Te właśnie są sprawdzane przez metody w rodzaju java.lang.Character.isWhitepace w Javie czy System.Char.IsWhiteSpace w .NET. Działają one tak samo niezależnie od kontekstu kulturowego.

A co mamy zrobić, jeśli optujemy za ANSI? Wówczas możemy ustalić sobie swój własny zbiór znaków interpretowanych jako białe. Według mnie najlepszym zestawem jest następujący:

  1. bool IsCharWhitespace(char c)
  2. {
  3.     return (c == 0x09 || c == 0x0A || c == 0x0D || c == 0x20);
  4. }

Te znaki to kolejno: tabulator (poziomy) (\t), line feed (\n), znak powrotu karetki (carriage return, \r) i zwykła spacja. Za takim wyborem przemawia chociażby fakt, że właśnie te znaki są uważane w XML za białe i że obejmują różne standardy kodowania końca wiersza na różnych platformach. Dwa inne znaki: tabulator pionowy (0x0B) i form feed (0x0C) są jeszcze uważane za białe w C, ale stosuje się je chyba zbyt rzadko, aby trzeba się było nimi przejmować :)

Tags: ,
Author: Xion, posted under Programming » Comments Off on Znaki białe i bielsze

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

Stos wywołań funkcji

2008-06-13 13:42

Jedną z bardziej przydatnych informacji podczas znajdowania przyczyn błędów wykonania jest śledzenie stosu (stack trace; właściwie to nie ma dobrego polskiego tłumaczenia, jak zwykle zresztą :]). Dzięki niemu możemy mieć informacje o kolejnych wywołaniach funkcji, które doprowadziły błędu wykonania. Zwykle stack trace jest dostępny z poziomu złapanego obiektu wyjątku – np. poprzez właściwość System.Exception.StackTrace w .NET czy metody java.lang.Throwable.get/printStackTrace w Javie.

Standardowa klasa exception w C++ nie posiada podobnej funkcjonalności, bo w tym języku stos nie jest automatycznie śledzony. Przy pewnym wysiłku możemy aczkolwiek zapewnić sobie podobną funkcjonalność – chociażby tak:

  1. typedef std::list<std::string> StackTrace;
  2.  
  3. class Trace
  4. {
  5.     private:
  6.         static StackTrace ms_Trace;
  7.  
  8.     public:
  9.         Trace(const std::string& item) { ms_Trace.push_back (item); }
  10.         ~Trace()    { ms_Trace.pop_back(); }
  11.  
  12.     friend const StackTrace& GetStackTrace()    { return ms_Trace; }
  13. };
  14. StackTrace Trace::ms_Trace;    // inicjalizacja pola statycznego (w .cpp)
  15.  
  16. // makro (dla Visual C++)
  17. #define GUARD Trace __trace(__FUNCTION__ " [" __FILE__ ", line " __LINE__ "]")

Przechowujemy po prostu globalną listę z danymi wszystkich wywoływanych funkcji. Elementy do niej dodawane są przy wejściu w każdą funkcję, która na początku ma makro GUARD:

  1. int Foo() { GUARD; /* ... */ }

zaś zdejmowane wtedy, gdy pomocniczy obiekt typu Trace wyjdzie poza swój lokalny zasięg – czyli po opuszczeniu funkcji. To sprawia, że na liście mamy zawsze informacje o wszystkich wywołaniach funkcji prowadzących do aktualnego miejsca (jakie one są, zależy od kompilatora; użyte wyżej makro __FUNCTION__ nie jest na przykład standardową częścią C++). Tworząc obiekt wyjątku, możemy więc zapisać kopię tej listy, by można ją było odczytać w bloku catch i wyświetlić.
Rozwiązanie nie jest oczywiście bardzo ładne, bo wymaga dodatkowej linijki w każdej funkcji. Zdaje się jednak, że nie ma żadnego sposobu, by tę niedogodność wyeliminować. Ja przynajmniej takiego nie znam :)

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

Nadmuchiwanie obiektów

2008-06-08 15:24

Od kilku dni nadspodziewanie popularnym sportem jest piłka nożna, co zapewne nie jest przypadkowe ;-) A jeśli już chodzi o piłkę, to musi być ona odpowiedniej jakości. I mówię tu o tym niewielkim, prawie okrągłym obiekcie: aby był przydatny, musi być… nadmuchany :) I właśnie o ‘nadmuchiwaniu’ powiem dzisiaj słów kilka.
Chodzi mi oczywiście o graficzny efekt rozszerzania się obiektu 3D, wyglądający tak, jakby ktoś w ów obiekt pompował powietrze. Można by pomyśleć, że osiągnięcie go jest niezmiernie trudne, bo przecież wchodząca w grę mechanika płynów (ruch gazu “wypełniającego” obiekt) jest jakąś kosmiczną fizykę. Ale – jak to często bywa – przybliżony a wyglądający niemal równie dobrze rezultat możemy dostać o wiele mniejszym wysiłkiem.

Jak? Sztuczka jest bardzo prosta. Aby nasz mesh zaczął się rozciągać na wszystkie strony, należy po prostu odpowiednio poprzesuwać mu wierzchołki. Wspomniane ‘wszystkie strony’ oznaczają tak naprawdę tyle, że każdy punkt powierzchni obiektu oddala się od niej w kierunku prostopadłym. Kierunek tego ruchu jest wyznaczony po prostu przez normalną.
Efekt jest w istocie trywialny i jego kod w HLSL/Cg może wyglądać na przykład tak:

  1. float fPumpFactor;  // współczynnik nadmuchania obiektu
  2.  
  3. struct VS_INPUT
  4. {
  5.     float3 Position : POSITION;
  6.     float3 Normal : NORMAL;
  7.     // ...
  8. }
  9.  
  10. void vs_main(VS_INPUT In, /* ... */)
  11. {
  12.     // nadmuchujemy
  13.     In.Position += fPumpFactor * normalize(In.Normal);
  14.  
  15.     // (reszta vertex shadera)
  16. }

Wśród zastosowań tego prostego triku można wymienić chociażby wyświetlanie poświaty wokół obiektów. Wystarczy do tego narysować obiekt najpierw w wersji zwykłej, a potem półprzezroczystej i nieco napompowanej.

A tak prezentują się rezultaty, gdy zaczniemy nadmuchiwać poczciwą futbolówkę:

Nadmuchana piłka nożna (PF = -1) Nadmuchana piłka nożna (PF = 2) Nadmuchana piłka nożna (PF = 8)

Na Euro jak znalazł ;]

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

Skróty i linki NTFS

2008-06-07 15:26

Tworzenie skrótu w Windows VistaSystem Windows od niepamiętnych czasów (a dokładniej od wersji 95) pozwala na tworzenie skrótów (shortcut), czyli plików będących odwołaniami do innych plików lub katalogów. Skróty mają zwykle rozszerzenie .lnk i występują głównie w katalogach tworzących strukturę Menu Start.
Wewnętrznie nie ma w nich nic pomysłowego, gdyż są to zwykłe pliki w formacie na wpółbinarnym, zawierające docelową ścieżkę lub URI (np. adres internetowy). Ich obsługa realizowana jest na poziomie Eksploratora Windows, więc nie są one specjalnie funkcjonalne. Otwarcie pliku skrótu w dowolnym programie nie spowoduje na przykład odczytania zawartości docelowego pliku, lecz samego skrótu (który zwykle ma kilkaset bajtów). Podobnie skrót do katalogu Foo o nazwie Bar.lnk nie spowoduje, że do pliku Foo/Coś.txt odwołamy się poprzez Bar.lnk/Coś.txt.

Ilustracja działania hardlinków
Ilustracja działania linków twardych
(Źródło: Wikipedia)

Taką funkcjonalność zapewniają jednak linki twarde i symboliczne, obecne w nowszym wersjach NTFS. Są one bardzo podobne do swoich odpowiedników w systemach plików ext2 i ext3 (uniksowych). I tak:

  • Link twardy (hardlink) to jakby alternatywna nazwa dla pliku, czyli nowa pozycja systemu plików odwołująca się do tych samych danych. Używając którejkolwiek z nich, można zmienić zawartość pliku i modyfikacje te będące widoczne dla innych, nawet jeśli uzyskają oni dostęp do pliku za pomocą innych jego nazw.
    W Windows 2000 i nowszych linki twarde można tworzyć poleceniem fsutil hardlink create link cel, a w Windows Vista dodatkowo mklink /H link cel. Programowo robi się funkcją WinAPI o nazwie CreateHardLink.
  • Link symboliczny (symlink) jest z kolei łączem do istniejącej nazwy pliku lub katalogu; zamiast do faktycznych danych na dysku odwołuje się więc do innego elementu systemu plików. To właściwie taki skrót pozbawiony ograniczeń, o których wspominałem na początku. Możliwe jest jednak, aby programy operowały bezpośrednio na linkach symbolicznych, o ile odpowiednio ładnie o to poproszą :)
    W miarę porządna implementacja obsługi symlinków jest niestety zawarta dopiero w Windows Vista; można je tworzyć programem mklink (trzeba jednak zaznaczać jawnie, czy robimy link do pliku czy do katalogu). We wcześniejszych wersjach możliwe jest jedynie tworzenie NTFS junctions, będących “linkami” tylko do katalogów oraz obarczonych innymi ograniczeniami (na przykład Eksplorator w Windows XP dość słabo sobie z nimi radzi).

Jak nietrudno zauważyć, podstawową zaletą linków nad zwykłymi skrótami jest to, że te pierwsze są obsługiwane na poziomie systemu plików. Wobec tego działają one w dowolnym programie i zupełnie przezroczyście dla aplikacji. Trochę szkoda, że ten przydatny drobiazg pochodzący z systemów uniksowych dopiero teraz pojawia się w Windows… ale przecież lepiej późno niż wcale ;]

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


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