Archive for Programming

Przesłanianie kontra nadpisywanie

2008-02-14 19:08

Jednym ze swego rodzaju kruczków programowania obiektowego jest różnica między przesłanianiem (hide) a nadpisywaniem (override) metod w klasach pochodnych. Niby nic w tym trudnego, dopóki nie uświadomimy sobie, że metody wirtualne – w przeciwieństwie do zwykłych – mogą podlegać obu tym procesom i że w ich przypadku różnią się one w dość istotny sposób.

Jeśli mianowicie mamy jakąś metodę wirtualną w klasie bazowej, to w klasie pochodnej teoretycznie możemy ją zarówno nadpisać, jak i przesłonić. Gdy zdecydujemy się na to pierwsze wyjście, wówczas nasz obiekt staje się polimorficzny. Posługując się wskaźnikiem lub referencją do obiektu klasy bazowej tak naprawdę możemy operować na obiekcie klasy pochodnej, a wywołując metodę wirtualną nie wiemy, która jej wersja będzie użyta. Wiemy jedynie, że będzie to ta “właściwa” (z klasy, do której należy nasz obiekt). No cóż, to podstawa OOP-u, jakkolwiekby na to nie patrzeć :)

  1. // klasa bazowa
  2. class CBase { public: virtual void Foo() { /* ... */ } };
  3.  
  4. // klasa pochodna
  5. class CDerived : public CFoo
  6. {
  7.    public:
  8.       // nadpisana metoda
  9.       void Foo()   { /* ... */ }
  10. };

Nie zawsze jednak musi tak być. Jeżeli metodę wirtualną się przesłoni, wtedy nic takiego nie zadziała i zostanie wywołana wersja z klasy bazowej. Typ obiektu nie zostanie bowiem rozpoznany w trakcie działania programu, a całe wywołanie będzie zapisane “na sztywno” w wynikowym kodzie. W gruncie rzeczy będzie tak samo, jakbyśmy przesłoni dowolną inną (niewirtualną) metodę.
O ile oczywiście możemy, bowiem w C++ metody wirtualnej przesłonić się po prostu nie da. Nadpisywanie jest po prostu wykonywane automatycznie i nie można z niego zrezygnować. W innych językach (jak w Delphi i C#) wymaga ono słowa kluczowego override. Tam też metody wirtualne można przesłaniać, stosując modyfikatory – odpowiednio reintroduce i new.

Żeby było zabawniej, przesłanianie i nadpisywanie może występować dla tej samej metody w tej samej hierarchii dziedziczenia. Metoda wirtualna zdefiniowana w klasie A może być na przykład nadpisywana w kolejnych klasach pochodnych A1, A2, A3, itd. I nagle w klasie B zostanie ona przesłonięta. Ba, ta przesłonięta wersja sama może być wirtualna i nadpisywana w dalszych klasach pochodnych: B1, B2, B3, …
Nie spotyka się tego często, jako że podobne wielopiętrowe drzewa dziedziczenia są rzadkie (i takie być powinny). Warto jednak o tym wiedzieć, bo nigdy nie wiadomo, kiedy podobny kruczek przybierze w naszym kodzie postać wielkiego kruka i zacznie nas dziobać zagadkowymi błędami ;]

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

Nie całkiem brakujące ogniwo DirectX

2008-02-12 20:49

Wszyscy wiedzą, że przesiadka na Vistę ma jedną niezaprzeczalną zaletę. Jeśli mianowicie dysponujemy odpowiednią kartą graficzną, to możemy cieszyć się całym bogactwem Shader Model 4 oraz dziesiątej wersji DirectX. Nadal jednak niewiele gier posiada jakieś efekty (oczywiście w(y)łączalne) uzyskiwane przy pomocy nowej wersji biblioteki. Najwyraźniej Microsoft nieumyślnie wpadł tutaj w rodzaj błędnego koła, które zapobiega powszechnemu wykorzystaniu DirectX 10.
Nie pomaga tu też za bardzo specjalna wersja “dziewiątki”, czyli tak zwany Direct3D 9Ex. Cóż to za zwierz?…

