Archive for Programming

Obiektowy szowinizm

2008-08-08 22:00

Od jakiegoś czasu najpopularniejszymi językami programowania są albo te stricte obiektowe, albo chociaż oferujące przynajmniej najważniejsze cechy tego modelu programowania (czyli dziedziczenie, polimorfizm i metody wirtualne). Prawdopodobnie większość powstającego kodu realizuje więc założenia OOP-u co najmniej w takim stopniu, że można go uznać za spełniający paradygmat obiektowy – albo chociaż podpadający pod obiektowość bardziej niż pod cokolwiek innego.
Ano właśnie: warto od czasu do czasu zauważyć, że oprócz OOP-u istnieje też “coś innego”, i że obiektowy model programowania wcale nie jest jedynym. Nie musi być on też ani ostatecznym, ani najbardziej uniwersalnym. A już tym bardziej nie jest on jedynie słusznym.

Jako koderzy podążający za wytycznymi programowania obiektowego często jednak tak uważamy – mniej lub bardziej świadomie. Zwłaszcza, że obecnie można zajmować się programowaniem całkiem długo i nawet nie słyszeć o innych paradygmatach niż obiektowy. A jeśli nawet ktoś na podobne “ciekawostki” lub na “dziwaków” opowiadających się na innym podejściem do kodowania, to najpewniej nie będzie to miało żadnego wpływu na jego dobre samopoczucie i przekonanie o wyższości OOP-u nad czymkolwiek innym.
Bo przecież łatwo znaleźć programistów myślących bardzo podobnie i bez wysiłku wyciągnąć całe mnóstwo silnych argumentów na poparcie swoich racji. Bodaj najczęściej wykorzystywanym jest ten mówiący o odpowiednim poziomie abstrakcji, który jest jakoby immanentną cechą programowania obiektowego. Z jednej strony jest on bowiem wyższy od brzydkiego programowania strukturalnego, dzięki czemu kod obiektowy łatwiej jest napisać i zrozumieć. Jednocześnie nie jest to też poziom zbyt wysoki, przez co nadal wiadomo, jak nasz program działa i jak z grubsza sprawuje się pod względem wydajnościowym. Tego samego nie można rzecz jasna powiedzieć wtedy, gdy kodujemy funkcyjnie czy deklaratywnie.

Wszystko to brzmi rozsądnie i wydaje się słuszne. W rzeczywistości jednak posługiwanie się takimi kryteriami jest zwykłym nadużyciem. Na takiej zasadzie można by narzekać na to, że nie da się przygotować omletu przy pomocy samego widelca (że pozwolę sobie posłużyć się analogią kulinarną ;]). Należy bowiem zawsze mieć na uwadze to, o jakim zastosowaniu mówimy. Programowanie nie jest przecież dziedziną abstrakcyjną, ale jak najbardziej praktyczną, niezależnie od tego, jak bardzo usiłowalibyśmy ją steoretyzować.
Dlatego też absurdem jest twierdzenie o wyższości programowania obiektowego nad innymi paradygmatami. Bo czy bawilibyśmy się w tworzenie klas w przypadku takiego oprogramowania jak firmware procesora czy karty graficznej, działającego w ścisłym powiązaniu ze sprzętem? I czy odpowiadałoby nam, gdyby zamiast określania za pomocą znaczników wyglądu fragmentów strony WWW bylibyśmy zmuszeni tworzyć dla każdego z nich osobny obiekt i ustawiać jego właściwości?…

Nie zamykajmy się więc w swoim obiektowym światku, nawet jeśli czujemy się w nim nadzwyczaj dobrze. Znajomość innych sposobów kodowania może nam bowiem tylko pomóc.

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

Czy void jest typem

2008-08-05 17:21

Jak wiadomo, w C i C++ nie ma procedur. Są tylko funkcje, czyli podprogramy obliczające i zwracające jakąś wartość. Jednak możliwe jest zadeklarowanie takiej funkcji, która w istocie nic nie zwraca; służy do tego słowo kluczowe void, rzecz jasna. Pełni ono wówczas rolę nazwy typu zwracanego przez funkcję:

  1. void Procedure { /* ... */ }

Ktoś mniej doświadczony (lub o mniej schematycznym podejściu do C++) mógłby wobec tego spytać: Czy void jest wobec tego rzeczywiście typem, takim jak chociażby int czy char, czy jednak jest to tylko dziwne słowo na oznaczenie procedur w C++?

