Archive for Programming

Co może udawać obiekt w C++

2010-05-06 14:56

Możliwości przeciążania operatorów w C++ dla własnych typów obiektów sprawiają, że mogą one (tj. te obiekty) zachowywać się w bardzo różny sposób. Mogą na przykład “udawać” pewne wbudowane konstrukcje językowe, nierzadko wykonując ich zadania lepiej i wygodniej. Przykładów na to można podać co najmniej kilka – oto one:

  • Obiekt może zachowywać się jak funkcja, czyli udostępniać możliwość “wywołania” siebie z określonymi parametrami. Takie twory często nazywa się funktorami i bywają używane podczas pracy ze standardową biblioteką STL.
    Działają one przy tym w bardzo prosty sposób, zwyczajnie przeciążając operator nawiasów okrągłych (). Jest on na tyle elastyczny, że może przyjmować dowolne parametry i zwracać dowolne rezultaty, co pozwala nawet na stworzenie więcej niż jednego sposobu “wywoływania” danego obiektu. Jednym z bardziej interesujących zastosowań dla tego operatora jest implementacja w C++ brakującego mechanizmu delegatów, czyli wskaźników na metody obiektów.
  • Na podobnej zasadzie obiekt może udawać tablicę – wie o tym każdy, kto choć raz używał klas STL typu vector. Wymagane jest tu przeciążenie operatora indeksowania []. Daje ono wtedy dostęp do elementów obiektu-pojemnika, który zresztą nie musi być wcale indeksowany liczbami, jak w przypadku tablic wbudowanych (dowodem jest choćby kontener map). Ograniczeniem jest jedynie to, że indeks może być (naraz) tylko jeden, bo chociaż konstrukcja typu:
    1. v[3,4] = 5;

    jest składniowo najzupełniej poprawna, to działa zupełnie inaczej niż można by było się spodziewać :)

  • Obiekty mogą też przypominać wskaźniki, które wtedy określa się mianem ‘sprytnych’ (smart pointers). Dzieje się tak głównie za sprawą przeciążenia operatorów * (w wersji jednoargumentowej) i ->. Normalnie te operatory nie mają zastosowania wobec obiektów, ale można nadać im znaczenie. Wtedy też mamy obiekt, który zachowuje się jak wskaźnik, czego przykładem jest choćby auto_ptr z STL-a czy shared_ptr z Boosta.
  • Wreszcie, obiekt może też działać jako flaga boolowska i być używany jako część warunków ifów i pętli. Sprytne wskaźniki zwykle to robią, a innymi wartymi wspomnienia obiektami, które też takie zachowanie wykazują, są strumienie z biblioteki standardowej. Jest to spowodowane przeciążeniem operatora logicznej negacji ! oraz konwersji na bool lub void*.

Reasumując, w C++ dzięki przeciążaniu operatorów możemy nie tylko implementować takie “oczywiste” typy danych jak struktury matematyczne (wektory, macierze, itp.), ale też tworzyć własne, nowe (i lepsze) wersje konstrukcji obecnych w języku. Szkoda tylko, że często jest to wręcz niezbędne, by w sensowny sposób w nim programować ;)

Argumenty w linii komend

2010-05-03 13:29

Parametry przekazywane programom podczas uruchomienia mają najczęściej formę kilku przełączników (switches), po których opcjonalnie występują właściwe dla nich argumenty. Zwyczajowo rzeczone przełączniki są poprzedzone myślnikiem (styl uniksowy) lub slashem (styl MS-DOS), jak w przykładzie poniżej:

gcc -c main.c -o main.o -pedantic

