Stałe i zmienne pola statyczneModyfikator 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:
// foo.cpp
int Foo::ms_iBar = 0;
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:
// foo.cpp
const int Foo::BAR;
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 ;-)
Pożytecznie ściągawkiPamięć 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?... ;-)
Gwiazdka w deklaracjachOto 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 intem" 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ą.
POSIX-owaniePOSIX (Portable Operating System Interface) to taki śmieszny "standard dla systemów operacyjnych", opracowany przez znane skądinąd konsorcjum IEEE. Celem jego stworzenia było zapewnienie jak największej zgodności w działaniu (lub niedziałaniu, rzecz jasna) dla aplikacji pracujących pod kontrolą różnych wariantów Uniksa. W tym celu określone jest pokaźnych rozmiarów API, które zajmować ma się takimi rzeczami jak procesy, wątki, sygnały, I/O, gniazda sieciowe, i tak dalej.
To, co POSIX w tym zakresie teoretycznie oferuje, jest w gruncie rzeczy całkiem zadowalające. Standard nie zabrania zresztą, by implementujące go systemy operacyjne dodawały do tego jakąś własną funkcjonalność.
![]()
Dlaczego więc zgodność poszczególnych systemów z POSIX-em jest w przybliżeniu odwrotnie proporcjonalna do ich popularności? :-) Wbrew pozorom te co bardziej znane, jak różnego rodzaju BSD i dystrybucje Linuksa, nie są pod tym względem doskonałe. Jedynie znacznie bardziej specyficzne Solarisy, QNX-y oraz, co ciekawe, Mac OS X spełniają standard POSIX-a w pełni.
A co z naszymi ulubionymi okienkami? W ich przypadku jesteśmy oczywiście bardzo, bardzo daleko... ale tylko do czasu. Windows można bowiem dość prosto doprowadzić do pełnej zgodności przy pomocy takich pakietów jak Microsoft Windows Services for UNIX czy Cygwin. Może to być dobra pomoc dla tych, którzy chcą pisać przenośne aplikacje bez opuszczania przyjaznego środowiska okienek.
C++, czyli skodź to samZwolennicy 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
Nie zabijaj pikseli swoich nadaremnoPoczą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.
Błędne błędyPrzeglą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.