Odpowiedzią na powyższe pytanie może być tylko stanowcze… i tak, i nie :)
Z jednej strony wiemy na przykład, że void może występować w nagłówkach funkcji, i to nie tylko w miejscu typu wartości zwracanej, ale też na liście parametrów. Jednakże w tym drugim przypadku oznacza to jedynie to, iż funkcja tak naprawdę nie przyjmuje żadnych argumentów. W C++ można spokojnie pominąć to użycie void, osiągając dokładnie ten sam efekt – funkcji bezparametrowej. (Warto aczkolwiek zaznaczyć, że w C jest inaczej).
Inną rolą void sugerującą jego “typowość” jest istnienie wskaźników typu void*. Do tego typu można niejawnie skonwertować dowolny wskaźnik:

  1. void* p = new CFoo;

Operacja odwrotna nie jest jednak możliwa (wymaga co najmniej static_cast), a sam void* nie jest tak naprawdę pełnoprawnym wskaźnikiem – nie pozwala bowiem na dereferencję. Ponadto nie istnieje też typ referencyjny void&.
Wreszcie, chyba najbardziej egzotycznym użyciem void jest rzutowanie na niego wyrażenia innego typu. Jest to jak najbardziej możliwe i dotyczy zwykle sytuacji podobnych do tej:

  1. // Pokaż okno komunikatu tylko z przyciskiem OK
  2. static_cast<void>(MessageBox(NULL, _TEXT("Błąd!"), NULL, MB_OK | MB_ICONERROR));

Używamy tutaj funkcji, która zwraca rezultat (int), ale on nas nie interesuje. Pokazujemy przecież komunikat z jednym przyciskiem OK (więc nie musimy pobierać decyzji użytkownika), a ponadto nie ma tutaj żadnej wyobrażalnej możliwości wystąpienia błędu, o którym mógłby nas poinformować rezultat funkcji. Niektóre kompilatory z włączonym maksymalnym poziomem ostrzeżeń mogą aczkolwiek ostrzegać o ignorowaniu potencjalnie przydatnego rezultatu funkcji; rzutowanie na void pozwala takich ostrzeżeń uniknąć. Jednak sam fakt, iż takie rzutowanie jest możliwe, nie implikuje od razu możliwości zadeklarowania zmiennej typu void. To w jest rzecz jasna niemożliwe.

Cóż więc z tym fantem zrobić? No cóż, sprawa jest raczej śliska. Zapewne dałoby się jeszcze znaleźć przynajmniej po kilka argumentów zarówno za tym, że void typem jest, jak i że nie jest. Ale od rozstrzygnięcia ważniejsze jest raczej to, aby z tego… słowa kluczowego (pozostańsmy przy neutralnej wersji) umieć korzystać. Co, swoją drogą, nie jest takie trudne ;)

Tags: ,
Author: Xion, posted under Programming » Comments Off on Czy void jest typem

Prawie jak mapa

2008-08-04 17:12

Każda porządna biblioteka pojemników posiada kontener w typie mapy lub słownika, który służy do przechowywania par klucz-wartość, czyli odwzorowywania jednych na drugie. Zwykle pojemnik taki jest zaimplementowany przy pomocy odpowiednio zarządzanej struktury drzewiastej. I tak np. w C++ mamy od tego klasę std::map, w .NET – System.Collections.Generic.Dictionary, a w Javie cały zestaw klas implementujących interfejs java.util.Map.

Czasami jednak korzystanie z tego typu rozwiązań może być strzelaniem z armaty do komara. Jeśli bowiem:

  • nasze klucze nie są typami złożonymi (np. łańcuchami znaków), a raczej wartościami wyliczeniowymi
  • nie jest ich zbyt dużo, zwłaszcza w stosunku do ilości pamięci, którą możemy zająć
  • przypisane kluczom wartości nie są dużymi obiektami
  • istnieją dla nich rozsądne ustawienia domyślne

to możemy zastosować o wiele prostsze rozwiązanie. Polega ono na użyciu typu wyliczeniowego dla kluczy:

  1. enum KEYS { KEY_0, KEY_1, /*... */ KEY_COUNT };

oraz zwykłej tablicy do przechowywania wartości (tutaj typu bool):

  1. bool dictionary[KEY_COUNT];

