Modyfikator static
ma w C++ kilka różnych funkcji, a jedną z nich jest deklarowanie składników statycznych w klasach. Jak doskonale wiadomo taki składnik jest wspólny dla wszystkich obiektów tejże klasy i można go używać nawet, jeśli takie obiekty w ogóle nie istnieją.
Zwykle nie ma większych problemów ze statycznymi metodami, natomiast w przypadku pól sprawa często potrafi się skomplikować. Zwłaszcza, że statyczne elementy zmienne i stałe deklarujemy nieco inaczej…
Przede wszystkim należy pamiętać, że statyczne zmienne są właściwie pewnym rodzajem zmiennych globalnych, różniącym się prawie wyłącznie pod względem składniowym. A w przypadku zmiennych globalnych (deklarowanych w nagłówkach poprzez extern
) konieczne jest ich przypisanie do jakiegoś modułu kodu, czyli pliku .cpp. Dzięki temu linker może potem powiązać odwołania do tej zmiennej z nią samą niezależnie od miejsca jej użycia.
To samo dotyczy statycznych pól:
Jest jednak drobna różnica między statycznymi stałymi a zmiennymi polami. W tym drugim przypadku możemy zmiennej nadać początkową wartość; robimy to już w pliku .cpp przy jej definicji – tak jak powyżej. Zrobienie tego od razu w deklaracji jest niedopuszczalne.
Natomiast dla stałych wartość podać musimy i zwykle czynimy to dla odmiany właśnie w pliku nagłówkowym, jeszcze wewnątrz bloku class
:
Wówczas z kolei nie możemy wręcz inicjalizować stałej ponownie w pliku .cpp!
Dość dziwne, prawda? Ale w miarę łatwo to wyjaśnić. Otóż stała ma, jak sama nazwa wskazuje stałą wartość. Dzięki temu, że jest podana w nagłówku, kompilator może zoptymalizować każde miejsce jej normalnego użycia. Zamiast odwoływania się do pamięci, wpisze po prostu wartość stałej “na sztywno” w kodzie wynikowym. Będzie więc to podobne do działania #define
.
Tym niemniej jest to pewna niedogodność, jeśli statyczne stałe i statyczne zmienne definiuje się inaczej. Ale przecież to nie jedyna dziwna cecha C++, która sprawia, że tak ten język lubimy ;-)
Pamięć kodera (ta w hipokampie, nie RAM) jest oczywiście doskonała, ale w większości wypadków także ulotna i mało pojemna. Zaglądanie do dokumentacji w poszukiwaniu każdego drobiazgu jest zaś męczące i mało efektywne. O grubych książkach, które wymagają wertowania spisu treści lub indeksu, już w ogóle nie wspominam.
Stąd np. leksykony O’Reilly, wydawane w Polsce przez Helion oraz Tablice informatyczne tego samego wydawnictwa. Zwłaszcza ten drugi pomysł jest interesujący…
Jego praktyczną (i darmową) realizację znalazłem na stronie pod zaiste wiele mówiącą nazwą AddedBytes.com (dawniej: ILoveJackDaniels.com :)). Tam też można zaopatrzyć się w przydatne tablice zwane cheat sheets.
Jest ich tam całkiem sporo, a po wydrukowaniu mogą być cenną pomocą naukową. Właściwie jedyną ich wadą jest to, że dotyczą tylko technologii webowych: HTML, CSS, PHP, i tak dalej. Tym niemniej mogą być użyteczne choćby jako wzorzec do skonstruowania swoich własnych ściągawek dla bardziej przydatnych koderom narzędzi, jak C++, DirectX, HLSL czy .NET.
Tylko kto odważny się tego podejmie?… ;-)
Oto odwieczny dylemat programistów C/C++: jak deklarować wskaźniki? A dokładniej: w którym miejscu umieścić gwiazdkę? Wybór dotyczy dwóch wariantów składniowych:
Dla kompilatora są one oczywiście równoważne, jednak zwolennicy każdego z nich potrafią podać całe mnóstwo argumentów za jednym i przeciwko drugiemu. I tak: przyklejanie gwiazdki do typu docelowego wskaźnika (tutaj int
) jest popierane tym, że jest ona częścią typu wskaźnikowego (czyli int*
). Z drugiej strony kruczkiem jest, iż deklaracja:
tworzy wskaźnik a
oraz zwykłą zmienną b
– co stanowi argument na rzecz przyklejenia gwiazdki jednak do nazwy zmiennej.
Czy to więc kwestia gustu? Być może. Ja osobiście preferuję styl int* p1;
, a ostatnio odnalazłem jeszcze jeden argument, który może za tym przemawiać. Jeśli bowiem oprócz zadeklarowania wskaźnika zechcemy go też zainicjalizować:
to w konwencji int *p2;
wygląda to na pierwszy rzut oka dość dziwnie. Zgodnie z interpretacją “*p
jest int
em” wychodzi bowiem także na to, iż “inicjalizujemy *p
zerem”. Można więc pomyśleć – zwłaszcza przy mniejszym doświadczeniu w programowaniu w C/C++ – że inicjujemy nie sam wskaźnik, lecz wartość, na którą on wskazuje. To rzecz jasna nieprawda; nadal inicjowany jest wskaźnik, otrzymuje on swój początkowy adres.
Wytrawni koderzy nie popełniliby naturalnie takiej pomyłki. Pamiętajmy jednak, że dobrze napisany kod powinien wymagać możliwie najmniej zastanowienia nad sprawami nieistotnymi. A znaczenie powyższej konstrukcji wydaje się właśnie taką nieistotną sprawą.
Zwolennicy C++ często argumentują, że w języku tym można bardzo, bardzo wiele. Kluczowe jest tu właśnie słówko ‘można’. Powiedziałbym mianowicie, że swego rodzaju potencjalność jest jego istotną cechą…
O co dokładnie chodzi? Najlepiej chyba mówią o tym przykłady – takie jak te poniższe:
finally
w blokach try
. W większości przypadków można jednak obejść się bez niej, stosując technikę znaną jako RAII (Resource Acquistion Is Initializon– pozyskanie zasobu inicjalizuje zmienną).Można więc to i tamto; powyższa lista z pewnością nie jest kompletna. Dawniej takie możliwości doskonale świadczyły o C++ jako o języku niebywale elastycznym. Robiły też dobrze dla jego efektywności oraz wstecznej kompatybilności z C. Krótko mówiąc, były niewątpliwymi zaletami.
Teraz jednak wydają się być listą braków, a w najlepszym przypadku niepodjętych, a koniecznych decyzji projektowych. Niezbędnych głównie dlatego, że twórcy nowszych języków nie wahali się ich podjąć. Dzięki temu w wielu przypadkach oszczędzili pracy i trudnych wyborów programistom, którzy z tych języków korzystają.
A programistom C++ wciąż pozostają możliwości… Niestety, aplikacji nie buduje się z kodu, który można by napisać, lecz z kodu już napisanego. Dlatego korzystanie z owych możliwości zamienia się często w uzupełnianie braków. I realizujemy wtedy stare powiedzenie: Jeśli programista chce mieć coś napisane, powinien napisać to sobie sam :P
Począwszy od wersji 1.1, w pixel shaderach istnieje instrukcja texkill
, której odpowiednikiem w HLSL jest funkcja clip
. Działanie ich obu polega z grubsza na całkowitym odrzuceniu danego piksela, czyli nierysowaniu go. Shader wprawdzie nadal produkuje jakiś rezultat, ale nie jest on uwzględniany, a więc rzeczony piksel po prostu nie pojawia się na ekranie.
Biorąc pod uwagę to, że w kodzie pixel shadera możemy już całkiem sporo, można by uznać, że texkill
to niezłe narzędzie do eliminowania niechcianych fragmentów sceny (przycinanych np. ustalonymi płaszczyznami). Lecz jak zawsze jest pewne ‘ale’, a nawet kilka. Oto one:
texkill
nie powoduje natychmiastowego zakończenia działania shadera dla danego piksela. Jak wiadomo, piksele (wierzchołki zresztą też) są przetwarzane na GPU równolegle i dlatego nie ma dla nich odpowiednika instrukcji ret
(urn
) ze zwykłych programów.texkill
może zepsuć anti-aliasing. Shader jest bowiem wywoływany tylko raz dla jednego piksela, zaś ponownie wygładzenie krawędzi wymagałoby modyfikacji koloru pikseli sąsiadujących z tym usuniętym – czyli ponownego uruchomienia dla nich pixel shadera.texkill
uniemożliwia szybkie odrzucenie pikseli poprzez tzw. wczesny Z-Test, czyli test dokonywany przed uruchomieniem pixel shadera. Skoro bowiem piksel może zniknąć z innych powodów, to testowanie głębokości trzeba zostawić na później (chyba że wyłączony został zapis do Z bufora).Nie znaczy to oczywiście, że texkill
jest zły, bo w niektórych sytuacjach bywa nieoceniony. Przykładem jest choćby opisane przez Blinda i Rega rzucanie cieni przez obiekty z częściowo przezroczystymi teksturami. Ważne, aby z tego “triku” korzystać ze świadomością możliwych konsekwencji – wydajnościowych, rzecz jasna.
Przeglądając ostatnio kod swojego… nazwijmy to umownie: silnika (;P), zauważyłem pewne dość ciekawe rozwiązanie. Jednak w tym przypadku ‘ciekawe’ znaczy mniej więcej tyle, co ‘zastanawiające’, ‘wątpliwe’ i ‘cokolwiek dziwne’. Chodzi o obsługę przeróżnych błędów czasu wykonania – czyli wyjątków – a w szczególności o ich rodzaje, wyróżnione w postaci klas obiektów reprezentujących rzeczone błędy.
Nie wiedzieć czemu podzieliłem je bowiem ze względu na źródło błędów. Zaowocowało to klasami wyjątków pochodzących od Windows API, od DirectX czy wreszcie takich, których źródło tkwiło w samym kodzie silnika. Oczywiście taki obiekt wyjątku miał też jakieś dodatkowe dane, co w najprostszej wersji ograniczało się po prostu do tekstowego komunikatu o tym, co się stało.
Nie waham się stwierdzić, że taka organizacja klas wyjątków to pomysł po prostu poroniony :) Na etapie obsługi błędów o wiele bardziej interesuje nas bowiem ich przyczyna, która powinna być określona jak najdokładniej. Stwierdzenie, że mamy do czynienia np. z błędną wartością argumentu naszej funkcji, mówi o wiele więcej niż fakt, że wartość ta spowodowała dalej błąd przy korzystaniu z jakiejś zewnętrznej biblioteki – na przykład DirectX. Różnica polega chociażby na tym, że dysponując klasami wyjątków w rodzaju InvalidArgumentException
zamiast WindowsAPIError
możemy wcześniej sygnalizować nieprawidłowe sytuacje. Ich obsługa jest też wygodniejsza, gdyż duża liczba klas wyjątków ułatwia oddzielanie od siebie fragmentów odpowiedzialnych za radzenie sobie z różnymi typami błędów.
Morał jest więc prosty: ważniejsze jest to, co konkretnie poszło źle – nie zaś to, gdzie rzecz się stała. Świadczą o tym także wyjątki spotykane w bibliotekach pokroju STL, .NET czy JDK. Wszędzie tam powyższa zasada jest nadrzędna.
W potocznym rozumieniu tekstura to taki obrazek, który nakłada się obiekt trójwymiarowy, aby w ten sposób imitować wygląd jego powierzchni. Rzeczywiście, dawno temu była to ich jedyna funkcja. Tego rodzaju tekstury (nazywane teksturami rozproszenia) są oczywiście nadal niezbędne. Obok nich powstało jednak całe mnóstwo innych rodzajów tekstur, które są wykorzystywane podczas renderowania scen 3D.
Wśród nich są na przykład takie, które zawierają pewne niezbędne informacje na temat obiektów na scenie – nie tylko zresztą geometrii. Są to chociażby:
Mapy wysokości (height maps). To czarno-białe tekstury, używane do modelowania terenu. Jasność konkretnego piksela odpowiada wysokości terenu w danym punkcie. Taka tekstura musi być naturalnie przetworzona na odpowiednie trójkąty (co specjalnie trudne nie jest), ale jej używanie zamiast innych reprezentacji ma dwie wyraźne zalety. Po pierwsze, umożliwia regulowanie stopnia szczegółowości (Level of Detail, LoD) wyświetlanego terenu. Po drugie, mapy wysokości są bardzo łatwe do wykonania za pomocą dowolnego programu graficznego nawet przez średnio uzdolnionego w tym kierunku kodera :)
Mapy normalnych (normal maps) obrazują z kolei wektory normalne punktów powierzchni. Pomysł jest bardzo prosty: kolor każdego piksela w formacie RGB odpowiada normalnej o współrzędnych XYZ. Ponieważ współrzędne te są ograniczone (długość normalnej to zawsze 1), mogą być zapisane jako kolor. Mapa normalnych jest potem wykorzystywana przy obliczeniu oświetlenia per-pixel.
Tego rodzaju tekstury są przygotowywane wcześniej i obok modeli, tekstur rozproszenia i innych danych stanowią informacje umożliwiają renderowanie sceny. Oprócz nich w trakcie samego rysowania wykorzystywane są też inne tekstury, tworzone na bieżąco i zwykle niezachowywane na później. Wśród tych efemerycznych tekstur mamy na przykład:
Mapy odbić środowiskowych (environmental maps) służą do symulowania przedmiotów o powierzchniach lustrzanych. Podobnie jak wyżej, wymagają osobnego przebiegu renderowania, i to często nawet niejednego (jak w przypadku map sześciennych). Tak powstałe obrazy odbić są potem nakładane na przedmiot, który dzięki temu sprawia wrażenie, jakby odbijał w sobie resztę sceny.
Potencjalnych i aktualnych zastosowań tekstur jest o wiele więcej. Ale już na tych przykładach widać, że tekstury tak naprawdę nie są obrazkami, a jedynie zbiorami jakichś informacji, które tylko z konieczności są zapisywane w postaci kolorów pikseli. Być może niedługo staną się one pełnoprawną “pamięcią operacyjną” kart graficznych, którą można będzie np. alokować i zwalniać w kodzie shaderów. Jak dotąd możliwy jest ich odczyt oraz w pewnym stopniu zapis (zależnie od modelu shaderów), ale kto wie – może wkrótce doczekamy się czegoś więcej?…