Jest to mianowicie pewne rozszerzenie znanej i lubianej wersji biblioteki, które wprowadza nowe możliwości, częściowo zbieżne z tym, czego można doświadczyć w D3D10. Wśród nich mamy chociażby:

  • Praktyczny brak zjawiska utraty urządzenia. W zasadzie począwszy od DirectDraw, gdzie mieliśmy tzw. utratę powierzchni, była to zawsze spora niedogodność. Jak wiemy, utrata urządzenia DirectX dotąd następowała wtedy, gdy użytkownik przełączał się z aplikacji pełnoekranowej, zmieniając tryb graficzny (rozdzielczość, głębię kolorów, itd.). Powodowała ona stratę wszystkich zasobów przechowywanych w pamięci karty graficznej, czyli głównie tekstur, i konieczność ich ponownego ładowania – co było i jest dość kłopotliwe. W D3D9Ex utrata urządzenia następuje tylko w dwóch ekstremalnych sytuacjach sprzętowo-sterownikowych i właściwie nie trzeba się nią przejmować.
  • Współdzielenie zasobów. Funkcje DirectX 9 miały od dawna tajemniczy parametr pSharedHandle. Był on opisany jako zarezerwowany i należało w jego miejsce przekazywać NULL. Rezerwacja ta jednak nie przepadła i w 9Ex jest on wykorzystywany do współdzielenia zasobów między urządzeniami – nawet takimi, które są w oddzielnych procesach. Mogę sobie wyobrazić zastosowanie tego mechanizmu chociażby do przełączania trybów graficznych bez konieczności ponownego tworzenia wszystkich tekstur, buforów, itp.
  • Przydatne funkcje w nowym interfejsie IDirect3DDevice9Ex. Jest wśród nich na przykład WaitForVBlank, służąca do ręcznej synchronizacji pionowej.

Wszystkie te możliwości prezentują się całkiem nieźle. Żeby jednak z nich korzystać, muszą być spełnione dwa warunki. Po pierwsze, sprzęt musi wspierać tak zwany WDDM (Windows Device Driver Model), co w przypadku popularnych kart graficznych dotyczy z grubsza tych, które udostępniają Shader Model 3.
A drugi warunek?… Niejako wynika on z pierwszego, bowiem WDDM jest częścią systemu Windows Vista. Aplikacje wykorzystujące Direct3D 9Ex będą więc działały wyłącznie na tym systemie. To pewnie trochę zniechęcające, prawda? Ale cóż, Microsoft próbuje jak może ;-)

Pętla ‘for each’

2008-02-09 20:17

W bardzo wielu językach dostępna jest dodatkowa postać pętli for, znana powszechnie jako foreach. Jak jej nazwa wskazuje, służy ona do przeglądania elementów jakiejś kolekcji. Różnica pomiędzy tą konstrukcją a dowolną inną pętlą jest taka, iż przeglądanie to jest zrealizowane w sposób najprostszy z możliwych:

  1. List<string> strings = new List<string>();
  2. // ...
  3. foreach (string s in strings)
  4.    Console.WriteLine (s);

W szczególności nie potrzeba się posługiwać żadnymi indeksami ani też jawnie stosowanymi iteratorami. Dostęp do poszczególnych elementów zbioru odbywa się za pośrednictwem symbolicznej zmiennej, która przyjmuje wartości równe tymże elementom. Proste i skuteczne.
Zresztą, niektórym twórcom języków tak się ta koncepcja spodobała, że… całkowicie zrezygnowali z tradycyjnej postaci pętli for (w wersji od-do, jak w Pascalu, lub start-warunek-inkrement, jak w językach C-podobnych).Tak jest chociażby w Pythonie; można tam jednak uzyskać identyczny efekt pętli “liczbowej” w taki oto zabawny sposób:

  1. # drukuje 0, ..., 9
  2. for i in range(0, 10):
  3.    print i

