Pakowanie w przestrzenieJeśli w C++ piszemy jakąś bibliotekę (albo nawet ogólniej: kod wielorazowego użytku), to zgodnie z dobrymi praktykami powinniśmy jej symbole zamknąć przynajmniej w jedną osobną przestrzeń nazw (namespace). Dzięki temu zapobiegniemy zminimalizujemy możliwość kolizji identyfikatorów w kodzie, który z naszego dzieła będzie korzystał - a także potencjalnie z innych bibliotek.
Nie wszystkie z nich jednak mogą być tak ładnie napisane - choćby dlatego, że któraś może być przeznaczona oryginalnie dla języka C. Najbardziej typowy przykład? Windows API. Dołączenie windows.h zasypuje globalną przestrzeń nazw istną lawiną symboli odpowiadających tysiącom funkcji czy typów zadeklarowanych w tym nagłówku. Nie jest to specjalnie dobre.
Jak temu zaradzić? Bardzo prostą, ale nierozwiązującą wszystkich problemów metodą jest stworzenie własnego nagłówka "opakowującego" ten biblioteczny w nową przestrzeń nazw:
namespace foo
{
#include <foobar.h>
}
#endif
Założenie jest takie, żeby wynikowego pliku nagłówkowego (foo.h) używać następnie w miejsce oryginalnego (foobar.h). Wtedy wszystkie symbole w nim zadeklarowane znajdą się wewnątrz nowej przestrzeni nazw, foo.
Wszystkie?... Nie! Pakując kod napisany w stylu C bezpośrednio do przestrzeni nazw nie osiągniemy bowiem wszystkich celów, którym namespace'y przyświecają. Owszem, da się co nieco poprawić: jeśli np. wspomniany windows.h zamknęlibyśmy w przestrzeni win, to poniższy kod będzie jak najbardziej działał:
podczas gdy wersja bez przedrostków win:: już niezupełnie. Jednak nie jest to całkowity - nomen omen - win, bo z kolei takie wywołanie:
skutkuje już niestety failem :) Nasza przestrzeń nie może bowiem zamknąć wszystkiego, gdyż nie podlegają jej dyrektywy preprocesora - a w szczególności #define. Pech polega na tym, że właśnie #define jest w C podstawowym sposobem definiowania stałych, więc użyta wyżej nazwa SW_MINIMIZE jest (w windows.h) określona po prostu jako:
Próba jej kwalifikowania powoduje zatem powstanie nieprawidłowego ciągu win::6 i słuszne narzekania kompilatora.
Nasz pojemnik (na nazwy) jest więc dziurawy i niestety nic z tym nie da się zrobić. Tak to już jest, gdy wciąż trzeba mieć do czynienia z API, które w tym przypadku liczy sobie - bagatelka - ponad 20 lat!
Narzędzia do DirectX
Zasadniczą i najważniejszą częścią DirectX SDK są pliki nagłówkowe oraz biblioteki (statyczne i dynamiczne), które pozwalają na pisanie programów korzystających z tego API. Do tego mamy jeszcze niezbędną dokumentację oraz przykładowe aplikacje (samples), pokazujące wykorzystanie poszczególnych jego elementów lub prezentujących implementacje różnych efektów graficznych.
Ale to nie wszystko, co można znaleźć w tym kilkusetmegabajtowym (i ciągle rosnącym) pakiecie. Niemal równie ważne są narzędzia pomocnicze, które można tam znaleźć. Podczas tworzenia aplikacji wykorzystujących zwłaszcza Direct3D umiejętność korzystania z tych programów jest niekiedy prawie tak samo ważna, jak znajomość samego API czy zagadnień z dziedziny grafiki.
Dlatego też postanowiłem pokrótce opisać niektóre z nich, żeby co mniej zaawansowani programiści DirectX mogli przynajmniej dowiedzieć się, że takowe istnieją :) Oto więc rzeczone aplikacje:
GetDeviceCaps urządzenia - w postaci przejrzystego interfejsu drzewiastego. Dobrze jest rzecz jasna wiedzieć, czego szukamy, ale w większości przypadków programistów grafiki 3D interesować będzie gałąź Direct3D9/10 Devices/<model karty graficznej>/D3D Device Types/HAL/Caps.HRESULT) na odpowiadające im stałe i komunikaty. To pierwsze potrafi też częściowo zrobić debuger Visual Studio (czujką $err,hr lub $eax,hr), ale mimo to programik ten bywa niekiedy przydatny.Niektóre z tych narzędzi są na tyle użyteczne, że warto zrobić sobie do nich skróty w łatwo dostępnych miejscach (dotyczy to chociażby Control Panelu). Wszystkie zaś możemy znaleźć wśród linków tworzonych w menu Start przez instalator SDK, w podkatalogu DirectX Utilities.
AI na WarsztacieO niezadowalającym poziomie forum Warsztatu słyszałem już wielokrotnie. Że pytania dotyczą głównie podstaw programowania, że wiele z nich to mniej lub bardziej zakamuflowane "No i co tu jest źle?", że odpowiedzi na większość z nich znajdują się na drugim końcu wyszukiwarki - i tym podobne. A jeśli już jakiś wątek nie podpada pod którąś z tych kategorii, to albo jest kolejnym "genialnym" pomysłem na grę, albo off-topikiem, albo w najlepszym razie dotyczy jakiegoś zagadnienia z zakresu programowania grafiki.
Pewnie coś w tym jest. Jednak ostatnio ku mojemu zaskoczeniu (i pełnej aprobacie) pojawiło się też kilka dyskusji na tematy związane ze sztuczną inteligencją. Niby taki dział też jest, ale jakoś dotąd nie zauważyłem, by był zanadto aktywny.
A tu proszę: kilka ciekawych wątków w ciągu paru dni. Może to nic wielkiego i może niespecjalnie nawet jest tu o czym pisać, bo ich tematyka jest dość ograniczona (głównie sieci neuronowe), a dziedzina raczej akademicka i pewnie nawet niekoniecznie związana z gamedevem. Ale...
Ale myślę, że jednak warto o tym wspomnieć :) Przede wszystkim po to, by nikt nie zapomniał, że gry to nie tylko ładna grafika i że w dzisiejszych czasach powinny one być choć trochę - z braku lepszego słowa - 'inteligentne'.
A poza tym miałem też ochotę zwrócić uwagę na istnienie tego właśnie działu forum Warsztatu - więc niniejszym właśnie to czynię :]
Szpieg++Pakiet Visual Studio to nie tylko samo środowisko programistyczne (IDE), ale i kilka mniejszych narzędzi. Większość z nich dotyczy albo programowania na platformy mobilne, albo .NET, ale jest przynajmniej jedno, które może okazać się przydatne dla każdego programisty Windows. Chodzi o program Spy++ (spyxx.exe).
Co ta aplikacja potrafi? Otóż pozwala ona na podgląd różnych obiektów w systemie, tj. okien, wątków i procesów wraz z ich właściwościami (jak na przykład wartości uchwytów czy ilość zaalokowanych bajtów). Na pierwszy rzut oka może nie wydawać się to jakoś wybitnie zachwycające, jako że zwykły Menedżer zadań potrafi (prawie) to samo, z dokładnością do mniejszej liczby szczegółów.
Wyróżniającym i znacznie przydatniejszym feature'em Spy++ jest bowiem co innego. Umożliwia on mianowicie podglądanie, filtrowanie i logowanie wszystkich lub wybranych komunikatów Windows (WM_PAINT, WM_LBUTTONDOWN, itd.) dochodzących do wskazanego okna lub grupy okien, wraz z ich parametrami (wParam, lParam) oraz wartościami zwróconymi przez okno w odpowiedzi na nie.
Działa to przy tym prosto i intuicyjnie. Najpierw wybieramy sobie okno do podglądania (Spy > Log Messages lub Spy > Find Window), co możemy zrobić przy pomocy przeciągnięcia celownika myszą w jego obszar. Potem możemy określić, jakiego rodzaju komunikaty potrzebujemy przechwytywać oraz jakie informacje chcemy z nich wyciągnąć. Wynikiem będzie log mniej więcej takiej postaci:
<00694> 0002009C P WM_MOUSEMOVE fwKeys:0000 xPos:51 yPos:259
<00695> 0002009C P WM_MOUSELEAVE
<00696> 0002009C P WM_PAINT hdc:00000000
<00697> 0002009C P WM_TIMER wTimerID:5 tmprc:00000000
<00698> 0002009C P WM_TIMER wTimerID:2 tmprc:00000000
która to, jak sądzę, powinna być zrozumiała dla każdego średnio zaawansowanego programisty Windows :]
Po co nam jednak coś takiego?... Ano odpowiedź jest prosta: do debugowania :) Można oczywiście podchodzić do tego w "tradycyjny" sposób przy pomocy pracy krokowej tudzież breakpointów, ale często ogranicza nas tutaj swego rodzaju zasada nieoznaczoności, gdy samo debugowanie zmienia działanie programu - chociażby ze względu na ciągłe przełączenia między nim a IDE. To oczywiście znacznie utrudnia wykrycie i poprawienie usterek.
Jak nietrudno się domyślić, istnieją też inne programy oferujące podobną funkcjonalność co Spy++, jak np. Winspector. Z Visual Studio otrzymujemy jednak dobre narzędzie out-of-the-box, więc czegóż można by chcieć więcej? ;]
Hmm... pewnie tego, by dowiedzieć się mniej więcej, jak ono działa. O tym można przeczytać na blogu jego autora.
Chilijskie łososie mają anemięTak, to stuprocentowa prawda - większość tych ryb w hodowlach w Chile jest dotknięta tą chorobą. Efektem tego będzie na pewno wzrost cen łososia również w sklepach.
No dobrze, ale co z tego - a dokładniej, czemu o tym piszę tutaj (bo fakt, że lubię dania z tych ryb pewnie nie jest wystarczającym uzasadnieniem ;])?... Otóż tytuł tej notki to doskonały przykład informacji, która jest prawdziwa, dokładna, a jednocześnie całkiem nieprzydatna i wywołująca tylko zdziwienie u odbiorcy.
To zupełnie tak, jak z niektórymi... komunikatami o błędach, produkowanymi przez kompilator C++ pracujący w ramach Visual Studio. Z punktu widzenia analizy kodu przez tenże kompilator mają one sens, jednak dla czytającego je programisty często mówią tyle co nic.
Z czasem aczkolwiek można nabrać wprawy i zacząć domyślać się, co tak naprawdę kompilator "miał na myśli", produkując tę czy inną wiadomość o błędzie. To jednak wciąż wybitnie niepraktyczne i dlatego postaram się dzisiaj pomóc w tej kwestii, wyjaśniając prawdziwe znaczenie niektórych komunikatów wypluwanych przez Visual C++:
else to często efekt postawienia o jednego średnika za dużo - mianowicie średnika po bloku if. Jeśli używamy makr do zastępowania powtarzających się fragmentów kodu, to może tu chodzić o niepoprawne zdefiniowanie takiego makra.->, . (kropką) lub ::. Niekoniecznie musi to znaczyć, że została ona źle zadeklarowana (i nie jest klasą/strukturą/unią) - najczęściej po prostu nie została zadeklarowana w ogóle.switch (takie, które zawierają deklaracje nowych zmiennych). Bardzo podobny błąd dotyczy też używania etykiet i instrukcji goto, które, jak wiadomo, są złem potwornym (w tym przypadku żartuję oczywiście).w)string. Nie jest to możliwe - ze względu na obecność konstruktora kopiującego - i choć teoretycznie możliwe jest obejście tego faktu, stosowanie go jest bardzo, bardzo złym pomysłem. Lepiej po prostu przymknąć oko na "marnotrawstwo pamięci" i zmienić unię w strukturę.Nie jest oczywiście możliwe wyliczenie wszystkich okoliczności, w których może objawić się każdy możliwy błąd. Powyższa lista jest więc trochę subiektywna w tym sensie, że zawiera tylko te pozycje, które przytrafiły mi się osobiście podczas kodowania. Bardziej doświadczeni programiści pewnie mogliby ją znacznie poszerzyć.
using w C#W C++ nie ma mechanizmu typu garbage collector, więc jedyne automatyczne zwalnianie obiektów, jakie w tym języku występuje, dotyczy tych lokalnych - tworzonych na stosie. Dlatego wszelkiego typu pomocnicze obiekty (np. uchwyty do zewnętrznych zasobów, jak pliki) deklaruje się tu zwykle jako właśnie zmienne lokalne.
W innych językach z kolei - dokładniej: w tych, w których GC występuje - praktycznie wszystkie obiekty są tworzone na stercie i zarządzane przez odśmiecacz pamięci. Nie musimy więc martwić się o to, jak i kiedy zostaną one zwolnione.
Ta zaleta staje się jednak wadą w sytuacji, gdy chcielibyśmy jednak móc swoje obiekty niszczyć samodzielnie. Jeśli na przykład mamy do czynienia ze wspominanym już uchwytem do zewnętrznego zasobu, to pewnie życzylibyśmy sobie, by został on zamknięty jednak nieco wcześniej niż na końcu działania programu (w niezbyt dużych aplikacjach zazwyczaj dopiero wtedy włącza się garbage collector). Inaczej będziemy niepotrzebnie zjadać zasoby systemowe.
W C# najlepszym sposobem na ograniczenie czasu życia obiektu jest instrukcja using (w tym kontekście to słowo kluczowe nie znaczy wcale użycia przestrzeni nazw!). Podajemy jej po prostu obiekt, którego chcemy użyć wewnątrz bloku; w zamian mamy zapewnione, że związane z nim zasoby zostaną zwolnione po wyjściu z tego bloku. Prosty przykład wygląda choćby tak:
Czemu jednak samodzielnie nie wywołać tego Close czy innej podobnej metody, która służy do zwolnienia zasobu?... Ano choćby dlatego, że istnieje coś takiego jak wyjątki. O ile powoływanie się na ten fakt w C++ bywa zwykle nadmiarem ostrożności, o tyle w .NET wyjątki latają praktycznie stale i mogą być rzucane przez właściwie każdą instrukcję. Nie można więc pomijać możliwości ich wystąpienia i liczyć na to, że mimo niedbałego kodowania wyciek zasobów jakimś cudem nigdy nam się nie trafi.
Może więc lepiej użyć zwykłego bloku try-finally? Zasadniczo using jest mu równoważny, a ponadto ma jeszcze dodatkowe zalety: automatycznie sprawdza istnienie obiektu przez jego zwolnieniem i ogranicza zasięg zmiennej przechowującej do niego referencję (jeśli deklarujemy ją tak, jak powyżej). Ponadto pozwala też nie wnikać w to, jaką metodę - Close, Disconnect, Release, End, ... - trzeba by wywołać na koniec w bloku finally. Jako że wymagane jest, by obiekt w using implementował interfejs IDisposable, będzie to zawsze metoda Dispose, która zawsze posprząta i pozamyka wszystko co trzeba.
O obiektowościKiedy programiści się nudzą, to często spierają się o terminologię. Przedmiotem takich sporów są zwykle nieco mgliste określenia, które czasami - zwykle niesłusznie - używane są jako atrybuty oceniające. Przykład? Choćby "obiektowość" - w zależności od punktu widzenia stwierdzenie, że dany kod/biblioteka/moduł/itp. są bardziej lub mniej obiektowe może stanowić albo zaletę, albo wadę. Zależy to głównie od 'poglądów' danej osoby na temat użyteczności podejścia OO w programowaniu.
Co to jednak znaczy, że dany kod jest obiektowy?... Nie uważam wcale, że należy go w tym celu napisać go w języku obiektowym. Twierdzę na przykład, że Windows API jest całkiem dobrym przykładem biblioteki realizującej obiektowy paradygmat, mimo tego że została ona stworzona w jak najbardziej strukturalnym C. Praktyczna różnica między poniższymi wywołaniami:
jest bowiem właściwie żadna. Dodajmy do tego fakt, że z punktu widzenia programisty-użytkownika w WinAPI występuje zarówno dziedziczenie (rodzajów uchwytów), jak i polimorfizm (funkcje niezależne od typu uchwytów, np. CloseHandle), a więc bardzo obiektowe feature'y.
Jeśli komuś wydaje się to naciągane i twierdzi, że w ten sposób pod obiektowość można podciągnąć właściwie każdy kod, to już spieszę z przykładem ukazującym, że tak nie jest. Otóż większa część biblioteki OpenGL obiektowa na pewno nie jest, zaś w tych nielicznych miejscach gdzie OOP jest niemal konieczny (jak zarządzanie teksturami czy buforami wierzchołków) pojawia się nieco dziwna koncepcja 'indeksów' używanych do identyfikowania poszczególnych obiektów.
Dla niektórych (łącznie ze mną) taki interfejs nie jest szczytem marzeń, a preferowane jest wyraźnie obiektowe podejście DirectX. Absolutnie jednak nie zgadzam się z twierdzeniem, że DirectX jest lepszy, bo bardziej obiektowy - to trochę tak jak powiedzenie, że ten obrazek jest ładniejszy, bo bardziej zielony. W obu przypadkach jest to kwestia gustu i nie powinniśmy zakładać, że cechy pozytywne dla nas są tak samo dobrze odbierane przez innych.
A wyższość DirectX nad OpenGL da się przecież uargumentować na wiele innych, lepszych sposobów :)