Tak skonstruowany wiersz poleceń jest przekazywany przez runtime do aplikacji C/C++ jako tablica ciągów znaków, powstałych już po podzieleniu go na części według spacji i ew. cudzysłowów. Tradycyjnie pierwszy parametr (argc) funkcji main określa liczbę tych części, zaś drugi (argv) jest wspomnianą tablicą je zawierającą. Warto pamiętać, że – jeśli interesują nas tylko argumenty wywołania – jest to tablica indeksowana od jedynki, gdyż argv[0] zawiera zawsze samo polecenie uruchamiające program (w powyższym przykładzie byłby to ciąg "gcc"). Ostatnim elementem jest z kolei argv[argc - 1], zatem w sumie tablica zawiera argc - 1 znaczących parametrów programu. Wreszcie, gwarantowane jest, iż argv[argc] jest zawsze pustym wskaźnikiem, jeśli do czegokolwiek mogłoby się to przydać.

Bezpośrednia interpretacja wiersza poleceń, który używa wspomnianych na początku przełączników, może być jednak nieco kłopotliwa. Wydaje mi się, że lepiej jest przetworzyć ją do bardziej znośnej postaci słownika (mapy), pozwalającego odwoływać się do switchy i ich argumentów po nazwie. Można to zrobić choćby w poniższy sposób:
#include
#include
#include
typedef std::map > CommandLine;

const char SWITCH_CHAR = ‘-‘; // lub ‘/’
const CommandLine ParseCommandLine(int argc, const char* argv[])
{
CommandLine cl;
for (int i = 1; i < argc; ) if (*(argv[i]) == SWITCH_CHAR) { std::vector p;
p.reserve (argc – i);

int j;
for (j = i + 1;
j < argc && (*(argv[j]) != SWITCH_CHAR || strstr(argv[j], " ")); ++j) p.push_back (argv[j]); cl.insert (std::make_pair(argv[i] + 1, p)); i = j; } else ++i; return cl; }[/cpp] W rezultacie otrzymamy mapę, w której wyszukiwanie (std::map::find) pozwoli nam określić, czy dany przełącznik był podany, zaś indeksowanie (operator []) da nam dostęp do jego parametrów w postaci wektora.

Tags: , ,
Author: Xion, posted under Programming » 6 comments

Naprawianie strumienia wejścia

2010-05-01 12:46

Kiedy czytamy dane przy pomocy strumienia wejściowego w C++ (basic_istream), wszystko działa pięknie do momentu, gdy zgadzają się one z tym, czego oczekujemy. Ale w rzeczywistych, a nie hello-worldowych programach nie możemy oczekiwać, że np. poniższy kod:

  1. int n;
  2. cin >> n;

w jakiś magiczny sposób zmusi użytkownika, by wpisał liczbę. To samo dotyczy odczytu z plików. Program musi więc być odporny na nieprawidłowe dane.

Łatwo jest na szczęście ocenić, czy takie dane otrzymaliśmy – wystarczy sprawdzić flagi bitowe strumienia, co w najprostszej wersji wygląda po prostu tak:

  1. if (cin) { /* ok */ }

Równie łatwo jest przywrócić je do stanu używalności (metodą clear) i tym samym dać ponownie możliwość odczytu ze strumienia. Wtedy jednak okaże się, że tym, co chcemy odczytać, nie są żadne nowe dane, lecz dokładnie te same, które spowodowały oryginalny błąd.

Ma to sporo sensu – dzięki takiemu zachowaniu może podjąć próbę ich reinterpretacji jako innego typu danych. Zależy to oczywiście od logiki i struktury wejścia, które czytamy. Jeśli jednak rzeczone dane już nas nie interesują i chcielibyśmy raczej powtórzyć próbę odczytania tego samego, musimy się ich jakoś pozbyć.
Da się to zrobić całkiem prosto. Każdy strumień wejścia utrzymuje bowiem bufor odczytu (read buffer), do którego najpierw trafiają znaki z wejścia. Jeżeli okaże się, że ich format nie zgadza się z tym żądanym przez polecenie odczytu, to ów bufor nie jest opróżniany – stąd wynika opisane wyżej zachowanie strumienia. Żeby więc zacząć znowu czytać bezpośrednio z wejścia, bufor ten należy opróżnić. Mamy na szczęście do niego dostęp (metoda rdbuf zwraca na niego wskaźnik), zatem da się to zrobić – w nieco oldschoolowy sposób:

  1. template <typename T> void FixStream(basic_istream<T>& is)
  2. {
  3.     if (!is)
  4.     {
  5.         const int BUF_SIZE = 32;
  6.         T buf[BUF_SIZE];
  7.  
  8.         is.clear();
  9.         basic_streambuf<T>* isBuf = is.rdbuf();
  10.         int toRead = isBuf->in_avail();
  11.         while (toRead > 0)
  12.         {
  13.             int c = toRead > BUF_SIZE ? BUF_SIZE : toRead;
  14.             isBuf->sgetn(buf, c);
  15.             toRead -= c;
  16.         }
  17.     }
  18. }