Ogółem jednak pętla foreach wydaje się bardzo zgrabnym wynalazkiem. Świadczy o tym chociażby istnienie takich makr jak BOOST_FOREACH, które to ładnie adaptuje cały pomysł do kontenerów STL w C++, tablic i nie tylko.
Jest jednak pewne ‘ale’. foreach sprawdza się świetnie, jeśli tylko kolekcję przeglądamy, bez dokonywania w niej zmian. Jeśli musimy przy okazji coś do niej dodać lub usunąć, lub nawet tylko zmienić wartość jakiegoś elementu, to nagle pętla taka przestaje nam wystarczać. Z doświadczenia wiem, że po tym, jak radośnie i bez wysiłku wpisuję pętlę foreach, w 80% muszę w którymś momencie i tak zmienić ją na zwykły for :-)

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

Konstruktory bywają złe

2008-02-04 11:42

Konstruktor to taka dziwna funkcja składowa, która jest wywoływana w trakcie tworzenia obiektu. Jest więc w zasadzie odpowiedzialna za jego przygotowania do użytku, czyli odpowiednią inicjalizację. Tak przynajmniej jest w teorii.
W rzeczywistości jednak konstruktor powinien robić jak najmniej. Jest tak z kilku powodów:

  1. Konstruktor działa na nie w pełni utworzonym obiekcie, co sprawia, że nie zachowuje się on do końca tak, jak inne metody. Najlepszym przykładem jest to, że w konstruktorze nie działają polimorficzne wywołania funkcji wirtualnych dla obiektu, który jest tworzony.
  2. Jeżeli w konstruktorze coś pójdzie źle, i zostanie wyrzucony wyjątek, to destruktor nie zostanie wywołany, aby nasz niekompletny obiekt posprzątać. Jest tak właśnie z tego powodu, że ów obiekt będzie wtedy “niezupełnie skonstruowany”, zaś destruktory w założeniu mają zwalniać tylko w pełni zainicjowane obiekty.
  3. Konstruktory są wywołane w wielu różnych sytuacjach i czasami trudno jest śledzić, kiedy. Do takich sytuacji należą wszelkie instrukcje wprowadzające zmienne należące do danej klasy, konstruktory klas pochodnych, a także konstruktory klas, które zawierają instancje naszej klasy w charakterze pól. Łącznie może być ich całkiem dużo i lepiej upewnić się, że przy okazji są wykonywane tylko te czynności, które są niezbędnie konieczne.

Pewne duże obiekty mogą oczywiście wymagać skomplikowanej inicjalizacji. Wówczas, zamiast pakować całą tę logikę do konstruktora, lepiej jest prawdopodobnie oddzielić inicjalizację od właściwego tworzenia obiektu. W tym celu można go zaopatrzyć w metodę w rodzaju Create czy Load, która dokonuje właściwego przygotowania obiektu do działania.
Może wydawać się to lekkim zawracaniem głowy, ale dla nietrywialnych obiektów ma sens. Pozbywamy się wad wymienionych powyżej, a jednocześnie zyskujemy dodatkowe możliwości (choćby dlatego, że wspomniana metoda może być wirtualna).

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

‘No i co tu jest źle?’ w wersji książkowej

2008-02-02 17:57

Kiedy ma się dość pokaźną biblioteczkę książek, czasami natrafia się na pozycję, która nie wiadomo skąd się w niej wzięła. Pytanie takie staje się tym bardziej uzasadnione, gdy rzeczoną książkę otworzymy, przekartkujemy i pobieżnie przeglądniemy, by po krótkim czasie uznać, że najchętniej widzielibyśmy ją w… punkcie skupu makulatury :P
Okładka książki “Jak NIE programować w C++”Coś takiego przytrafiło mi się zupełnie niedawno. Książką o której mam taką “pochlebną” opinię, jest dziełko opatrzone tytułem Jak NIE programować w C++. Jeszcze ciekawszy jest chyba podtytuł, który mówi, że wewnątrz znajdziemy dokładnie 111 programów z błędami oraz trzy działające. Statystyka jest imponująca, ale o co tak naprawdę tutaj chodzi?… Autor przedstawia nam mianowicie coś w rodzaju zbioru koderskich zagadek do samodzielnego rozwiązania, które polegają oczywiście na znalezieniu błędu w przedstawionym kawałku kodu.

