Dawno, dawno temu miałem dość oryginalny pomysł na program użytkowy. Miała to być (o wiele) lepsza wersja konsoli Windows, której wyższość miała się objawiać w wyglądzie oraz możliwościach; pewne inspiracje czerpałem tutaj z terminali linuksowych oraz basha (nie, nie chodzi tu o serwis z cytatami z IRC-a ;P). Jak większość “genialnych” pomysłów, także i ten nie doczekał się realizacji. Przyczyną było to, iż nie za bardzo wówczas wiedziałem, jak można zapewnić, aby aplikacje konsolowe funkcjonowały w ramach projektowanego terminala tak, jak to robią w domyślnym oknie konsoli w Windows. Większość problemu rozbijała się o to, jak pobierać dane przez ów program produkowane i jak przekazywać do niego te wpisywane przez użytkownika.
Konsolka mała i ciasna, ale własna
Jako że było to bardzo dawno temu, nie znałem wtedy jeszcze takich terminów jak ‘standardowe wejście/wyjście’, ‘proces potomny’ czy ‘pipe‘, które byłyby tutaj bardzo pomocne. Dowiedziałem się o nich więcej dopiero znacznie później, przy czym wiedza ta w większości odnosiła się do systemu zgoła innego niż Windows :)
Pomyślałem jednak, że do problemu można by wrócić – zwłaszcza, że pytanie o to, jak uruchomić program z przekierowanym wejściem i wyjściem, pojawia się stosunkowo często. Okazało się, że nie jest to specjalnie trudne i sprowadza się właściwie do trzech kroków – z których tylko pierwszy może być nieco zakręcony. Cała procedura (w Windows) może wyglądać na przykład tak:
CreatePipe
), które posłużą nam do odbierania wyjścia od i dostarczania wejścia do procesu potomnego. Należy przy tym zwrócić uwagę na dwie rzeczy:
SECURITY_ATTRIBUTES
(tak, tej którą w 95% przypadków się ignoruje) i przekazując ją do funkcji tworzącej pipe.CreateProcess
). Musimy mu podać właściwe końce rurek w strukturze STARTUPINFO
oraz poinstruować Windows, by były one wykorzystywane (flaga STARTF_USESTDHANDLES
). Ponadto musimy wskazać, że chcemy dziedziczyć uchwyty i że utworzony proces konsolowy nie powinien pokazywać okienka (CREATE_NO_WINDOW
) – to już przekazujemy w parametrach CreateProcess
.WaitForMultipleObjects
. Natomiast organizacja wejścia zależy już od naszej aplikacji. Warto pamiętać, że w standardowej konsoli jest ono buforowane wierszami, zatem i my powinniśmy wysyłać coś do procesu potomnego dopiero wtedy, gdy zbierzemy całą linijkę danych wejściowych.Łatwe, prawda? ;-) W innych systemach operacyjnych robi się to minimalnie inaczej (pipe
, fork
, dup2
, …), ale ogólna idea jest podobna. Jak widać, kilkoma małymi rurkami można zdziałać całkiem sporo :]
Kod skompilowany nie musi być kodem działającym poprawnie – tę prawidłowość zna każdy programista. Z drugiej jednak strony dogłębne przetestowanie programu jako całości to proces żmudny, trudny, niepokojący i obarczony sporym ryzykiem niepowodzenia. A przy tym oczywiście konieczny. Można go jednak nieco usprawnić poprzez ograniczenie możliwości występowania błędów w niektórych częściach programu – takich, które wcześniej potraktujemy testami jednostkowymi (unit tests).
Ten rodzaj testów w skrócie polega na tym, aby każdy napisany element kodu – co zależnie od języka i sposobu programowania może oznaczać np. funkcję lub klasę – opatrzyć jedną lub kilkoma procedurami testującymi. Mają one na celu sprawdzanie, czy ów element robi dokładnie to, co założyliśmy, poprzez użycie go “na zewnątrz”, w innym kodzie. Innymi słowy, taka procedura testowa ma na celu uruchomienie funkcjonalności jakiejś klasy/funkcji/itp. i sprawdzenie, czy rezultaty jej działania są zgodne z oczekiwanymi.
Takie procedury można pisać po zakończeniu implementacji testowanego elementu. Niektóre metodyki tworzenia oprogramowania zalecają jednak, aby… najpierw pisać kod testowy, a dopiero potem ten właściwy! Wbrew pozorom ma to całkiem spory sens. Jeśli bowiem zaczniemy od kodu, który ma naszej klasy używać, to istnieje większa szansa, że od początku wyposażymy ją w interfejs, którego rzeczywiście da się używać bez większych problemów. Na koniec zaś będziemy dysponowali też przykładowym kodem, obrazującym sposób korzystania z danej klasy, co zazwyczaj bywa przydatne.
Według mnie największą zaletą testów jednostkowych jest to, że przy pomocy odpowiednich narzędzi możemy je bardzo szybko napisać w postaci nadającej się do natychmiastowego uruchomienia. Brak konieczności tworzenia nowej aplikacji – dla samych tylko testów – jest niezwykle wygodny. Najczęściej bowiem jedyne, co musimy zrobić, to napisać klasę z procedurami testowymi i oznaczyć ją odpowiednio w kodzie, a następnie po prostu uruchomić testy (co często daje się zautomatyzować jako czynność następną po kompilacji) i obserwować wyniki.
Szczegóły mogą się aczkolwiek różnić w zależności od języka, jako że frameworków dla testów jednostkowych jest sporo. Mamy na przykład:
Tak naprawdę to właściwie dla każdego liczącego się języka istnieje narzędzie tego typu. Polecam więc przyjrzenie się im (i całemu zagadnieniu testów jednostkowych), jeśli wcześniej nie mieliśmy z tym tematem styczności. Bo przecież testowania nigdy dość ;)
Internetowe fora to podobno następny stopień ewolucji list i grup dyskusyjnych Usenetu. Na pewno mają niezaprzeczalne zalety – jak choćby to, że można się do nich dostać przy pomocy zwykłej przeglądarki (nie potrzeba specjalnych klientów), a ich zawartość jest indeksowana przez co sprytniejsze wyszukiwarki. Ale razem z tymi zaletami “w pakiecie” mają też pewną cechę, którą osobiście uważam za wadę – wprawdzie niezbyt ciężką gatunkowo, lecz stanowiącą według mnie pewien mankament.
Mam tu na myśli panujący na forach kult numerków, którego najważniejszym obiektem jest oczywiście licznik postów opublikowanych przez danego użytkownika. Liczba ta, niby ważna dana osobowa, prezentowana jest prawie zawsze po lewej stronie każdej wypowiedzi (w miejscu, które żartobliwie nazywam ‘nagrobkiem’ :]).
Co w niej takiego złego? Obiektywnie patrząc, to tylko statystka, która nie niesie żadnej znaczącej informacji. Nie mówi ani o stażu danego delikwenta na forum, ani o jego codziennej aktywności, ani o jego poziomie zaawansowania w temacie, którego forum dotyczy, ani o umiejętności stosowania przezeń zasad netykiety. Część z tych informacji można odnaleźć, przyglądając się profilowi ze szczegółowymi informacjami o danym użytkowniku. Jednak one nie są pokazywane przy każdym poście – w przeciwieństwie do wspomnianego licznika postów. Czym więc on jest? Otóż jest to taki “magiczny numerek”, kojarzący się nieodparcie z dziesiątkami liczb występujących w grach (MMO)RPG i których mozolne podbijanie zajmuje wiele godzin. Prawie nic nie znacząca wartość, która może być co najwyżej skorelowana z samooceną tego, który zwraca na nią uwagę. Notabene zdarzało mi się widzieć rozwinięcie owego numerka do karykaturalnej postaci RPG-owej: z paskami życia, many i doświadczenia! Na całe szczęście na warsztatowe forum takie wynalazki (jeszcze) nie dotarły :)
Ażeby dodać przeciwwagę dla licznika postów (podbijanego zarówno przez wartościowe, jak i bezsensowne wypowiedzi), na wielu forach – włączając Warsztat – istnieje karma (choć są warianty tego mechanizmu o innych nazwach). W założeniu każdy użytkownik forum może innemu użytkownik dodać jeden punkt owej karmy, jeśli uzna jakąś jego wypowiedź za pomocną, interesującą lub wartościową. Akt ten nazywa się ‘dokarmianiem’, chociaż użyta w tym znaczeniu nazwa ‘karma’ nie ma rzecz jasna nic wspólnego z jedzeniem :) Tak czy owak, sumaryczna wartość zebranej przez forumowicza karmy jest, podobnie jak licznik postów, wyświetlana obok każdej jego wypowiedzi.
Pomysł na pierwszy rzut oka wydaje się znacznie lepszy niż licznik postów, ale ja uważam, że skutki istnienia obu tych mechanizmów są w gruncie rzeczy podobne. Oba są związane z jakimiś rzekomo znaczącymi liczbami, które mają w jakiś sposób opisywać użytkownika, którego się tyczą – co w domyśle znaczy oczywiście, że im większe one są, tym forumowa twórczość danej osoby jest bardziej wartościowa. Oba są też bardzo narzucające się innym użytkownikom; w końcu nie sposób przeoczyć czegoś, co jest wyświetlane obok każdej wypowiedzi.
I oba mogą sprawić, że będziemy – choćby podświadomie – zwracali uwagę na to, aby “przy okazji” dyskusji na forum podbijać te współczynniki jak najwyżej. Samo to nie będzie dobrze jej służyło, a poziom i przydatność forum może wręcz na tym ucierpieć. Zwiększenie licznika postów polega przecież na, niespodzianka, pisaniu postów – a wiadomo przecież, ze ilość nie musi przekładać się bezpośrednio na jakość. Z kolei polowanie na każdy dodatkowy punkt karmy oznacza dopisywanie swoich pięciu centów do każdego pojawiającego się wątku – zwłaszcza takiego z pytaniem lub prośbą o pomoc – nawet jeśli nie mamy w temacie zbyt wiele do powiedzenia (co może wręcz ograniczać się do zakamuflowanego powtarzania wypowiedzi przedmówców). Trudno się nie zgodzić, że podobne praktyki nie służą dobrze funkcjonowaniu forum jako źródła wiedzy i miejsca do rzeczowej dyskusji oraz zadawania pytań i otrzymywania na nie pożytecznych odpowiedzi.
I dlatego przyznam się do radykalnego poglądu. Gdybym dysponował odpowiednią mocą sprawczą, czym prędzej pozbyłbym się tego rodzaju pseudostatystyk – a przynajmniej schował je głęboko i nie eksponował przy każdej okazji. Na forum wymowni powinni być ludzie, a nie liczby :P
Zdążyliśmy się już przyzwyczaić do tego, że w programowaniu grafiki bardzo często wykorzystujemy zastane mechanizmy w zupełnie inny sposób niż ten formalnie założony. I tak: tekstury nie muszą być tylko bitmapami nakładanymi na geometrię, ich rzekome współrzędne mogą tak naprawdę przechowywać pozycję 3D lub normalne, a piksele nie muszą wcale zawierać danych o kolorach.
Analogicznie potok renderowania nie musi też produkować pikseli do wyświetlenia na ekranie! Zamiast tego możliwe jest, aby dokonywał on najpierw wyliczania pewnych właściwości materiałów dla każdego piksela na powierzchni ekranu i to właśnie te informacje zapisywał jako wynikowy “kolor” w teksturze typu Render Target. Później byłyby one wykorzystywane do obliczeń związanych np. z oświetleniem i cieniowaniem sceny. Zaletą takiego podejścia (znanego powszechnie jako Deferred Shading) jest między innymi oszczędzenie sobie wielokrotnego przetwarzania wierzchołków przez vertex shader, np. wtedy, gdy potrzebnych jest wiele przebiegów dla wielu świateł. Z drugiej strony w ten sposób znacznie bardziej obciąża się jednostki Pixel Shader, których liczba w starszych karta jest stała i niezbyt duża. Korzyścią jest jednak uproszczenie całego procesu renderowania, nawet jeśli z powyższego opisu nieszczególnie to wynika ;-)
Pozostaje jeszcze pewien drobny szkopuł. Otóż materiały mają wiele parametrów, które często są złożone, i nie da się ich wszystkich upakować w pojedynczym pikselu, składającym się jedynie z czterech wartości zmiennoprzecinkowych. Dlatego też stosuje się tutaj “wielokrotne cele renderowania” (Multiple Render Targets – MRT), a więc produkowanie przez potok więcej niż jednej wartości koloru naraz. Zwykle po prostu każdy parametr materiału jest zapisywany osobno, do innej powierzchni typu Render Target. Rozróżnienie odbywa się na poziomie pixel shadera. Zamiast zwracać jedną wartość o semantyce COLOR0
(która domyślnie trafia do bufora ramki, czyli – w przybliżeniu – na ekran), może on wypluć także COLOR1
, COLOR2
i tak dalej:
Rezultaty te chcielibyśmy rzecz jasna odebrać i zachować, ale w DirectX wystarcza do tego zwykła metoda SetRenderTarget
, której podajemy powierzchnię działającą jako Render Target o danym indeksie. Są tutaj oczywiście pewne obostrzenia (łącznie z tym najbardziej oczywistym – rozmiaru powierzchni).
Największym mankamentem jest jednak to, że jedynie stosunkowo nowe karty (np. nVidii od serii 6) obsługują MRT. Można to sprawdzić, czytając wartość pola NumSimultaneousRTs
struktury D3DCAPS
, które da nam – niezbyt imponującą, bo wynoszącą zwykle 4 lub 8 – maksymalną liczbę Render Targets podpiętych jednocześnie. W tak niewielkiej ilości (zwłaszcza, jeśli równa jest ona nawet mniej, czyli… 1 :]) może być niełatwo zmieścić wszystkie potrzebne informacje o materiałach.
Ale w programowaniu grafiki zwykle bywa tak, że najlepiej jest wybierać techniki, które jeszcze wydają się nowe. Wtedy bowiem jest całkiem prawdopodobne, że w chwili kończenia projektu wsparcie dla nich będzie już powszechne. Zważywszy na to, że w moim przypadku ciężko jest powiedzieć, kiedy pisanie właściwego potoku renderowania zdołam chociaż zacząć – o zakończeniu nie wspominając – techniki typu deferred zdają się być całkiem rozsądnym wyborem do rozważenia :)
Chociaż programowanie staje się coraz bardziej wysokopoziomowe, zdarza się, że trzeba zejść na niziny i wykonać kilka “staromodnych” operacji’. Dotyczy to na przykład działań na bitach, co jest zazwyczaj związane jest z koniecznością interpretacji różnych sposobów reprezentowania danych. Bardzo dobrym przykładem jest chociażby blokowanie i odczyt/zapis powierzchni (tekstur, bufora ramki, głębokości, itd.) w bibliotekach graficznych. Format pikseli może być bowiem taki, że dostanie się do informacji o kolorze będzie wtedy wymagało nieco zachodu, w tym także operacji na pojedynczych bitach.
Ale nie tym będę się zajmował, gdyż, jak wiadomo, jest to przecież proste ;-) Dzisiaj chciałem zwrócić uwagę na pewien niezwykle ważny szczegół związany z operacjami bitowymi, mogący powodować powstawanie błędów, które na pierwszy rzut oka wyglądają cokolwiek tajemniczo. Chodzi mianowicie o różnice między typami liczbowymi ze znakiem i bez znaku, kiedy chodzi właśnie o działania z użyciem operatorów bitowych.
Ogólna zasada brzmi, aby – jeśli to tylko możliwe – korzystać wtedy wyłącznie z typów liczbowych bez znaku. Mogłoby się wydawać, że to żadna różnica, skoro i tak nie interesują nas wartości zmiennych w interpretacji liczbowej, a jedynie “gołe” bity. Rzecz w tym, że dla kompilatora takie rozróżnienie zazwyczaj nie istnieje i poza nielicznymi wyjątkami (jak std::bitset
w C++) należy stosować zmienne typu liczbowego, gdy operujemy na bitach.
Istnieją zaś przypadki, kiedy uznawanie jakiegoś ciągu bitów za liczbę ze znakiem powoduje zupełnie inne efekty niż interpretacja tego samego ciągu jako liczby bez znaku. Naturalnie cały czas mówimy o korzystaniu jedynie z operatorów bitowych! Co to za szczególne sytuacje?…
-1
. Po rozszerzeniu wartości -1
na 32 bity otrzymamy znów ciąg samych jedynek – lecz tym razem aż trzydziestu dwóch, bo taka jest 32-bitowa reprezentacja liczby -1
. Dla typów bez znaku otrzymalibyśmy naturalnie ciąg 24 zer i 8 jedynek.>>
, lecz z drugiej strony wcale nie jest powiedziane, że zawsze stosowany jest wariant bez znaku dla typów signed
i bez znaku dla unsigned
. Jest to zależne od kompilatora, choć rzecz jasna większość zachowuje się w tym względzie odpowiednio rozsądnie.Tym niemniej nie powinno się na takiej dobroci polegać i należy, do celów działania na bitach, używać typów bez znaku. Inna sprawa, że niektóre języki programowania takich typów w ogóle nie mają i wtedy możemy napotkać (nie)wielki problem. Ale to już całkowicie odrębny temat do narzekania i poruszę go może przy innej okazji :)
Zapewne wielu programistów C++ zetknęło się z taką lub podobną sytuacją. Oto w pocie czoła napisana funkcja w postaci podobnej do tej:
zostaje obejrzana przez bardziej doświadczonego kodera, który stwierdza: “Powinieneś użyć tutaj stałej referencji jako parametru – czyli napisać:
Unikniesz wtedy niepotrzebnego kopiowania string
a”. Wówczas możemy zrobić wielkie oczy i zdziwić się bardzo, zwłaszcza jeśli wiemy co nieco o referencjach w C++. Mimo to sugestia ta jest jak najbardziej na miejscu i powinniśmy się do niej stosować. Wstyd przyznać się, że przez dłuższy czas niesłusznie brałem ją tylko na wiarę, lecz na szczęście jakiś czas temu dowiedziałem się dokładnie, o co tutaj chodzi. Tą cenną wiedzą oczywiście się podzielę :)
Wiadomo, że w C++ referencje to w zasadzie to samo, co (raczej rzadko używane) stałe wskaźniki – czyli zmienne w typu T* const
. Różnią się one aczkolwiek składnią: wszystkie dereferencje są dokonywane przezroczyście i kod wygląda tak, jakbyśmy posługiwali się zwykła zmienną docelowego typu, na który referencja wskazuje (czyli T
). Podobnie stałe referencje są odpowiednikami stałych wskaźników na stałe (const T* const
), czyli takich, które nie pozwalają ani na modyfikację obiektu wskazywanego, ani na zmianę samego wskazania.
W każdym przypadku wskaźniki muszą jednak na coś pokazywać; na coś, co ma określone miejsce w pamięci, czyli adres. W zasadzie podobna reguła dotyczy też referencji – z jednym małym, ale jakże ważnym wyjątkiem.
Otóż powyższą funkcję (w wersji ze stałą referencją jako parametrem) możemy bez problemów wywołać tak, jak poniżej:
Niby nic nadzwyczajnego, ale zauważmy, że tworzony jest tutaj tymczasowy obiekt string
, na który następnie pokazuje referencja w ciele naszej funkcji. Błąd standardu lub kompilatora? Wręcz przeciwnie – it’s not a bug, it’s a feature :)
Po prostu w C++ stałe (i tylko stałe) referencje mogą poprawnie wskazywać na obiekty tymczasowe. Życie takich obiektów jest wówczas przedłużane aż do czasu wyjścia poza zasięg istnienia referencji. W naszym przypadku utworzony tymczasowy obiekt string
będzie więc dostępny w całej funkcji.
Biorąc pod uwagę fakt, że zamiana deklaracji string s
na const string& s
nie kosztuje nas nic (wewnątrz funkcji do parametru odwołujemy się tak samo), możemy zerowym kosztem zyskać sporą optymalizację. Przekazanie referencji kosztuje przecież tyle samo co przekazanie wskaźnika i na pewno jest nieporównywalnie tańsze niż wykonywanie kopii całego napisu.
Dlatego też nie tylko w przypadku klasy string
, ale i we wszystkich podobnych sytuacjach obiekty powinniśmy przekazywać właśnie przez stałe referencje.
Języki programowania są tworami sztucznymi, ale wykazują pewne podobieństwo do języków naturalnych – czyli tych, którymi posługują się ludzie. Mają na przykład swoje gramatyki, składnie i semantyki, chociaż pojęcia te rozumiemy oczywiście nieco inaczej. W obu typach języków można też wyrazić bardzo wiele różnych myśli i relacji, z tym że domena języków naturalnych jest rzecz jasna znacznie szersza.
Ponieważ jednak wszystkimi językami posługują się głównie ludzie, niektóre języki programowania dorobiły się pewnej “otoczki” podobnej do tej, jaka towarzyszy części języków naturalnych. Myślę nawet, że bazując na tym, można wyróżnić odpowiedniki między obiema grupami. Może nie zawsze będą one wystarczająco sugestywne, ale przecież nie zaszkodzi spróbować ;-)
Jak dotąd w przypływie niewytłumaczalnej kreatywności wymyśliłem takie oto pary:
DoSomething() || die();
albo funkcja list
. Aha, i wypada też wspomnieć o najpopularniejszym znaku w tym języku, czyli $
;)Trochę to naciągane? :) Zapewne. Między językami programowania a naturalnymi istnieje przecież jedna zasadnicza różnica: tych pierwszych można się nauczyć zdecydowanie łatwiej i szybciej niż tych drugich. Ale w obu przypadkach sprawdza się stwierdzenie, że ‘znać’ to nie znaczy ‘umieć zastosować’. Więc może szukanie analogii podobnych do powyższych nie jest takie zupełnie bez sensu :]