W skrócie: czytamy z niego po kawałku znaki, aż w końcu nie będzie już niczego… do odczytania :) Pusty bufor sprawia wtedy, że kolejne operacje odczytu ze strumienia będą pobierały dane już bezpośrednio z wejścia.

Tags: ,
Author: Xion, posted under Programming » 2 comments

O inicjalizacji zmiennych globalnych

2010-04-28 19:15

Wśród dziwnych błędów wykonania, jakie mogą przytrafić się źle napisanym programom w C++, poczesne miejsce zajmuje przypadek, gdy obiekt zadeklarowany po prostu jako zmienna:

  1. Foo foo;

w rzeczywistości nie istnieje (jeszcze). Dokładniej mówiąc: pamięć na zmienną foo, jest jak najbardziej zaalokowana, ale konstruktor klasy Foo nie został jeszcze wywołany.
Czy taka sytuacja jest w ogóle możliwa? Odpowiedź brzmi: jak najbardziej, jeśli rzeczona zmienna jest globalna lub statyczna w klasie (static) i odwołujemy się do niej podczas konstrukcji innej takowej zmiennej.

Uważa się to za bardzo złą praktykę z prostego powodu: kolejność inicjalizacji zmiennych globalnych/statycznych umieszczonych w różnych jednostkach translacji (czyli plikach .cpp) jest niezdefiniowana. Ci, którzy znają trochę terminologię używaną w standardach C/C++ wiedzą, iż znaczy to tyle, że porządek tej inicjalizacji może się zmieniać w zależności od pory dnia, fazy księżyca i poziomu opadów w dorzeczu Amazonki – czyli w praktyce od platformy, wersji kompilatora czy nawet specyficznych jego ustawień (np. optymalizacji). Nie można więc na niej polegać, bo jeśli nawet “teraz u mnie działa”, to wcale nie oznacza, że za jakiś czas nadal będzie.
Niestety, napisanie kodu zależnego od porządku konstrukcji zmiennych globalnych jest prostsze niż się wydaje. Wystarczy chociażby wyobrazić sobie grę złożoną z kilku podsystemów (dźwięku, grafiki, fizyki, UI, itp.), z których każdy wystawia globalny obiekt będący jego interfejsem. Jeśli rozpoczęcie pracy któregoś z tych podsystemów wymaga innego, już gotowego do pracy (np. UI korzystające z części graficznej), to wówczas pojawią się opisywane wyżej zależności między inicjalizacją obiektów globalnych. A wtedy mamy klops :)

Z problemem, jak to zwykle bywa, możemy poradzić sobie dwojako. Można go obejść, stosując funkcję dostępową do obiektu i zmienną statyczną wewnątrz tej funkcji:

  1. GfxSystem& Gfx() { static Gfx gfx; return gfx; }