Proste i skuteczne, a i nieco wygodniejsze składniowo niż standardowe std::map. A także wybitnie mało odkrywcze; podejrzewam, że każdy średnio zaawansowany programista miał okazję zetknąć się z podobną “sztuczką”. Co jednak nie znaczy, że nie warto o niej czasem wspomnieć i zrobić dobry uczynek w postaci propagowania dobrych i sprawdzonych rozwiązań ;-)

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

Przeciążanie przecinka

2008-07-31 16:34

Przecinek to taki niepozorny znak, którego grubo ponad 90% zastosowań ogranicza się zapewne do oddzielania parametrów funkcji (czy to w deklaracji, czy to w wywołaniu). Niektóre języki używają też go do separowania indeksów wielowymiarowych tablic, ale do nich C++ akurat nie należy.
Zamiast tego przecinek jest tam operatorem, o czym rzadko się pamięta. Być może dlatego, że ma on najniższy priorytet spośród wszystkich operatorów. Wydaje się też, że domyślne jego działanie (obliczenie wszystkich argumentów, a następnie zwrócenie ostatniego) nie jest specjalnie przydatne – jeśli w ogóle.

Można je jednak zmienić. Tak, tak – operator przecinka można przeciążać, jakkolwiek zaskakujące i przekombinowane by się to wydawało. W rzeczywistości jest on bowiem zwyczajnym operatorem binarnym o łączności lewostronnej i wspomnianym najniższym priorytecie. A przeciążyć można go, pisząc funkcję operator,() – dokładnie tak samo, jak każdego innego dwuargumentowego operatora.
Do czego może to służyć? Otóż względnie typową sztuczką jest użycie przedefiniowanego przecinka do konstrukcji obiektów złożonych z wielu elementów, np. wektorów lub macierzy. Oto przykład przeciążenia dla standardowej klasy vector:

  1. #include <vector>
  2. template <typename T> std::vector<T> operator , (std::vector<T> v, const T& obj)
  3. {
  4.     v.push_back (obj);
  5.     return v;
  6. }

Pozwala to na inicjalizację wektora w taki oto nietypowy sposób:

  1. std::vector<int> ints = (std::vector<int>(), 1, 2, 3, 4, 5);

Pierwszy element tego ciągu (pusty wektor) jest konieczny, gdyż w przeciwnym razie użyty zostałby standardowy operator przecinka.

Na pierwszy rzut może to wyglądać efektownie. Pamiętajmy jednak, że przecinek ten pozostaje wciąż przecinkiem, zachowując chociażby swoją główną cechę szczególną, czyli priorytet. W szczególności próba dodania do wektora kolejnych elementów w ten sposób:

  1. ints = ints, 6, 7, 8, 9;

zakończy się niezupełnie po naszej myśli…
Mimo to być może warto spróbować wymyślić dla tego operatora jakieś własne, w miarę sensowne zastosowanie. Możliwe, że okaże się on wcale nie tak nieużyteczny, na jakiego zdaje się wyglądać :]

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

Wektory w różnych przestrzeniach

2008-07-28 17:26

Kiedy zaczynamy uczyć się programowania, dowiadujemy się, że zmiennym przypisane są zawsze konkretne typy, których należy się trzymać. Trochę później, gdy zajmujemy się już shaderami i programowalnym potokiem graficznym, okazuje się, że większość wartości jest tam tego samego typu (wektor trzech lub czterech floatów). Różnią się one jednak przypisaną im semantyką, czyli znaczeniem w opisie wierzchołka lub piksela (POSITION, NORMAL, COLOR, i tak dalej).
Jednak sama semantyka nie jest jedyną cechą charakterystyczną danego wektora. Nawet mając do czynienia z dwoma wektorami “geometrycznymi” (czyli opisującymi pozycję lub normalną, a nie np. kolor), nie zawsze możemy wykonywać na nich łączne operacje. Należy bowiem zadbać o to, by wektory te znajdowały się w tej samej przestrzeni. W przeciwnym razie konieczne jest odpowiednie przekształcenie, co sprowadza się do pomnożenia przez jakąś macierz.