Jak podejrzewam, celem tej książki w zamyśle autora było ustrzeżenie programistów C++ przez różnego rodzaju typowymi błędami, jakie mogą się zakraść do pisanego przez nich kodu. Cel to chwalebny, chociaż dość utopijny – w końcu nie wystarczy wiedzieć, na czym błąd polega, aby w magiczny sposób już nigdy więcej go nie popełnić. Trochę więcej wątpliwości mam natomiast co do obranej metody. Nie wiem, w jaki sposób obejrzenie ponad stu błędnych kodów ma sprawić, że będziemy częściej pisali poprawny kod. Spodziewałbym się raczej podświadomego nauczenia się prezentowanych tam złych przykładów i ich spontanicznego stosowania w rzeczywistych programach, co raczej nie ułatwiłoby nikomu pracy :)
Byłoby oczywiście świetnie, gdyby przykłady te były jedynymi niepoprawnymi kawałkami kodów, z którymi przychodzi nam się mierzyć. Ale przecież jest to odległe od prawdy o całe lata świetlne. Jako koderzy sami nieuchronnie produkujemy niepoprawny kod, który co rusz musimy korygować. Prawdopodobnie więc nie potrzebujemy dodatkowych łamigłówek tego rodzaju, bo wystarczą nam te, które w nieumyślny sposób tworzymy sami dla siebie. I niestety nie możemy w ich przypadku – w przeciwieństwie do kodów z książki – zajrzeć do części końcowej po wskazówki i odpowiedzi.
Jednak nawet wobec takich mankamentów, prezentowane w książce przykłady mogłyby mieć pewną wartość poznawczą. Rzecz w tym, że naprawdę interesujące zagadki można bez trudu policzyć na palcach obu rąk. Pozostałe są albo pomyłkami aż do bólu klasycznymi (na czele z pomyleniem operatora przypisania i równości w warunku logicznym), albo trywialnymi literówkami, albo świadectwami na – delikatnie mówiąc – niekompletną znajomość języka. (Moim faworytem jest deklaracja int n = 1,000,000;, w założeniu inicjująca zmienną wartością równą milion).

Aż chce się zapytać, czy są w tej książce jakieś cechy pozytywne. Odpowiadam prędko, że jak najbardziej, tyle że mają się one nijak do jej zasadniczej treści. Do każdej zagadki autor dołączył bowiem krótką, zabawną historyjkę “z życia wziętą” lub inny śmieszny tekst – wszystko oczywiście z dziedziny IT. Paradoksalnie więc ta “książka o C++” lepiej sprawdza się jako książka z dowcipami.
Jest też druga dobra strona. Przypomniałem sobie mianowicie, skąd u mnie wzięła się ta dziwna pozycja. Otóż zakupiłem ją podczas jakichś tragów wydawniczych, które akurat odbywały się na uczelni, zapłaciwszy za nią około dziesięć złotych. Nietrudno przeboleć taką niewielką sumę, nawet mimo tego, że nikomu nie poleciłbym wydania na tę książkę nawet jednej złotówki :)

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

Własny operator rzutowania

2008-01-30 22:27

W C++ nadal można używać “tradycyjnego” rzutowania w postaci (typ)wyrażenie, bo cały czas zachowywana jest przynajmniej częściowa kompatybilność wstecz ze starym C. Jak wiadomo jednak, zaleca się raczej stosowanie nowych (tzn. wprowadzonych w C++; faktycznie są już one raczej stare) operatorów rzutowania, czyli static_cast, dynamic_cast, reinterpet_cast i const_cast. Jest tak między innymi dlatego, iż pozwalają one rozróżnić rzutowania bezpieczne – które można sprawdzić w czasie kompilacji i które zawsze “mają sens” – od pozostałych.
Składnia takiego rzutowania to oczywiście something_cast<typ>(wyrażenie). Jeśli przyjrzymy się jej bliżej, to zauważymy interesującą rzecz. Otóż jest to bardzo podobne do pewnej formy wywołania szablonu funkcji. Ten fakt plus pewne zasady dotyczące dedukcji argumentów takich szablonów sprawiają, że zasadniczo możemy w C++ tworzyć… własne operatory rzutowania.

