W wielu API funkcje mają bardzo prosty sposób powiadamiania o tym, czy ich wykonanie zakończyło się sukcesem czy porażką. Albo więc wykorzystują typ bool
bezpośrednio, albo wpasowują się w konwencję, iż niezerowa wartość liczbowa jest tożsama z prawdą, a zero z fałszem. To sprawia, że możliwe jest pisanie warunków podobnych do poniższego:
Ładne to i opisowe – wręcz samodokumentujące się. Ale czasami tak się zrobić nie da, bo wartości zwracane nie chcą współpracować z tym modelem.
Przykład? To większość biblioteki runtime języka C oraz API systemów uniksowych. O ile tylko rezultatem funkcji należącej do któregoś z tych dwóch zbiorów nie jest wskaźnik, konwencja informowania o powodzeniu lub niepowodzeniu jest zwykle dość osobliwa. Według niej zero oznacza sukces, natomiast porażka wykonania jest sygnalizowana przez wartość mniejszą od zera – zazwyczaj -1
. Oczywiście nijak nie pasuje to sposobu interpretowania liczb jako wartości logicznych. Sprawia to, że sprawdzanie rezultatu takich funkcji może wyglądać cokolwiek enigmatycznie:
Powód, dla którego to działa, jest dość prosty. W standardowym sposobie zapisu liczb całkowitych, stosowanym na zdecydowanej większości typowych i nietypowych maszyn (zwanym uzupełnieniem do 2 – U2), wartość -1 to w zapisie binarnym same jedynki. Negując je bitowo, otrzymujemy same zera – czyli zero, a więc logiczny fałsz. A odwrotnością fałszu jest oczywiście prawda i wszystko działa poprawnie. Wygląda więc tak, jakby skromna tylda zdołała “naprawić” funkcję, by zachowywała się zgodnie z oczekiwaniami…
Tylko czy aby na pewno nowy zapis jest bardziej sugestywny? Mam nadzieję, że każdy potrafi poprawnie odpowiedzieć na to pytanie we własnym zakresie :) Na koniec jednak muszę – dla spokoju sumienia – ostrzec wszystkich: zdecydowanie nie róbcie tego w domu :D
Kolega rr- rzucił dzisiaj na kanale #warsztat ciekawy problem rysowania dowolnych wielokątów za pomocą samych trójkątów. Jak wiadomo, karty graficzne posługują się właśnie trójkątami, zatem w celu wyświetlenia wielokąta należy go odpowiednio podzielić. Matematycy dowodzą, że jest to zawsze możliwe, i podają prosty sposób dla wielokątów wypukłych: należy po prostu narysować wszystkie przekątne wychodzące z jednego wierzchołka. Nie jest trudno przełożyć ten przepis na kod.
Co jednak z dowolnymi wielokątami? Tu sprawa wydaje się bardziej skomplikowana, chociaż – jak się ukazuje – rysowanie przekątnych można na ten przypadek uogólnić. Wpadłem jednak na inny pomysł, polegający na odpowiednim “chodzeniu” po wierzchołkach naszego wielokąta i “odcinaniu” od niego kolejnych trójkątów brzegowych. Wygląda to mniej więcej tak:
Na tym rysunku można prześledzić, jak wyglądają kolejne cykle spaceru po krawędziach wielokąta – oznaczyłem je różnymi kolorami:
Z tym sposobem wiąże się oczywiście problem stwierdzenia, czy dany odcinek należy do wielokąta – co w ogólności nie musi być takie proste ani efektywne (może mieć złożoność liniową). Dodatkowo wielokąt może być “wredny” i niezbyt dobrze poddawać się operacji obcinania trójkątów. Na szczęście można udowodnić, że w każdym cyklu da się przynajmniej jeden taki trójkąt wyodrębnić. Te dwa fakty powodują, że cała operacja może mieć złożoność sięgającą nawet O(n3), chociaż pewnie da się ją zaimplementować lepiej.
Jest naturalnie bardzo możliwe, że algorytm ten jest znany od dawna, a ja po prostu nie przeczesałem Internetu dość dokładnie w poszukiwaniu już istniejącego opisu. Jednak biorąc pod uwagę to, co przed chwilą powiedziałem o jego możliwej “efektywności”, nie jest to znów takie pewne ;-) Istnieje aczkolwiek szansa, że może się on przydać komuś, kto implementuje bibliotekę graficzną 2D w oparciu o API w rodzaju DirectX czy OpenGL.
Gdybym chciał byś złośliwy, to stwierdziłbym, że w C++ nawet ‘nic’ (czyli NULL
) nie jest takie, jak być powinno. Ale ponieważ w rzeczywistości jestem wcieleniem łagodności (;]), napiszę raczej o tym, jak można zaradzić na niedogodności obecnej postaci wskaźnika pustego w C++.
Cały problem z NULL
-em polega na tym, że nie jest on wartością odpowiedniego typu. Dwie klasyczne definicje tej stałej – jako 0
lub (void*)0
– mają zauważalne mankamenty. Pierwsza definiuje NULL
jako liczbę, co sprawia, że dozwolone są bezsensowne podstawienia w rodzaju:
Natomiast druga nie da się wprawdzie skonwertować na typ liczbowy, ale nie da się też niejawnie zmienić w wartość żadnego innego typu wskaźnikowego niż void*
. A tego oczekiwalibyśmy po wskaźniku pustym. Dlatego z dwojga złego w standardzie przyjęto pierwszą definicję i NULL
jest po prostu zerem.
To, czego naprawdę byśmy chcieli po NULL
, to wartość 0
, która:
To zaś da się osiągnąć, pisząc odpowiednią… klasę:
Sztuczką są tu oczywiście szablony operatorów konwersji. Zapewniają one możliwość traktowania naszego NULL
-a jako wartości dowolnego typu wskaźnikowego – łącznie ze wskaźnikami do składowych klas. A ze względu na brak innych konwersji, nie jest możliwe automatycznie przypisanie naszego pustego wskaźnika do zmiennej liczbowej.
Taki NULL
jest na tyle dobry, że działa nawet z uchwytami Windows API, gdyż wewnętrznie są one zdefiniowane jako specyficzne wskaźniki. Dziwi więc, dlaczego nadal nie ma go chociażby w bibliotece standardowej C++. Najwyraźniej nawet jeśli chcemy mieć ‘nic’, trzeba się trochę napracować ;P
W terminologii DirectX programowalny potok graficzny ma dwie ważne części: vertex shader i pixel shader. Nazwa tej drugiej jest zasadniczo trochę myląca. Sugeruje ona, że shader jest wykonywany dla każdego piksela na ekranie, co tak naprawdę jest dalekie od prawdy.
Tak naprawdę bowiem “piksele” te są dopiero kandydatami do zapisania w buforze tylnym. Po drodze mają bowiem wiele okazji na to, aby być całkowicie wyeliminowane z renderingu. Może to się stać z któregoś z poniższych powodów, które jednak nie wyczerpują wszystkich możliwości:
A zatem wyjście pixel shadera niekoniecznie musi bezpośrednio trafić na ekran. Nie powinniśmy więc brać liczby pikseli w buforze ekranu za średnią liczbę wywołań tego shadera, a już na pewno nie jako górną granicę.
Dlatego też trzeba przyznać, że używana w OpenGL nazwa ‘fragment program‘ jest o wiele lepsza niż ‘pixel shader’. Fragment (wyjście shadera) nie jest bowiem jeszcze pikselem, a jedynie kandydatem na niego, który może odpaść przy wielu okazjach.
Czasami dobrze jest reagować na wszystkie przychodzące do okna wciśnięcia klawiszy w jednym miejscu. W C# ustawia się wtedy właściwość KeyPreview
dla formy. Sprawia to, że dostaje ona zdarzenia klawiatury niezależnie od tego, która z potomnych kontrolek ma aktualnie fokus. Dzięki temu można implementować polecenia aktywowane wciśnięciami określonych klawiszy lub ich kombinacji, których nie da się przypisać np. do skrótów klawiszowych w menu:
Jeśli jednak nic w tej kwestii nie zrobimy, zdarzenia klawiatury domyślnie i trafią do kontrolki aktywnej, co niekoniecznie musi nam się podobać. W powyższym przykładzie nasza prosta konsolka “złapałaby” na przykład wszystkie wciśnięcia klawisza tyldy jako znaki ~.
Jak temu zapobiec? Wystarczy wykorzystać właściwość SuppressKeyPress
z klasy KeyEventArgs
:
To sprawi, że zdarzenie wciśnięcia klawisza nie będzie przekazywane dalej i żadna kontrolka go nie otrzyma. To przydatne, jeśli mamy ręcznie zakodowane skróty klawiszowe, które pokrywają się ze standardowymi sekwencjami.
Wygasła już chyba dyskusja w wątku na forum Warsztatu, w którym zwrócono uwagę na poziom pojawiających się w serwisie gamedev.pl projektów. Dotyczyło to zwłaszcza screenów, jakie to można niekiedy rotacyjne oglądać na stronie głównej. Dość często podnoszonym argumentem było to, iż marny ich poziom szkodzi wizerunkowi Warsztatu na zewnątrz. Proponowane przy okazji rozwiązania wahały się zwykle w przedziale między całkowitym usunięciem wszystkim tetrisów, pongów, projektów konsolowych i innych subiektywnie ocenianych “zaniżaczy poziomu”, a wysłaniem ich do specjalnie przygotowanego działu (nazywanego np. ‘żłobkiem’), z którego screeny nie wystawałaby na widoku publicznym na stronie głównej serwisu.
Troska o ów poziom Warsztatu jest doprawdy rozczulająca. Zastanawiam się tylko, jaka stoi za nią motywacja? Może to być chęć, by serwis oraz społeczność były postrzegane na zewnątrz jako bardziej profesjonalne (niż w rzeczywistości, chciałoby się dodać). Obecność screenów, które niekoniecznie powalają oglądających na kolana, ma w tym oczywiście przeszkadzać.
A ja pytam: jaki jest w tym cel oraz sens? Po pierwsze, na tle innych podobnych serwisów, Warsztat nie ma się czego wstydzić (dla porównania można obejrzeć jego odpowiedniki z innych krajów). Zaś po drugie, aspirowanie do “bardziej profesjonalnej” roli w przypadku serwisu nieanglojęzycznego nie ma większego sensu. W przeliczeniu na nadal skromny przemysł gier komputerowych w Polsce (jeden Wiedźmin wiosny nie czyni), obecna postać i poziom Warsztatu – zwłaszcza społeczności – są chyba nawet lekką nadwyżką.
A mimo to zdaje się, że cierpimy od jakiegoś czasu na kompleks rozpaczliwego udowadniania światu, iż Warsztat to serwis jak najbardziej poważny i – powtórzmy jeszcze raz to kluczowe słowo – profesjonalny.
Otóż nie, to nieprawda i stanowczo przeciwko temu protestuję! Warsztat zawsze był i będzie serwisem i społecznością skupiającą amatorskich oraz zawodowych programistów gier – w tej właśnie kolejności. Wszelkie próby zmiany tego stanu rzeczy, polegające na składaniu różnorodności i walorów edukacyjnych Warsztatu na ołtarzu utrzymania ‘poziomu’ i poprawiania ‘wizerunku’ na pewno nie mogą skończyć się dobrze.
W programowaniu relacja ‘jest’ najczęściej oznacza możliwość potraktowania pewnej wartości (lub ogólniej: obiektu) jako należącego do określonego typu (klasy). Od kiedy wynaleziono dziedziczenie, obiekty mogą być polimorficzne – czyli być traktowane tak, jakby należały do kilku różnych typów danych. Dwa proste przypadki obejmują: zwykłe dziedziczenie publiczne, gdy obiekt klasy pochodnej jest też obiektem klasy bazowej, oraz implementowanie abstrakcyjnych interfejsów.
Czasami jednak to, co w związku ze słówkiem ‘jest’ bywa intuicyjne, nie zawsze sprawdza się w praktyce. Tak jest chociażby wtedy, gdy bierzemy pod uwagę:
reinterpret_cast
) skonwertujemy odwołanie do obiektu na odwołanie do klasy bazowej, będziemy mogli z niego poprawnie skorzystać.List<string>
na List<object>
w C#, musimy świadomie wykonać kopię pojemnika.Zatem proste, zdawałoby się, stwierdzenie “coś jest jakiegoś typu”, w programowaniu może wcale nie być takie oczywiste. Takie są aczkolwiek uroki naszego ulubionego OOP-u :]