Aby łatwiej zorientować się, z którą przestrzenią mamy do czynienia, można stosować odpowiednie nazewnictwo, na przykład w postaci sufiksów. Przyrostki te mogą być przyporządkowane choćby następująco:

  • _o (np. vPos_o) niech odpowiada przestrzeni obiektu (object space), związanej z układem lokalnym konkretnej instancji modelu
  • _w (np. vPos_w) może oznaczać globalną przestrzeń świata (world space)
  • _v to z kolei przestrzeń widoku (view space), związana z pozycją obserwatora
  • _t możemy przyporządkować do tangent space przy obliczeniach związanych z oświetleniem
  • _l może w końcu być przyrostkiem wektorów w przestrzeni związanej z pozycją konkretnego światła

Oczywiście cały ten pomysł na kilometr śmierdzi sławetną notacją węgierską, jednak w tym przypadku “dekorowanie” nazw zmiennych ma znacznie głębszy sens. Błędów spowodowanych operacjami na wektorach z różnych przestrzeni nie wykryje nam bowiem żaden kompilator, a ich samodzielne wyśledzenie bywa bardzo trudne (podobnie jak większości innych błędów matematycznych). Dlatego należy zawsze zwracać baczną uwagę na to, z jaką przestrzenią aktualnie pracujemy, i stosować jeśli nie specjalne nazewnictwo, to chociaż szczegółowe i jednoznaczne komentarze.

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

Jak działają metody wirtualne

2008-07-24 17:16

Zauważyłem, że niektóre osoby mają problem z właściwym wykorzystaniem metod wirtualnych i polimorfizmu, nie widząc czasami możliwości ich wykorzystania nawet w najbardziej oczywistych przypadkach. Być może powodem jest coś w rodzaju braku zaufania do całego mechanizmu, który jest spowodowany wrażeniem, że kompilator dokonuje przy jego okazji jakichś pseudomagicznych sztuczek.

A tak naprawdę z metodami wirtualnymi sprawa jest dosyć prosta. Pomiędzy nimi a zwykłymi metodami różnica polega wyłącznie na sposobie ich wywoływania.
W tym pierwszym przypadku użycie zwykłej metody względem obiektu:

  1. CObject* pObject = new CObject;
  2. // ...
  3. pObject->Method();

jest zamieniane w wynikowym kodzie na normalne wywołanie funkcji z dodatkowym parametrem:

  1. CObject_Method (pObject); // nazwa tej funkcji jest czysto umowna

Ten parametr jest potem dostępny jako wskaźnik this wewnątrz metody i pozwala odwoływać się do innych składowych obiektu.

W przypadku metod wirtualnych wywołanie jest natomiast dwuetapowe i polega na wykorzystaniu pewnych dodatkowych informacji. Są nimi: tablica funkcji wirtualnych (zwana vtable) oraz wskaźnik na nią (czyli vptr). Tablica występuje tylko w jednej kopii na całą klasę i zawiera tyle elementów, ile funkcji wirtualnych klasa ta posiada. Jej elementami są po prostu adresy w pamięci tych właśnie funkcji: pierwsza funkcja wirtualna ma więc adres zapisany w elemencie vtable o indeksie 0, druga – o indeksie 1, itd.


Tablica metod wirtualnych i wskaźnik na nią (Źródło)

Vptr jest natomiast daną trzymaną wraz z każdym obiektem, podobnie jak zwykłe pola tego obiektu (często vptr jest umieszczany w pamięci tuż po nich). Jest on niczym innym, jak wskaźnikiem na vtable i służy obiektowi, gdy ten chce wywołać którąś ze swoich metod wirtualnych. Dokładniej wygląda to mniej więcej tak:

  1. // (wywołanie pObject->Method(), jeśli Method() jest wirtualna)
  2. pMethod = pObject->_vptr[0]; // załóżmy, że metoda ta ma indeks 0
  3. (*pMethod)(pObject);

Widać więc, że polega to po prostu na pobraniu odpowiedniego adresu metody z vtable, kierując się indeksem ustalanym w czasie kompilacji, zwykle na podstawie kolejności deklaracji metod wirtualnych w bloku class.

Dlaczego jednak raz może być wywołana wersja metody z klasy bazowej, a raz z pochodnej?… Tego nietrudno się już chyba domyślić: wszystko zależy od tego, na jaką tablicę pokazuje vptr. Jego ustawienie następuje automatycznie w trakcie tworzenia obiektu; wtedy wiadomo oczywiście, jakiego on jest typu. Potem jednak może on być dostępny zarówno przez wskaźnik do swojej klasy, jak i do klasy bazowej. W obu przypadkach metody wirtualne będą działały poprawnie, gdyż w ich wywołaniu będzie pośredniczył vptr.

