Właśnie się dowiedziałem, że dzisiaj właśnie przypada Dzień Programisty. A to dlatego, że dziś jest dokładnie 256-ty dzień roku, a znaczenia tej liczby chyba nie trzeba wyjaśniać. Wprawdzie taka okazja mogłaby pasować chyba do każdej informatycznej profesji, ale i tak uważam ją za o niebo lepszą chociażby od rocznicy urodzin naszego tzw. patrona św. Izydora czy innych tego typu pomysłów.
Mamy zatem święto koderów i należałoby je uczcić w jakiś nietypowy sposób. W pierwszej chwili pomyślałem, że tego odmiany mógłbym dla odmiany… nie napisać ani jednej linijki kodu. Niestety, ta sposobność przepadła kilka godzin wcześniej :) A może wobec tego wcielić w życie sentencję głoszącą, iż święto trzeba uczcić pracą? :)
Niezależnie od ich własnych pomysłów na obchodzenie tego dnia, życzę wszystkim programistom oraz tym, którzy dopiero do tego miana aspirują, przede wszystkim powodzenia w realizacji wszelkich koderskich zamierzeń. Wiadomo, że do wszystko dochodzi się głównie ciężką pracą i odpowiednią motywacją, ale czasami przydaje się ta odrobina szczęścia, na którą nie mamy wpływu. Niech dzisiaj (i nie tylko dzisiaj) to szczęście sprzyja wszystkim programistom, aby ta ilustracja po prawej jak najmniej do nas pasowała ;]
Wyjątki są sposobem na zasygnalizowanie nietypowych i niespodziewanych błędów, które poważnie zaburzają działanie programu. I właśnie to, że potencjalnie wyjątek może wystąpić w bardzo wielu miejscach w kodzie, rodzi pewne kłopoty. Problemami są chociażby zasoby: coś, co się pozyskuje, wykorzystuje, a następnie zwalnia, gdyż w przeciwnym razie doszłoby do wycieku. Typowym zasobem jest chociażby dynamicznie alokowana pamięć – jeżeli jej nie zwolnimy, nastąpi klasyczny wyciek, jako że C++ nie posiada garbage collectora, który mógłby się tym zająć za nas.
W C++ zalecanym rozwiązaniem tego problemu jest technika znana jako RAII (Resource Acquision Is Initialization – pozyskanie zasobu jest inicjalizacją). Korzysta ona z faktu, że w naszym ulubionym języku programowania możemy tworzyć obiekty lokalne z konstruktorami i destruktorami. Te drugie wywołają się zawsze przy opuszczaniu danego bloku kodu – niezależnie od tego, czy stało się z powodu wyjątku czy tez normalnego przebiegu programu. Pomysł polega więc na tym, by tworzyć obiekt specjalnie przygotowanej klasy w momencie pozyskania zasobu, zaś destruktor tego obiektu zajmie się już jego zwolnieniem, niezależnie od powodu.
Dopóki korzystamy z pamięci albo z plików, wszystko jest w porządku; odpowiednie klasy (jak auto_ptr
) posiada bowiem Biblioteka Standardowa. Gorzej jeśli chcemy skorzystać z innego rodzaju zasobów. Jeśli odpowiednia klasa realizująca technikę RAII nie istnieje, nie pozostaje nam nic innego, jak samemu ją sobie zapewnić (czytaj: napisać). I tak dla każdego rodzaju niestandardowych zasobów, które używamy. Po niedługim czasie można by z tych klas ułożyć własną “bibliotekę standardową” ;)
Alternatywą dla RAII jest dodanie trzeciego bloku (po try
i catch
) do konstrukcji łapiącej wyjątki. Jest on zwykle nazywany finally
. Instrukcje zawarte w tym bloku są wykonywane zawsze po tych z bloku try
– niezależnie od tego czy wyjątek wystąpił czy nie. Jest to więc bardzo dobre miejsce na wszelki kod zwalniający pozyskane wcześniej zasoby, np.:
Co ciekawe, posiadają go języki, które jeden z najważniejszych zasobów – pamięć – mają zarządzaną przez odśmiecacz, który praktycznie wyklucza możliwość powstania wycieków. Rzecz jednak w tym, że niektóre zasoby, jak chociażby otwarte pliku, nie mogą sobie czekać na to, aż odśmiecacz przypomni sobie o nich, gdyż wtedy byłyby blokowane stanowczo zbyt długo.
Czy C++ też potrzebuje instrukcji finally
? Na pewno nie jest to bardzo paląca potrzeba, jako że technika RAII zapewnia komplet potrzebnej tutaj funkcjonalności. To drugie, alternatywne rozwiązanie ma jednak szereg zalet:
finally
można też pożytecznie wykorzystać nawet wówczas, gdy w grę nie wchodzi możliwość pojawienia się wyjątku. Jeżeli na przykład w jakiejś skomplikowanej funkcji mamy wiele miejsc, w których może nastąpić jej zakończenie, a przy każdej okazji może być potrzeba wykonania jeszcze jakichś czynności końcowych. Wtedy moglibyśmy zamknąć całą treść funkcji w try
, a owe czynności umieścić w sekcji finally
. Trzeba by się było jednak liczyć z tym, że blok try
nie jest darmowy i jego użyciu nakłada pewien narzut.I przede wszystkim: RAII i finally
nie wykluczają się nawzajem. Dlatego obecność tego drugiego mechanizmu w C++ na pewno by nam nie zaszkodziła :)
Kończąc powoli prace nad systemem GUI (a przynajmniej jakimiś sensownymi podstawami tego systemu), zakańczam jednocześnie prace nad “płaską” częścią silnika. Innymi słowy, już wkrótce nie będzie żadnej wymówki i trzeba będzie zabrać się za dodanie upragnionego, a zarazem niezwykle komplikującego życie trzeciego wymiaru :)
Pomyślałem jednak, że najpierw dobrze byłoby poskładać napisane już w cegiełki w sensowną całość i stworzyć coś w rodzaju frameworka. Chodzi tutaj o tę warstwę pośrednią między kodem silnika a użytkownikiem i systemem, czyli szkielet umożliwiający wygodne tworzenie rzeczywistych aplikacji.
W wielu bibliotekach różnie to rozwiązano. Z jednego strony DirectX czy OpenGL zostawiają to całkowicie w gestii programisty. Musi on samodzielnie przygotować chociażby to okienko, w którym będzie się odbywało rysowanie. Z kolei np. SDL bardzo głęboko ingeruje w kod programu, narzucając nawet określoną formę funkcji main
.
Najlepsze jest oczywiście takie rozwiązanie, które zapewnia zarówno dużą elastyczność, jak i nie zmusza do napisania kilkuset linijek w celu zobaczenia czegokolwiek. Wydaje mi się, że bliski ideałowi jest pomysł zamknięcia funkcjonalności frameworka w klasę w rodzaju Application
, zawierająca metody pozwalające na inicjalizację programu i późniejsze sterowanie nim. Metody tej klasy byłyby wywoływane w funkcji startowej programu, czyli main
lub WinMain
. Tak to wygląda na przykład w Windows Forms czy w VCL (Delphi):
[delphi]program SomeProject;
uses
Forms,
MainFrm in ‘MainFrm.pas’ {Form1};
{$R *.res}
begin
Application.Initialize;
Application.CreateForm(TMainForm, MainForm);
Application.Run;
end.[/delphi]
Według mnie najlepiej jest, gdy obiekt głównej klasy programu jest albo statycznie tworzonym singletonem, albo zostaje wykreowany przez programistę przy rozruchu aplikacji. Najważniejsze, aby nie zmuszać do dziedziczenia po klasie Application
– po to na przykład, by nadpisując metody wirtualne zapewnić możliwość reakcji na zdarzenia (jak wciśnięcia klawiszy czy ruch myszy). Dzięki delegatom, choćby tym z biblioteki FastDelegate, można to zrobić dokładnie tak, jak w “bardziej cywilizowanych” językach programowania.
Rysując grafikę dwuwymiarową dość często chcemy, aby była ona wpasowana tylko w ograniczony obszar ekranu. Jeśli większość obrazu ma być wyrenderowaną trójwymiarowa scena, grafika HUDa nie może zajmować zbyt dużo miejsca. Wielkość rysowanych sprite’ów można kontrolować na wiele sposobów, począwszy od zabawy z macierzami (projekcji i/lub świata), a skończywszy na programowym skalowaniu wielokątów.
Podobnie jest z innym aspektem tego ‘ograniczania’ wyświetlanej grafiki, czyli przycinaniem. Tutaj jednak dużo zależy od tego, jakie kształty chcemy przycinać oraz czym jest region przycinania.
Najprostszy przypadek dotyczy ograniczenia prostokąta przez inny prostokąt. Przycinanie, czyli wyznaczenie ich części wspólnej, to w tym przypadku tylko kilka porównań współrzędnych tych prostokątów:
Ponadto trzeba jeszcze tylko sprawdzić, czy powstały prostokąt jest poprawny – czyli czy spełnione są warunki: left
mniejsze od right
i top
mniejsze niż bottom
. W przeciwnym wypadku wyjściowe prostokąty nie mają części wspólnej.
Kiedy prostokątem chcemy uciąć coś o innym kształcie, wówczas sprawa znacznie się komplikuje z matematycznego punktu widzenia. Można jednak skorzystać z przydatnego narzędzia wbudowanego w Direct3D i znanego jako scissor test. Jest to specjalna, późna faza potoku renderowania, która odrzuca wszystkie piksele poza tymi mieszczącymi w podanym regionie (w przypadku DirectX 9 chodzi o prostokąt). Skorzystanie z tego mechanizmu jest bardzo proste i ogranicza się do:
D3DRS_SCISSORTESTENABLE
na TRUE
SetScissorRect
urządzenia (współrzędne prostokąta liczone są w pikselach według viewportu)A co zrobić w najbardziej skomplikowanym przypadku: gdy zarówno przycinane figury, jak i region przycinania są niestandardowymi kształtami? Cóż, DirectX to nie GDI i nie można oczekiwać, że będzie miał wszystko :) Nie jest to aczkolwiek sytuacja bez wyjścia, gdyż można ją rozwiązać przy pomocy stencil buffera (‘bufora szablonu’ lub ‘bufora matrycy’, że wymienię co ciekawsze tłumaczenia, jakie dotąd spotkałem). Wymaga to jednak nieco zabawy ze stanami urządzenia, a przede wszystkim renderowania regionu przycinania w postaci prymitywów od nowa w każdej klatce.
W teorii dziedziczenie to niezbyt skomplikowana relacja między klasami, ale w praktyce potrzeba czasem niestandardowych rozwiązań. Niekiedy bowiem możliwość rozszerzenia hierarchii klas w nieskończoność nie jest wcale pożądana. Zazwyczaj chodzi tu o względy projektowe, rzadziej o efektywność.
Pytanie brzmi: czy można jakoś zabezpieczyć konkretną klasę przed dziedziczeniem (czyli zabronić w tworzenia opartych o nią klas pochodnych)?
W C# i Javie jest to możliwe za pomocą specjalnych słów kluczowych – odpowiednio sealed
(‘zapieczętowany’) i final
. Wystarczy opatrzyć nimi definicję klasy, by kompilator odmówił skompilowania kodu, który wykorzystywałby taką klasę jako bazową:
Modyfikatory sealed
/final
można też stosować do metod wirtualnych. W tym przypadku sprawiają one, że dana metoda wirtualna nie może być nadpisywana w klasach pochodnych. Chwilowo nie przychodzi mi do głowy żaden pomysł na pokazanie przydatności takiego triku, ale zapewne takowy przykład istnieje :)
W C++ nie mamy rzecz jasna wspomnianych słów kluczowych (ani żadnych, które działałyby podobnie), ale istnieje sposób na zabezpieczenie klasy przed dziedziczeniem. Jest on jednak brzydki. A nawet bardzo brzydki. Mimo to zaprezentuję go.
Jak to często bywa w takich sytuacjach, sztuczka polega na kreatywnym wykorzystaniu pewnego mechanizmu językowego unikalnego dla C++, który generalnie służy do czegoś zupełnie innego. Tutaj chodzi o dziedziczenie wirtualne – a dokładniej o to, że wirtualna klasa bazowa musi być bezpośrednio inicjalizowana przez każdą klasę pochodną; nawet tą w setnym pokoleniu niżej. Do tego dodać należy jeszcze prywatne konstruktory i deklarację przyjaźni, a następnie zamieszać i doprawić do smaku, by ostatecznie upichcić coś w tym stylu:
Klasa pomocnicza CFoo_Lock
ma prywatny konstruktor domyślny, więc generalnie nie może być on wywoływany. W szczególności nie może być on wywołany przez konstruktor CBar
– a jest to konieczne, gdyż CFoo_Lock
jest wirtualną klasą bazową i musi być zainicjalizowana bezpośrednio przez dowolną klasę pochodną (nawet niebezpośrednio pochodną). Dlatego też próba dziedziczenia CFoo
skończy się błędem mówiącym o braku dostępu do prywatnego konstruktora CFoo_Lock
.
Dlaczego zatem CFoo
działa? To już zasługa deklaracji przyjaźni umieszczonej w klasie-blokadzie. Przyjaźń międzyklasowa nie jest jednak dziedziczna, więc nasza blokada odmawia dostępu do prywatnych składowych (czyli konstruktorów) klasom pochodnym CFoo
. Dzięki temu (i niuansom dziedziczenia wirtualnego) cały ten trik działa.
To rozwiązanie można naturalnie uczynić bardziej automatycznym – wystarczy zaprząc do pracy szablony i preprocesor, by uzyskać prawie to samo co sealed
/final
w C#/Javie. Trudno będzie jednak uznać to kombinowanie za eleganckie.
Wygląda więc na to, że najlepszym sposobem na zabezpieczenie klasy przed dziedziczeniem w C++, to po prostu… pokazać na nią palcem (tj. komentarzem) i powiedzieć “Nie dziedzicz mnie!”. No i mieć nadzieję, że ktoś nas posłucha.
Czasami przydaje się posiadanie więcej niż jednego operacyjnego na jednym komputerze. Jeżeli oba używają tego samego systemu plików, to zwykle nie ma problemu z wymianą danych.
A co zrobić, jeżeli tak nie jest – czyli na przykład chcemy dostać się z poziomu Linuxa do plików używanych w Windows lub odwrotnie? W jedną stronę jest łatwo: większość dystrybucji Linuxa obsługuje przynajmniej odczytywanie z partycji NTFS, a niektóre automatycznie montują znalezione woluminy, które korzystają z tego systemu plików.
Jeżeli zaś chodzi o operację odwrotną, to naturalne Windows nie patrzy tak przychylnie na inny system operacyjny. Wbudowana w okienka obsługa różnych systemów plików nie obejmuje ext2 i ext3, czyli tych używanych w ogromnej większości Linuksów. Tutaj mogą pomóc tylko zewnętrzne narzędzia.
Jednym z nim jest Ext2 IFS. Ma on tę zaletę, że działa jak sterownik, a nie np. wtyczka do Eksploratora, Total Commandera czy innego menedżera plików. Dzięki temu podmontowane partycje linuksowe są widoczne w całym systemie.
Można z nich korzystać właściwie tak samo jak z dysków NTFS, czyli odczytywać i zapisywać pliki, katalogi, zmieniać etykiety woluminów, itp. Nie trzeba też kopiować plików tam i z powrotem, aby je modyfikować.
Sam nie używam Linuksa zbyt często, ale taki sterownik jest bardzo wygodny. Chociaż prawdopodobnie dostęp do partycji ext3 z Windows sprawia, że Linuksa uruchamiam nawet jeszcze rzadziej niż wcześniej ;)
Zwykłe przyciski (buttons) można tylko wciskać jak guziki na monitorze lub klawisze na klawiaturze. Wydawałoby się, że nie mają one wiele wspólnego z bardziej zaawansowanymi elementami interfejsu: polami wyboru (checkboxes) czy przyciskami opcji (radio buttons).
Te pierwsze to proste przełączniki: mogą być zaznaczone lub nie, swój stan zmieniają po kliknięciu i zazwyczaj odwzorowane są na jakieś funkcje programu, które mogą być włączane i wyłączane. Te drugie oferują z kolei wybór z większej liczby wariantów, z których tylko jeden może być aktywny w danej chwili. Z punktu widzenia użytkownika największa różnica polega jednak na tym, że pola wyboru mają obok etykiet tekstowych kwadraciki, zaś przyciski opcji – kółka :)
Podobieństwo tych dwóch kontrolek do zwykłych przycisków polega na tym, że w identyczny sposób reagują na zdarzenia od myszy – czyli przede wszystkim kliknięcia. Podobieństwo okazało się na tyle duże, że sensowne okazało się połączenie wszystkich tych trzech rodzajów kontrolek wspólną klasą bazową. Nie lubię nadużywania dziedziczenia, ale akurat tutaj rozciągnięcie hierarchii klas wydaje się rozsądne.
Z tych trzech typów kontrolek prawdopodobnie najbardziej interesujący jest przycisk opcji, czyli ten z kółeczkiem :) Trzeba bowiem zastanowić się, w jaki sposób zrealizować grupowanie tych obiektów w zestawy, w ramach których tylko jeden radio button może być zaznaczony. Spotkałem tutaj dwa rozwiązania:
RadioGroup
. Kontener ten może zawierać tylko przyciski opcji, które wówczas nawet nie muszą być samodzielnymi kontrolkami (ale cały czas powinny oczywiście sprawiać takie wrażenie). Taka ‘grupa radiowa’ jako pojemnik może, ale nie musi być widoczna na ekranie. Przy takim rozwiązaniu spełniony jest zawsze warunek, że spośród wszystkich przycisków opcji będących bezpośrednio zawartych w jakimkolwiek kontenerze (RadioGroup
lub innym) zaznaczony jest tylko jeden.Ostatecznie zdecydowałem się na wariant drugi. Z punktu widzenia implementacji najważniejszego elementu – czyli dbania o to, by tylko jedna opcja była zaznaczona – oba rozwiązania są bardzo podobne. Tak czy owak trzeba zawsze najpierw odznaczyć wszystkie przyciski, a potem zaznaczyć ten kliknięty. Jest to przy tym jedna z tych sytuacji, w których bardzo ułatwia życie przeklinany operator rzutowania dynamic_cast
:]