Posts tagged ‘C/C++’

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

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

Sklejanie wierszy kodu w C++

2010-04-06 22:24

Języki programowania o składni podobnej do C nie narzucają specjalnych ograniczeń na to, jak pisany w nich kod formatujemy wizualnie. Możemy więc robić dowolne wcięcia, a także w dowolny sposób dzielić poszczególne instrukcje między wiersze w pliku źródłowym. Jest tak dlatego, że to inne znaki – jak średniki czy nawiasy klamrowe – nadają listingowi strukturę odczytywaną przez kompilator.

W C/C++ są jednak przynajmniej dwie sytuacje, w których “fizyczne” wiersze w pliku z kodem mają znaczenie. Przekonał się o tym każdy, kto próbował zdefiniować (dyrektywą #define) makro rozciągające się na kilka linijek. Ściśle rzecz ujmując, w ogóle nie da się tego zrobić – dyrektywy preprocesora z założenia zajmują zawsze dokładnie jeden wiersz. Można jednak sprawić, by ów wiersz… rozciągał się na kilka linijek:

  1. #define FUNCTOR(name,params,code) class name { \
  2.     public: \
  3.         void operator()(params) \
  4.             code \
  5. };
  6. // disclaimer: nie należy zakładać, że powyższe makro
  7. // ma jakikolwiek praktyczny sens ;)

Znak \ (backslash) robi właśnie ten rodzaj magii. “Skleja” on mianowicie dwie linijki kodu tak, że dla kompilatora stają się one jedną. Używając go, możemy więc rozbić makro preprocesora na wiele wierszy.

Druga sytuacja, w której tak stosowany backslash jest przydatny, to długie ciągi znaków pisane w kodzie:
std::cout << "To jest bardzo długi komunikat, który w konsoli pojawi się \ w pojedynczym wierszu mimo tego, że tutaj w pliku źródłowym jest on podzielony \ na kilka następujących po sobie linijek." << std::endl;[/cpp] Potrafi on bowiem także "oszukać cudzysłów". Jednak identyczny efekt można też osiągnąć, jeśli wiemy, że kompilator sam skleja następujące po sobie napisy. “a” “b” daje na przykład to samo "ab". Dlatego też backslash w wielolinijkowych napisach jest stosunkowo rzadko używany.

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

Domyślna inicjalizacja zmiennych

2010-03-17 1:57

Używanie zmiennych bez uprzedniego nadania im wartości to stosunkowo częsty błąd. Wiele języków kompilowanych będzie z tego powodu stroiło fochy wahające się od ostrzeżeń podczas kompilacji przez błędy tejże aż po wyjątki w czasie wykonania programu (przynajmniej w wersji debugowej). To zresztą bardzo dobrze, gdyż alternatywą jest korzystanie ze zmiennej, której zawartością są jakieś losowe śmieci w pamięci.

Tak więc zmienne trzeba inicjalizować i w niemal każdym języku da się to zrobić mniej więcej tak:

  1. int x = 5;

W C++ oczywiście też. Jednak w C++ mamy też feature, który nazywa się inicjalizacją domyślną. Opiera się on założeniu, że dany typ (tj. obiekt tego typu) może się sam zainicjalizować bez żadnych dodatkowych informacji – czyli że np. ma rozsądną wartość domyślną albo bezparametrowy konstruktor. Stąd T() będzie “domyślnym obiektem typu T“, który możemy łatwo uzyskać, nie mając w ogóle pojęcia, czym ów typ T w istocie jest (codzienność szablonów, swoją drogą).
Inicjalizacja zmiennej tego typu domyślną wartością będzie więc wyglądała mniej więcej tak:

  1. T foo = T();

Co bardziej spostrzegawczy zauważą jednak, że mamy tutaj dwie operacje: konstrukcję obiektu i kopiowanie go. Nie radzę jednak próby “optymalizacji” w postaci zamiany na deklarację T foo(); – można się nielicho zaskoczyć. Najlepiej pozostawić to zadanie kompilatorowi; przynajmniej w teorii powinien on sobie z nim bez problemu poradzić.

W powyższy sposób możemy domyślnie inicjalizować dowolne typy zmiennych. Dla pewnej ich podgrupy – zwanej agregatami – możemy jednak zastosować inne rozwiązanie. Cóż to jednak są te agregaty? Otóż są to typy złożone (tablice, struktury, itp.), które nie posiadają zdefiniowanych przez programistę konstruktorów. Mimo że instrukcja T() działa zupełnie dobrze również dla nich, dopuszczalne jest stosowanie nieco innej formy inicjalizacji.
Formą ta jest lista inicjalizacyjna. Można ją niekiedy spotkać w kodzie korzystającym z jakiegoś API, które operuje na dużych strukturach:

  1. WNDCLASSEX wc = { sizeof(WNDCLASSEX) }; // {...} to inicjalizator
  2. wc.lpszClassName = TEXT("MyVeryOwnAndCuteWindow");
  3. wc.hInstance = hInstance;
  4. // itd.