Tak to wszystko wygląda, w wielkim skrócie rzecz jasna :) Całość nie jest może trywialna ze względu na pewne kombinacje ze wskaźnikami do funkcji, jednak nie zaszkodzi mieć przynajmniej ogólne pojęcie o tym, jak to właściwie działa. Istnieje szansa, że dzięki temu będziemy potrafili korzystać nieco lepiej z metod wirtualnych, a przy okazji trochę więcej rozumieć z tajemniczych dyskusji “ekspertów C++” przerzucających się takimi tajemniczymi terminami jak vtable i vptr ;-]

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

Przerwa na komunikaty Windows

2008-07-23 17:05

W Windows obsługa zdarzeń opiera się na koncepcji pętli komunikatów (message loop). Taka pętla powinna nieustannie kręcić się w każdym wątku, w którym tworzone są okna, i na bieżąco pobierać pojawiające się komunikaty o zdarzeniach. Dzięki temu użytkownik może przeprowadzać interakcje z interfejsem programu. Dlatego też wszelkie dłuższe działania (np. złożone obliczenia lub odczyt/zapis, zwłaszcza przez sieć) mogą sprawić, że aplikacja wyda się zawieszona. Pętla komunikatów nie będzie bowiem pobierać i przetwarzać gromadzących się w kolejce zdarzeń.
Typowym rozwiązaniem jest umieszczenie takich czasochłonnych operacji w osobnym wątku. Czasem jest to oczywiście niezbędne, lecz wielowątkowość, jak wiadomo, oprócz korzyści wprowadza dodatkowe problemy, które należy uwzględnić – jak choćby synchronizacja dostępu do danych.

Jeśli jednak nasza długa operacja daje się podzielić na kilka mniejszych, to możemy wówczas zastosować inne rozwiązanie. Można mianowicie co jakiś (krótki) czas pobierać oczekujące w kolejce komunikaty i zajmować się nimi, dzięki czemu interfejs będzie miał szansę zareagować na akcje użytkownika. Zazwyczaj oznacza to odpowiednio częste wywoływanie funkcji w rodzaju System.Windows.Forms.Application.DoEvents z .NET lub TApplication.ProcessMessages z VCL (Delphi). Jeśli na przykład dokonujemy jakichś obliczeń w pętli, wspomniane funkcje można wywoływać w każdym jej cyklu.
Jest nieco gorzej, jeśli piszemy z użyciem samego Windows API. Podobną funkcję musimy wtedy napisać sami. Nie jest to na szczęście trudne – należy po prostu “na chwilę” uruchomić naszą pętlę komunikatów, aby przetworzyła oczekujące zdarzenia:

  1. bool DoEvents()
  2. {
  3.     MSG msg;
  4.     while (PeekMessage(&msg, 0, 0, 0, PM_REMOVE))
  5.     {
  6.         if (msg.message == WM_QUIT) return false;
  7.         TranslateMessage (&msg);
  8.         DispatchMessage (&msg);
  9.     }
  10.     return true;
  11. }

Korzystamy z PeekMessage, która sprawdza, czy w kolejce nie ma już więcej zdarzeń i informuje nas o tym. Trzeba tylko pamiętać, że wśród tych zdarzeń może pojawić się WM_QUIT, który to komunikat oznacza konieczność zamknięcia aplikacji. Sposobem na jego obsłużenie może być chociażby wartość zwrócona z naszej funkcji; wywołujący będzie wówczas wiedział, że powinien zakończyć to, co aktualnie robi, gdyż program ma zostać zamknięty.
Oczywiście postać powyższej pętli powinna tak naprawdę być identyczna jak tej głównej, w funkcji WinMain – a więc niekoniecznie taka, jak powyżej. To się aczkolwiek zdarza wtedy, gdy korzystamy z okien dialogowych, akceleratorów menu i tym podobnych rzeczy. Samo to wymaga jednak na tyle dużej biegłości w Windows API, że odpowiednie przerobienie zaprezentowanej funkcji to przy tym naprawdę mały pikuś ;-)

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


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