Mamy wówczas pewność, że zwróci ona zawsze odwołanie do już skonstruowanego obiektu. Jest to możliwe dzięki własnościom zmiennych statycznych w funkcjach, które sprawiają, że obiekt ten zostanie po prostu utworzony przy pierwszym użyciu (wywołaniu funkcji).
Nazywam ten sposób obejściem, bo w najlepszym razie jest on nieelegancki, a w najgorszym może powodować problemy na drugim końcu życia obiektów, czyli podczas ich destrukcji. Można rzecz rozwiązać lepiej i w sposób bardziej oczywisty, chociaż mniej “efektowny” niż automatyczna konstrukcja obiektów przy pierwszym użyciu.

Mam tu na myśli stare, dobre inicjalizowanie jawne na początku działania programu:

  1. g_Gfx = new GfxSystem();
  2. g_Sfx = new SfxSystem();
  3. g_UI = new UiSystem(g_Gfx); // tu *g_Gfx na pewno istnieje

i równie wyraźne zwalnianie wszystkiego na końcu. Może to wszystko być w samej funkcji main/WinMain, może być w wydzielonym do tego miejscu – te szczegóły nie są aż takie ważne. Grunt, że w ten sposób żadna nietypowa (i niezdefiniowana) sytuacja nie powinna się już nam przytrafić.

Tags: , , ,
Author: Xion, posted under Programming » 9 comments

C++ i tablice asocjacyjne

2010-04-23 17:34

Zwykłe tablice (jednowymiarowe) w “normalnych” językach programowania to po prostu ciągłe obszary pamięci, do których pierwszego elementu posiadamy odwołanie. Dostęp do kolejnych polega na korzystaniu z arytmetyki wskaźników, więc niedziwne jest, że takie tablice indeksuje się wyłącznie liczbami – i to z określonego zakresu.
Ci, którzy programowali w na przykład w Pythonie lub PHP znają jednak koncepcję tablic asocjacyjnych, dla których to ograniczenie nie obowiązuje. W C++ do ich symulowania używa się często map z STL-a:

  1. std::map<std::string, Foo*> m;
  2. // ...
  3. m["one"]->DoSomething();

Jest to oczywiście odpowiednie przeciążenie operatora [], a map jest rodzajem pojemnika. Skoro jednak ma to udawać tablicę, to dobrze by było, żeby narzut na obsługę dostępu do elementów nie różnił się zbytnio od tego z prawdziwych tablic. A ten, jak wiemy, jest stały.

Jak to osiągnąć?… Przede wszystkim powinniśmy – jeśli tylko możemy – nie korzystać z kontenera map. Problem z nim polega na tym, że poświęca on dużo uwagi sortowaniu elementów według ich kluczy (“indeksów”). Gdy klucze te są złożone – bo są nimi np. łańcuchy znaków – może to zająć trochę czasu i jednocześnie nie dawać nam żadnej korzyści. Dla tablicy asocjacyjnej najważniejsze są bowiem operacje wyszukiwania wartości (“indeksowania”) i dodawania nowych elementów, względnie ich usuwania. To one muszą działać szybko; wewnętrzny porządek elementów nie jest dla nas istotny.
Dlatego też lepsza jest mapa korzystająca z dobrodziejstw mechanizmu haszowania. W Visual C++ (podobnie zresztą jak w GCC) takie cudo dostępne jest od dawna jako klasa hash_map. Długo było ono niestandardowym rozszerzeniem biblioteki STL, ale wraz z nową wersją standardu staje się ono jego częścią. Poza tym “od zawsze” istnieje też rozwiązanie z Boosta w postaci klasy unordered_map. Przyjemną cechą wszystkich tych implementacji jest niemal jednolity interfejs, tożsamy ze standardową klasą map.

Oprócz używania właściwego pojemnika powinniśmy też wiedzieć, jak go z niego korzystać – a dokładniej mówiąc, czego unikać. Felerną operacją jest próba dodania elementu poprzez zwykłe indeksowanie z przypisaniem:

  1. m["two"] = new Foo();
  2. // klucz "two" nie występuje wcześniej w m