Prawdopodobnie najbardziej znanym jest implicit_cast, czyli operator rzutowania niejawnego. Możemy go zdefiniować następująco:

  1. template <typename Dest, typename Src>
  2. inline Dest implicit_cast(const Src& arg)
  3. {
  4.    return arg;
  5. }

i wywoływać w bardzo porządny sposób:

  1. string str = implicit_cast<string>("Hello world");

Ze strony swojego dzialania operator ten nie jest specjalnie ciekawy, bo to przykład językowego purytanizmu czystej wody. implicit_cast wykonuje bowiem te konwersje, które kompilator i tak by przeprowadził niejawnie; widać to zresztą po kodzie odpowiadającej mu funkcji.
No właśnie – warto zauważyć, że jest w gruncie rzeczy zwykła funkcja szablonowa. Działa ona jak operator rzutowania, gdyż drugi z argumentów tego szablonu – typ źródłowy – może zostać wydedukowany z argumentu funkcji (czyli wyrażenia, które rzutujemy). Musimy jedynie podać pierwszy z nich, czyli typ docelowy – ale tego można się było spodziewać. Trzeba zaznaczyć, że kolejność obu parametrów szablonu musi być właśnie taka, ponieważ ich przestawienie spowodowałoby, że automatyczna dedukcja nie mogła by zostać przeprowadzona. Zachodzi ona bowiem począwszy od końcowych parametrów.

Prawdopodobnie taki własny operator rzutowania to przede wszystkim efektowna sztuczka. Nic więc dziwnego, że kilka przykładów (np. lexical_cast) jest częścią biblioteki Boost, która niemal w całości jest zbiorem takich językowych gadżetów :) Sam aczkolwiek wymyśliłem dosyć dawno temu operator “rzutowania bezwzględnego”, który wygląda mniej więcej tak:

  1. template <typename Dest, typename Src>
  2. inline Dest absolute_cast(const Src& arg)
  3. {
  4.    return *(reinterpret_cast<Dest*>(const_cast<Src*>(&arg)));
  5. }

Służy on do dosłownego rzutowania dowolnego typu na dowolny inny w sensie zmiany interpretacji bajtów. Zdaje się on działać dla większości typów w C++ (głównym wyjątkiem są typy referencyjne, co nie jest specjalną niespodzianką) i bywa całkiem przydatny.

Naturalnie można wymyślić jeszcze sporo innych niestandardowych operatorów rzutowania, jak choćby konwersje między napisami w różnych kodowaniach (ANSI, Unicode) czy do/z jakichś własnych typów, opakowujących klasy zewnętrzne. W każdym razie widać znowu (i pewnie nie ostatni raz), że C++ pozwala nam na bardzo dużo, skoro nawet te nieco rozwlekłe *_casty można jakoś oswoić do własnych potrzeb :]

Tags: , ,
Author: Xion, posted under Programming » Comments Off on Własny operator rzutowania

Kolejność przekształceń macierzowych

2008-01-28 20:38

Kiedy uczyłem się biblioteki DirectX, miałem dość spore kłopoty z kwestią właściwej kolejności przekształceń opisanych przez macierze. Jak wiadomo, w grafice każdą transformację możemy opisać macierzą, a złożenie takich przekształceń możemy być reprezentowane przez odpowiedni iloczyn macierzy. Wówczas pomnożenie wektora (odpowiednio rozszerzonego o czwartą współrzędną) przez taką macierz skutkuje zastosowaniem do niego tych wszystkich przekształceń. Może być ich bardzo wiele, lecz wymagana jest tylko jedna macierz i jedno mnożenie przezeń wektora. Jest to więc efektywne, jeśli mamy dużą ilość geometrii do przetworzenia – czyli, co tu ukrywać, w zasadzie zawsze :)