Chodzi tu po prostu o podanie wartości dla kolejnych elementów agregatu, czyli pól w strukturze/klasie lub komórek tablicy; całą tę listę zamykamy w nawiasy klamrowe. Nie musi ona być przy tym pełna: jeśli jakieś pozycje zostaną pominięte, to odpowiadające im pola/komórki zostaną automatycznie zainicjalizowane w sposób domyślny. W przykładzie powyżej inicjalizujemy więc strukturę tak, że pierwsze pole zawiera jej rozmiar, a reszta domyślne wartości (czyli pewnie zera).
Dane dla pierwszego pola zostały wprawdzie tutaj podane jawnie, jednak w ogólności nie jest to wymogiem. Na liście inicjalizacyjnej możemy równie dobrze opuścić wszystkie pozycje i to właśnie jest ten drugi, uniwersalny sposób inicjalizacji agregatu. Wygląda on więc po prostu tak:

  1. D3DVECTOR v = {};

Jakkolwiek nietypowo (jak na kod C++) linijka ta wygląda, jest ona najzupełniej poprawna. W wyniku jej wykonania nowo zadeklarowany wektor v będzie miał wszystkie współrzędne wyzerowane.

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

Pakowanie w przestrzenie

2010-03-09 17:05

Jeśli w C++ piszemy jakąś bibliotekę (albo nawet ogólniej: kod wielorazowego użytku), to zgodnie z dobrymi praktykami powinniśmy jej symbole zamknąć przynajmniej w jedną osobną przestrzeń nazw (namespace). Dzięki temu zapobiegniemy zminimalizujemy możliwość kolizji identyfikatorów w kodzie, który z naszego dzieła będzie korzystał – a także potencjalnie z innych bibliotek.
Nie wszystkie z nich jednak mogą być tak ładnie napisane – choćby dlatego, że któraś może być przeznaczona oryginalnie dla języka C. Najbardziej typowy przykład? Windows API. Dołączenie windows.h zasypuje globalną przestrzeń nazw istną lawiną symboli odpowiadających tysiącom funkcji czy typów zadeklarowanych w tym nagłówku. Nie jest to specjalnie dobre.

Jak temu zaradzić? Bardzo prostą, ale nierozwiązującą wszystkich problemów metodą jest stworzenie własnego nagłówka “opakowującego” ten biblioteczny w nową przestrzeń nazw:

  1. #ifndef FOO_H
  2. #define FOO_H
  3.  
  4. namespace foo
  5. {
  6.     #include <foobar.h>
  7. }
  8.  
  9. #endif

Założenie jest takie, żeby wynikowego pliku nagłówkowego (foo.h) używać następnie w miejsce oryginalnego (foobar.h). Wtedy wszystkie symbole w nim zadeklarowane znajdą się wewnątrz nowej przestrzeni nazw, foo.

Wszystkie?… Nie! Pakując kod napisany w stylu C bezpośrednio do przestrzeni nazw nie osiągniemy bowiem wszystkich celów, którym namespace‘y przyświecają. Owszem, da się co nieco poprawić: jeśli np. wspomniany windows.h zamknęlibyśmy w przestrzeni win, to poniższy kod będzie jak najbardziej działał:

  1. win::HWND hNotepad;
  2. hNotepad = win::FindWindow("Notepad", 0);

podczas gdy wersja bez przedrostków win:: już niezupełnie. Jednak nie jest to całkowity – nomen omen – win, bo z kolei takie wywołanie:

  1. win::ShowWindow (hNotepad, win::SW_MINIMIZE);

skutkuje już niestety failem :) Nasza przestrzeń nie może bowiem zamknąć wszystkiego, gdyż nie podlegają jej dyrektywy preprocesora – a w szczególności #define. Pech polega na tym, że właśnie #define jest w C podstawowym sposobem definiowania stałych, więc użyta wyżej nazwa SW_MINIMIZE jest (w windows.h) określona po prostu jako:

  1. #define SW_MINIMIZE 6

Próba jej kwalifikowania powoduje zatem powstanie nieprawidłowego ciągu win::6 i słuszne narzekania kompilatora.

Nasz pojemnik (na nazwy) jest więc dziurawy i niestety nic z tym nie da się zrobić. Tak to już jest, gdy wciąż trzeba mieć do czynienia z API, które w tym przypadku liczy sobie – bagatelka – ponad 20 lat!

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


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