Skutkuje to stworzeniem na chwilę obiektu domyślnego dla danego typu wartości, a potem jego zastąpieniem (przypisaniem) tym właściwym, który ma się w ‘tablicy’ znaleźć. W przypadkach, gdy konstruktor i operator przypisania nie są trywialne, będzie to strata na wydajności. Powinniśmy więc używać raczej metody insert do wstawiania. Może jest to niezupełnie “tablicowe”, ale cóż – albo rybka, albo akwarium ;P

Tags: , , , ,
Author: Xion, posted under Programming » 1 comment

Jak się robi screeny

2010-04-20 16:19

W robieniu zrzutów ekranowych (screenshotów) nie ma, wydawałoby się, nic nadzwyczajnego. Wciskamy po prostu klawisz Print Screen i obraz ekranu ląduje nam w systemowym Schowku, po czym możemy go użyć w dowolny sposób. Przynajmniej tego właśnie się spodziewamy zazwyczaj.
W rzeczywistości robienie screenów to dość śliski temat, jeśli mamy do czynienia z czymś więcej niż zwykłymi aplikacjami okienkowymi. Dotyczy to chociażby pełnoekranowych gier, korzystających z bibliotek graficznych typu DirectX i OpenGL. Wykorzystują one mechanizm znany jako hardware overlay, pozwalający – w skrócie – na ominięcie narzutu systemu operacyjnego związanego z zarządzeniem oknami wielu aplikacji na współdzielonym ekranie monitora. Skutkiem ubocznym jest między innymi to, że wbudowany w system mechanizm tworzenia zrzutów ekranu staje się w tej sytuacji bezużyteczny, jeśli chcemy wykonać screenshota tego rodzaju aplikacji.

Skąd w takim razie biorą się screeny, a nawet całe filmy ze współczesnych gier?… Cóż, istnieją oczywiście metody pozwalające na przechwytywanie obrazów generowanych przez aplikację – można bowiem skłonić ją do wywoływania naszej własnej wersji metody IDirect3DDeviceN::Present zamiast tej wbudowanej :) Brzmi to pewnie tajemniczo i nieco hakersko, ale tak mniej więcej działają programy do przechwytywania wideo typu Fraps.

Do zwykłych screenów zazwyczaj jednak przewiduje się odpowiednie rozwiązania w samych grach. Jednym ze sposobów jest, w przypadku wykrycia wciśnięcia klawisza odpowiadającego za screenshoty (czyli zwykle Print Screen), wyrenderowanie sceny najpierw do tekstury, a ją potem na ekran. Rzeczoną teksturę można później zapisać do pliku.


Aliasing (i nie tylko)
w pełnej krasie

Czy to dobre rozwiązanie? Otóż nie, i to aż z trzech powodów. Stosunkowo najmniej ważnym jest to, iż implementacja może być kłopotliwa, bo wymaga dobrania się do początku i do końca potoku renderingu w aplikacji z kolejnym render targetem. Bardziej istotny jest brak multisamplingu (a więc wygładzania krawędzi wielokątów znanego jako anti-aliasing) w powstałym obrazku. Nie będzie on więc wyglądać zbyt ładnie, już nie wspominając o tym, że nie będzie on odpowiadał temu, co widzimy na ekranie.

Jak więc należy robić screeny? Dokładnie tak, jak nam podpowiada intuicja: należy wziąć to, co widać na ekranie – czyli zawartość bufora przedniego (front buffer) i zapisać to do pliku. W DirectX mamy do tego metodę urządzenia o nazwie GetFrontBufferData, która pobiera nam to jako powierzchnię:

  1. IDirect3DSurface9* screen;
  2. dev->CreateOffscreenPlaneSurface(Width, Height, D3DFMT_A8R8G8B8,
  3.     D3DPOOL_SCRATCH, &screen, NULL);
  4. // screenshot i zapis go do pliku
  5. dev->GetFrontBufferData(0, &screen);
  6. D3DXSaveSurfaceToFile(ScreenFile, D3DXIFF_JPG, screen, NULL, NULL);