Rzeczone macierze opisujące przekształcenia są kwadratowe; w przypadku grafiki 3D mają rozmiar 4×4. Dlatego też możliwe jest ich mnożenie w dowolnej kolejności. Wiemy jednak, że operacja mnożenia macierzy nie jest przemienna. Odpowiada to zresztą temu, iż przy przekształcaniu punktów w przestrzeni też liczy się kolejność: obrót, a potem przesunięcie to nie to samo, co przesunięcie, a potem obrót.
I tu się zaczyna problem, bowiem w bardzo wielu źródłach wprowadzone jest niezłe zamieszanie, jeśli chodzi o kolejność mnożenia macierzy opisujących geometryczne przekształcenia. Najczęściej pomieszane są konwencje tego, jaki porządek jest poprawny w danej bibliotece graficznej, a jaki “w matematyce”. Ostatecznie więc nie wiadomo, czy trzeba iloczyn macierzy zapisywać w kolejności, w jakiej chcemy aplikować przekształcenia, które reprezentują – czy może na odwrót. Dość prosto można oczywiście sprawdzić, jak to jest akurat w naszej bibliotece graficznej, lecz to nie mówi nic o istocie problemu…

Wektor kolumnowyWłaściwie to dopiero niedawno dowiedziałem się, gdzie jest tu pies pogrzebany. Otóż matematycy z pewnych przyczyn lubią traktować wektory jako kolumnowe, tj. jako macierze Nx1 (N wierszy, 1 kolumna). Przy takiej interpretacji tylko iloczyn w postaci:

macierz1 * wektor_kolumnowy

daje w wyniku wektor (także kolumnowy, rzecz jasna). W tym przypadku będzie on przekształcony przez macierz1. Jeżeli teraz zechcemy dodać drugie przekształcenie, to mnożenie przez odpowiednią macierz również musimy zapisać z przodu:

macierz2 * (macierz1 * wektor_kolumnowy)

Ale mnożenie jest oczywiście łączne, więc:

(macierz2 * macierz1) * wektor_kolumnowy = macierz * wektor_kolumnowy

a wynikowa macierz = macierz2 * macierz1 opisuje złożenie naszych przekształceń. Jak widać wyżej, najpierw jest stosowane to opisane przez macierz1, a dopiero potem to z macierzy2 – mimo że są one mnożone w porządku odwrotnym. Tak bowiem wygląda sprawa kolejności przekształceń dla wektorów kolumnowych.

Twórcy DirectX uznali prawdopodobnie, że jest to nieintuicyjne dla nie-matematyków i dokonali pewnego “triku”. Opiera się on na tym, że gdy w dwóch macierzach zamienimy ze sobą wiersze i kolumny – czyli dokonamy transpozycji – pomnożymy je przez siebie, a następnie transponujemy wynik, to rezultat będzie taki, jakbyśmy mnożyli wyjściowe macierze w odwrotnej kolejności. Wyjątkowo trzeba tutaj przyznać, że wzór mówi więcej niż jego opis, więc spójrzmy na ten wzór :)

(A * B)T = BT * AT

W DirectX dokonano więc transpozycji wszystkich macierzy opisujących przekształcenia. Przykładowo, funkcja D3DXMatrixTranslation zwraca macierz z wartościami przesunięć wpisanych w ostatnim wierszu, podczas gdy w wersji “matematycznej” powinny być one w ostatniej kolumnie. Podobnie jest ze wszystkimi innymi macierzami… ale także z wektorami!
Wektor wierszowyChociaż wektory z programistycznego punktu widzenia to cztery składowe i nic więcej, to w DirectX należy je traktować jako wektory wierszowe, czyli macierze 1xN. Dla nich zaś sensownym sposobem mnożenia przez macierz jest tylko następujący:

wektor_wierszowy * macierz1

Dodając kolejne przekształcenie, mamy:

(wektor_wierszowy * macierz1) * macierz2

i znów opierając się na łączności mnożenia otrzymujemy ostatecznie:

wektor_wierszowy * (macierz1 * macierz2) = wektor_wierszowy * macierz

Tutaj z kolei widać wyraźnie, że przekształcenia są stosowane w takiej samej kolejności, w jakiej odpowiadające im macierze występują w iloczynie.

Ponieważ, jak wspomniałem wyżej, cała sprawa jest kwestią czysto arbitralną (wystarczy transpozycja, aby odwrócić porządek), powinniśmy tym bardziej zwrócić na nią uwagę. A jeśli programujemy w DirectX, nie należy dopuścić do tego, by matematycy wmawiali nam ‘właściwą’ kolejność :P

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


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