Napiszę dzisiaj o nierzadko zapominanej klasie z biblioteki standardowej C++, czyli o tytułowym zbiorze bitów – std::bitset
. Działania na pojedynczych bitach nie są raczej czymś, co wykonujemy codziennie, ale zdarzają się sytuacje, gdy stanowią one najrozsądniejsze rozwiązanie w danej chwili. Takim typowym przypadkiem jest zbiór flag logicznych: kilku opcji działających jak przełączniki, których możliwą wartością jest tylko true
albo false
. Wszelkie API doskonale zna takie konstrukcje, czego przykładem może być metoda Clear
urządzenia DirectX:
D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER
jest właśnie kombinacją flag bitowych, a dwie występujące tu stałe zostały odpowiednio zdefiniowane, by dało się je łączyć operatorem sumy bitowej |
.
Używając std::bitset
możemy nieco uprościć obsługę takich opcji. Zamiast deklarowania specjalnych wartości, możemy użyć zwykłego enum
a, np.:
bitset
jest wtedy łatwy w użyciu:
Zachowana jest przy tym “kompatybilność wsteczna” w stosunku do ręcznie definiowanych stałych, jeśli chcielibyśmy z nich korzystać:
Na koniec wspomnę tylko, że aby korzystać z klasy bitset
do tych lub innych zastosowań, należy wpierw dołączyć standardowy nagłówek o nazwie… bitset :)
Ponieważ programowaniem zajmuję się już od dość długiego czasu, mam w tym nieco doświadczenia praktycznego. Przez ten czas zdążyłem też zmienić część swoich poglądów na niektóre rzeczy z tym związane. Mówiąc trochę górnolotnie: stałem się bogatszy o wiedzę z życia wziętą :)
Jedną z takich nauczek życiowych jest to, by nie ufać zbytnio standardom, zaleceniom i innym tzw. dobrym praktykom podpowiadających (a czasem wręcz nakazujących), jak powinien wyglądać pisany przez nas kod. Dotyczy to na przykład instrukcji rzekomo złych “z natury” i innych tego rodzaju przesądów. Tak, uważam określenie ‘przesądy’ za uprawnione w wielu przypadkach.
Nie jestem ponadto odosobniony w tych poglądach – ba, wśród swoich kolegów po fachu mógłbym wskazać wielu kilku, którzy są pod tym względem nawet bardziej… radykalni :) Jak już jednak wspomniałem na początku, nie zawsze tak było. Kiedyś miałem bardziej rygorystyczne i pryncypialne podejście do kwestii tego, jak kodować należy lub nie należy. Skąd brałem dla niego uzasadnienie, jeśli nie z praktyki, której wtedy zbyt wiele jeszcze nie miałem?…
Ano właśnie – tu dochodzimy do sedna. Przekonanie to brało się głównie – jeśli nie wyłącznie – ze wszelkiego typu materiałów pomocniczych do nauki programowania: książek, kursów, artykułów, czasem dokumentacji. Choćby nie wiem jak techniczne i merytoryczne były te teksty, w każdym – co do jednego, z moim skromnym dziełem włącznie – znajdą się rady, zalecenia, wskazówki i inne “metainformacje” odnośnie tego, jak programować należy – a nie tylko o tym, jak można. Czy ktoś widział w kursie programowania opis instrukcji goto
(że pozwolę sobie użyć najbardziej typowego przykładu), który nie zawierał chociaż śladowej wzmianki o tym, iż.. no, w zasadzie to nie należy jej używać?… …Tak myślałem ;-)
Nasuwającą się od razu repliką w obronie autorów jest sugestia, że w sumie to przecież całkiem dobrze, iż uczą oni nowicjuszy tego, co sami musieli wcześniej wypracować przez lata. Nie wątpię wręcz, że taka intencja – oszczędzenia początkującym trudności – rzeczywiście autorom przyświeca. Dodają oni do pisanych przez siebie tekstów, kursów, tutoriali, itp. liczne wskazówki, te wszystkie dos and don’ts właśnie dlatego, iż wierzą w to, że w ten sposób pomogą swoim czytelnikom od razu, już na starcie kodować w sposób przejrzysty, efektywny, elastyczny, elegancki, “łatwo konserwowalny”, i tak dalej.
Chwalebne intencje – nie da się ukryć. Ale nietrudno jest wskazać przynajmniej dwa błędy takiego rozumowania. Pierwszym jest założenie, że każdą wiedzę i każdą umiejętność da się przekazać innym – czyli że w ogóle istnieje “droga na skróty”, niejako omijająca zdobywanie doświadczenia praktycznego w danej dziedzinie (w tym przypadku w programowaniu). Żeby stwierdzić, że to jednak nieprawda, wystarczy zastanowić się, czy ukończenie podstawowego kursu szachów u arcymistrza da nam od razu ELO 2500. Niestety w rzeczywistości dojście do takiego poziomu wymaga dziesiątków tysięcy rozegranych partii – i nic się na to nie poradzi.
Drugim błędem jest przyjmowanie, że przekazywane mądrości są faktycznie odpowiednie dla odbiorców. Nie, nie chcę wcale sugerować, że ktoś może mieć traumę z powodu wczesnych doświadczeń z hermetyzacją czy wzorcem fabryki ;) Raczej sugeruję, że przywiązując zbyt wielką wagę do przekazywanych zaleceń, początkujący mogą stracić wiele czasu na dopasowywanie swoich pierwszych programów do ich wysoko zawieszonej poprzeczki – najczęściej zupełnie niepotrzebnie. Gra w zgadywanie liczby, kółko i krzyżyk czy tetris nie potrzebują zwykle skomplikowanej, wewnętrznej architektury z dokładnie zaprojektowanymi relacjami między klasami i starannym oddzieleniem interfejsu od implementacji. Prosi się to bowiem o przytoczenie znanego powiedzenia na temat artylerii i pewnego gatunku insekta :)
Tak oto okazuje się więc, że zalecenia i rady tudzież – szumnie nazywane – standardy kodowania są “dziedziczone” przez początkujących programistów od autorów książek, kursów i tutoriali, którzy je tam umieszczają pomiędzy objaśnieniami kolejnych konstrukcji językowych czy elementów API. A że ponadto, jak opisałem wyżej, standardy te zbyt wcześnie poznane mogą być nie tyle nawet zbędne, co wręcz szkodliwe, pozwalam sobie nazwać je ‘chorobami dziedzicznymi’. Nic tak bowiem nie trafia do wyobraźni jak obrazowe porównanie ;>
Zakończyć chcę jednak optymistycznym akcentem. Otóż choroby te są jak najbardziej uleczalne. Terapia jest też nieskomplikowana i – jak sądzę – skuteczna. Trzeba po prostu kodować, kodować i jeszcze raz kodować :)
Jeśli widzieliśmy już w swoim wystarczająco dużą ilość kodu w C lub C++, to istnieje pewna szansa, że natrafiliśmy na konstrukcję znaną jaką do-while(false)
. Występuje ona w dwóch wersjach. W pierwszej jest to właściwie goto
w przebraniu – dzięki otoczeniu jakiegoś fragmentu kodu taką “pętlą” sprawiamy, że instrukcja break
będzie powodowała skok tuż za nią. To pozwala na “wyjście” z danego fragmentu kodu np. wtedy, gdy wystąpi w nim jakiś błąd – oczywiście przy założeniu, że w środku nie ma kolejnej pętli lub switch
a ;)
O drugiej wersji pisałem kilkanaście miesięcy temu. Jej zastosowaniem są makra preprocesora zawierająca kilka instrukcji; zamknięcie ich w do-while(false)
zamiast w zwykły blok kodu ({ }
) gwarantuje nam brak błędów składniowych niezależnie od miejsca użycia takiego makra.
Widać więc, że pętla z zawsze fałszywym warunkiem ma – mimo wszystko – jakieś zastosowanie… A co z analogicznym if
em? Czy if(false)
ma jakieś sensowne zastosowanie poza ukazywaniem, że osoba używająca takiej konstrukcji do wyłączania kodu z kompilacji zapomniała o istnieniu dyrektywy #if
, że o komentarzach nie wspomnę?…
Okazuje się, że jednak ma – przynajmniej mi dało się takowe zastosowanie znaleźć. Chcąc je zaprezentować, zacznę od takiego oto ciągu if
ów:
który w założeniu może być oczywiście dowolnie długi i niekoniecznie musi też udawać switcha
dla string
ów. Rzecz w tym, aby każdy if
miał podobną formę – na tyle podobną, że aż prosi się o jej zmakrowanie:
To by wyszło gdyby nie feler w postaci pierwszego if
a, który bynajmniej nie zaczyna się wcale od else
‘a. Można jednak sprawdzić, żeby tak było. Jak?… Ano właśnie dzięki zawsze fałszywemu if(false)
:
Robi on dokładnie nic, ale jednocześnie sprawia, że następujące dalej, prawdziwe if
y mają dokładnie taką samą strukturę. Teraz już możemy je skrócić:
Naturalnie dla trzech wariantów taka zabawa nie ma specjalnego sensu, ale dla dłuższej listy – czemu nie? Sam użyłem tej sztuczki parę razy, m.in. w parserze wyrażeń matematycznych, gdzie ciąg if
ów wybierał wywołanie jednej z ponad 20 wbudowanych w niego funkcji. Nie jest to ładne – trzeba to przyznać – ale cóż, takie jest życie: czasami rozwiązania eleganckiego po prostu nie ma…
XML kojarzy się raczej z technologiami webowymi i językami, które się z nimi wiążą – jak C#, Java, Ruby, itp. Nie zapominajmy jednak, że jest to także pewien format zapisu informacji. Ma on swoje wady (duży rozmiar), ale również zalety – jak np. możliwość edycji dowolnym edytorem tekstu – które mogą być pożyteczne w wielu zastosowaniach. Także wtedy, gdy piszemy w C++.
Oczywiście stworzenie własnego parsera XML nie jest bardzo trudne – mogę to powiedzieć z czystym sumieniem, gdyż samemu zdarzyło mi się takie dzieła (o różnym stopniu skomplikowania i funkcjonalności) popełnić w ilości co najmniej dwóch :) Nie jest to jednak zajęcia specjalnie kreatywne. Są też dla niego alternatywy i właśnie o jednej z nich – bibliotece dla C++ przeznaczonej do obsługi XML-a – chciałbym dzisiaj napisać.
Nazywa się ona TinyXML i jest to nazwa nadzwyczaj trafna. Jest to bowiem mały (ok. kilka tysięcy linii) i zgrabny kawałek kodu, który pozwala operować na dokumentach XML zarówno sensie odczytu, jak i zapisu. Rozprowadzany jest on na licencji ZLib, pozwalającej na używanie w produktach komercyjnych i niekomercyjnych. A ponieważ składa się na niego tylko kilka plików .cpp, łatwo korzystać z niego w projektach albo nawet dołączyć do własnego frameworka czy engine‘u.
A korzystać z TinyXML warto. Wśród istotnych cech tej biblioteki można wymienić to, że:
<!DOCTYPE>
(czyli z czymś, czego nikt normalny już nie używa ;-))Nie mniej ważne jest to, że biblioteka TinyXML jest prosta w obsłudze. Jeśli mieliśmy coś wspólnego z przetwarzaniem XML w innych językach programowania, nie powinna nam ona sprawić żadnego kłopotu. Nie ma w niej naturalnie żadnych zaawansowanych feature‘ów w rodzaju zapytań XPath, transformacji XSL czy walidacji w oparciu o XML Schema, bo nie do takich zastosowań została ona stworzona. Jeżeli jednak nie potrzebujemy takiej zaawansowanej funkcjonalności, a chcemy jedynie np. odczytywać i zapisywać XML-owe pliki konfiguracyjne, stworzyć klienta RSS czy wykonywać tysiące innych pożytecznych, a nieskomplikowanych operacji na znacznikowym formacie danych, to TinyXML jest całkiem dobrym wyborem.
No, chyba że bardzo lubimy samodzielne pisanie parserów ;)
W wielu językach wyjątki to niemal obowiązkowa metoda zgłaszania i obsługi błędów. W innych (tj. w C++) jest ona w dużym stopniu opcjonalna. W obu przypadkach istnieją zwykle dostępne out-of-the-box klasy wyjątków, których obiekty można wyrzucać, bez tworzenia swoich własnych. Ich używanie jest zdecydowanie wskazanie. Warto więc przyjrzeć się, jakie predefiniowane obiekty wyjątków przewidują standardowej biblioteki różnych języków.
I tak w C++ mamy bazową klasę exception
, która zawiera w sumie niewiele więcej ponad komunikat o błędzie. Zresztą dotyczy to tak naprawdę wszystkich wbudowanych w C++ klas wyjątków; tym, co je rozróżnia, jest po prosty typ. Mamy tu prostą hierarchię dziedziczenia, wyróżniającą klasy bazowe: runtime_error
dla niskopoziomowych błędów czasu wykonania (jak na przykład overflow_error
) oraz logic_error
dla błędów logicznych aplikacji. Z tej drugiej, bardziej dla nas interesującej, wyprowadzone są też wyjątki nieco bardziej szczegółowe, jak np.: invalid_argument
, range_error
, domain_error
czy lenght_error
. Wszystkie cztery są zresztą podobne do siebie pojęciowo i wydają się służyć głównie do sygnalizowania nieprawidłowych wartości argumentów funkcji. Do innego rodzaju błędów sensowniej jest używać klas bazowych lub niestety napisać własne (najlepiej dziedziczące z nich).
Na platformie .NET i w Javie jest z tym dużo lepiej – tam typów wyjątków jest niemal za dużo. Nie ma tu miejsca na omawianie ich wszystkich i nie jest to zresztą konieczne, gdyż nazwy klas są zwykle wystarczające do zorientowania się, z jakim rodzajem błędu mamy tu do czynienia. Według mnie najczęstszymi sytuacjami, w których można wyrzucić obiekt którejś z natywnych klas, są:
System.ArgumentException
/ java.lang.IllegalArgumentException
null
-em w sytuacji, gdy wymagany jest obiekt – System.ArgumentNullException
System.IndexOutOfRangeException
/ java.lang.IndexOutOfBoundsException
System.InvalidOperationException
/ java.lang.IllegalStateException
System.NotImplementedException
System.NotSupportedException
/ java.lang.UnsupportedOperationException
System.IO.IOException
/ java.io.IOException
System.TimeoutException
/ java.util.concurrent.TimeoutException
Można zauważyć, że dla większości pozycji odpowiadające klasy wyjątków istnieją na obu platformach i nawet nazywają się podobnie. Zapewne to kolejny znak wzajemnej inspiracji ich twórców ;)
Przeglądając dokumentację do DirectX (a przynajmniej do części graficznej) można natknąć się na wiele typów wyliczeniowych. Większość z nich (a może wszystkie?) na końcu swojej definicji ma stałą o nazwie kończącej się na _FORCE_DWORD
. Przykładem jest znany, lubiany i przez wszystkich używany D3DRENDERSTATETYPE
:
typedef enum D3DRENDERSTATETYPE
{
// (co najmniej 1<<8 różnych stałych)
D3DRS_FORCE_WORD = 0x7fffffff;
} D3DRENDERSTATETYPE, *LPD3DRENDERSTATETYPE;[/cpp]
Zjechanie na sam dół pomocy pouczy nas, że stała ta zasadniczo... nie jest używana. Jednocześnie jednak ma ona wymuszać kompilację typu wyliczeniowego jako 32-bitowego. O co tutaj właściwie chodzi?
Kompilatory C++ mogą mianowicie wybierać dla typów wyliczeniowych właściwie dowolne typy liczbowe - byle tylko wszystkie wartości stałych się zmieściły. To sprawia, że wielkość enum
a może się różnić nie tylko między kompilatorami, ale i między różnymi ustawieniami kompilacji. Nietrudno na przykład wyobrazić sobie, że przy optymalizacji szybkości ów enum
będzie miał rozmiar równy słowu maszynowemu, zaś przy optymalizacji zajętości pamięci będzie to rozmiar najmniejszy możliwy.
No i tu zaczynają się schody tudzież pagórki. Zmienność (a raczej niezdefiniowanie) wielkości typu wyliczeniowego jest może kłopotliwa w pewnych sytuacjach. Trudno byłoby na przykład przewidzieć to, w jaki sposób należy odczytać wartość zwróconą przez metodę GetRenderState
urządzenia, która jest zapisana w 32-bitowym DWORD
-zie, jeśli nie zajmowałaby ona w nim wszystkich 4 bajtów. Podejrzewam też, że na którymś etapie renderowania we wnętrzu DirectX określony rozmiar pewnych flag (np. typów prymitywów) jest po prostu wymuszany przez sterownik karty graficznej. Całkiem rozsądne jest więc zapewnienie go od samego początku – czyli już w kodzie pisanym przez programistę-użytkownika DirectX.
Czemu jednak potrzebny jest takich hack? Ano tutaj znowu wychodzi niedookreślenie pewnych rzeczy w standardzie C++, zapewne z powodu źle pojętej przenośności. Częściowo zostanie to naprawione w przyszłej wersji standardu, gdzie – podobnie jak np. w C# – możliwe będzie określenie typów liczbowych używanych wewnętrznie przez enum
y.
Dzisiaj chcę napisać o ważnej kwestii okołokoderskiej, przed którą chyba każdy prędzej czy później stanął. Pisząc kod, nie zawsze potrzebujemy wielkiego skupienia i absolutnej ciszy; przeciwnie, wiele osób lubi programować przy dźwiękach muzyki. Zazwyczaj jest to jakaś własna kolekcja utworów, zorganizowana w playlisty.
Ma to swoje wady. Jedną z nich jest to, że wymaga sporo manualnej obsługi; jeśli jej zabraknie, to zawartość list(y) może nam się znudzić nawet w przypadku losowej kolejności odtwarzania (no, chyba że nasza kolekcja jest wystarczająco gigantyczna ;P). Poza tym w ten sposób nie usłyszymy nigdy niczego nowego, a być może wartego usłyszenia.
Z drugiej strony włączenie radia zwykle nie jest sensowną opcją – nawet jeśli chodzi o radia internetowe. Nie da się bowiem znaleźć takiego, które idealnie w wpasuje się w nasz muzyczny gust. A gdyby tak można było stworzyć własne?… Eh, pomarzyć dobra rzecz :)
Okazuje się jednak, że to zupełnie wykonalne. Istnieją serwisy, które właśnie na coś takiego pozwalają. Jednym z najstarszych i najbardziej znanych jest Last.fm, zawierający chyba wszystkie feature‘y, jakich można oczekiwać (łącznie ze stacjonarnym klientem), Niestety, jest on płatny.
Osobiście polecam darmowe Jango. Potrafi on dokładnie tyle, ile potrzeba – tj. tworzy wirtualne stacje radiowe (w dowolnej ilości, o ile mi wiadomo) grające muzykę wybranych wykonawców (z możliwością określenia preferencji do pojedynczych utworów) oraz artystów zbliżonych gatunkowo. To wyszukiwanie podobieństw działa przy tym całkiem dobrze, więc mamy spore szanse na odkrycie ciekawych, nieznanych wcześniej wykonawców. Wady?… Jak to bywa w przypadku takich serwisów, może to być niewystarczająco bogata biblioteka zarówno utworów, jak i artystów. Brakuje też desktopowego klienta, co może niektórym przeszkadzać; można wówczas spróbować zewnętrznego programu Jango Desktop.