Ważne jest, by pamiętać o właściwych rozmiarach powierzchni – takich, jakie ma bufor przedni. W trybie pełnoekranowym odpowiada to buforowi tylnemu, ale dla aplikacji okienkowej wcale nie musi, jeśli rozmiar jej okna możemy zmieniać.

W OpenGL podobną rolę spełnia do GetFrontBufferData spełnia funkcja glReadPixels. Z zapisaniem pobranego obrazu może być tylko “nieco” trudniej ;-)

Cokolwiek, czyli Boost.Any

2010-04-17 11:52

Zasadniczo w C++ zmienne mają jednoznacznie określone typy, znane w czasie kompilacji. Istnieją oczywiście pewne mechanizmy, które zdają się tę zasadę lekko naciągać (dziedziczenie i polimorfizm, szablony), ale bez znaczącej nieścisłości można przyjąć, że jest ona prawdziwa. W języku kompilowanym nie jest to zresztą nic nadzwyczajnego.
Z kolei w językach skryptowych i interpretowanych dość powszechne jest używanie zmiennych bez określonego (tj. jawnie podanego w deklaracji) typu. To, co obecnie przechowuje liczbę, za chwilę może zawierać ciąg znaków czy odwołanie do obiektu i nie ma z tym specjalnego problemu. Typy w takich językach oczywiście istnieją, ale mają je wartości, nie zaś zmienne.

Czasami coś podobnego – czyli zmienna mogąca przechowywać wartości różnego rodzaju – przydaje się i w C++. Wtedy niektórzy przypominają sobie o void*, ale “ogólny wskaźnik na coś” rzadko jest w tym przypadku szczytem marzeń. Jego podstawową wadą jest to, że wymaga on przechowania gdzieś poza nim informacji o docelowym typie wartości, na którą pokazuje – jeśli chcemy ją później wykorzystać, rzecz jasna. Równie poważną jest fakt, że mamy tu do czynienia z wskaźnikiem; pamięć, na którą on pokazuje, musi być kiedyś zwolniona.
Dlatego też lepszym rozwiązaniem jest biblioteka Any z Boosta, umożliwiająca korzystanie ze zmiennych “typu dowolnego”. Reprezentuje go klasa boost::any, mogąca przechowywać wartości prawie dowolnego rodzaju (wymaganiem jest głównie to, by ich typy posiadały zwykły konstruktor kopiujący):

  1. #include <boost/any.hpp>
  2. boost::any any1 = 5, any2 = std::string("Ala ma kota");
  3. boost::any foo = Foo(); // o ile Foo da się kopiować

Ponieważ jednak jest to wciąż C++, a nie język skryptowy, do wartości zapisanych w obiekcie any bezpośrednio dostać się nie da. W szczególności nie można liczyć na niejawne konwersje powyższych zmiennych do typów int, string czy Foo. Zamiast tego korzysta się ze specjalnego operatora rzutowania any_cast, który pozwala na potraktowanie wartości zapisanej w any zgodnie z jej właściwym typem:

  1. int x = 1 + boost::any_cast<int>(any1);

W przeciwieństwie do void*, próba jej reinterpretacji na inny typ danych skończy się wyjątkiem. Nie trzeba jednak polegać na jego łapaniu, jeśli docelowego typu nie jesteśmy pewni: boost::any pozwala też pobrać jego type_info (tj. to, co zwraca operator typeid) bez dokonywania rzutowania.

I to w sumie wszystko jeśli chodzi o tę klasę, a jednocześnie i całą bibliotekę. Mimo swej niewątpliwej prostoty jest ona bardzo przydatna tam, gdzie potrzebujemy zmiennych przechowujących różne rodzaje wartości. Warto więc się z nią zapoznać :)

Tags: , , ,
Author: Xion, posted under Programming » 9 comments
 


© 2023 Karol Kuczmarski "Xion". Layout by Urszulka. Powered by WordPress with QuickLaTeX.com.