Posts from 2008

Artykuł o odśmiecaniu pamięci w C++

2008-05-23 13:52

W przypływie weny twórczej zrealizowałem pomysł na artykuł, który dotąd znajdował się na szczycie kolejki priorytetowej takich pomysłów. Opisuje on, jak można uzupełnić język C++ o prosty mechanizm odśmiecacza pamięci (garbage collector – GC), czyli modułu zajmującego się automatycznym zwalnianiem nieużywanych obiektów. Osoby programujące choć przez chwilę w Javie czy .NET wiedzą, że obecność GC jest bardzo wygodna, lecz dopóki nie powstanie standard C++0x, w naszym ulubionym języku programowania mechanizm nie będzie domyślnie obecny.

W artykule opisuję, jak można to naprawić, pisząc własną, prostą implementację odśmiecacza pamięci w C++. Zainteresowanych zapraszam do lektury – mam nadzieję, że warto :)

Tags:
Author: Xion, posted under Website » Comments Off on Artykuł o odśmiecaniu pamięci w C++

Cztery rodzaje operatora new

2008-05-20 21:04

Do alokowania pamięci (albo raczej: tworzenia obiektów na stercie) służy w C++ operator o wiele mówiącej nazwie new. Chociaż jest on powszechnie znany i nieustannie używany przez każdego programistę C++, pewni nie wszyscy wiedzą, że występuje on nie w jednej, ale aż w czterech odmianach, z których każda różni się sposobem wywołania!
Oto krótki przegląd:

  • “Zwykły” operator new (co obejmuje też formę tablicową new[]) jest oczywiście najbardziej znany i najczęściej stosowany. Warto pamiętać, że zazwyczaj robi on dwie rzeczy: oprócz alokacji pamięci wywołuje też konstruktor dla tworzonego obiektu (lub obiektów w tablicy), któremu możemy też podać parametry.
  • Przeciążony operator new zmienia natomiast swoje zachowanie tylko w zakresie tej pierwszej czynności, czyli samej alokacji. Ciekawostką jest to, że możemy wyposażyć go (tj. sam operator new) w dodatkowe parametry – czyli przeciążyć go w pełnym znaczeniu tego słowa! Typowym przykładem, jaki się tutaj zwykle przytacza, jest następująca funkcja:
    1. void* operator new (size_t bytes, char fill)
    2. {
    3.     // alokacja z wypełnieniem podanym wzorcem
    4.     char* p = ::new char[bytes];
    5.     for (size_t i = 0; i < bytes; ++i) p&#91;i] = fill;
    6.     return p;
    7. }&#91;/cpp]
    8. która tworzy przeciążoną wersję operatora <code>new</code>, wypełniającą świeżo zaalokowaną pamięć podanym wzorcem:
    9. [cpp]char* ptr = new('X') char[10];  // alokuje tablicę charów wypełnioną X-ami

    Naturalnie możliwe są bardziej przydatne zastosowania. Jeśli mamy na przykład kilka rozłącznych ze sobą stert, to możemy tak napisać operator new, by poprzez dodatkowy argument pozwalał decydować o tym, którą z nich chcemy w danym przypadku wybrać.

  • new nierzucający wyjątków. Domyślnie alokacja za pomocą new rzuca wyjątek std::badalloc, jeżeli operacja się nie powiodła (zwykle z powodu braku pamięci). To zachowanie – wymagające do poprawnej obsługi bloku try-catch może nam się nie podobać, ale na szczęście można je zmienić. Wystarczy użyć wersji new z dodatkowym parametrem std::nothrow:
    1. int* pEnormousArray = new(std::nothrow) int[0xffffffff]; // "tylko" 4GB :)

    Wymaga to jeszcze dołączenia standardowego pliku nagłówkowego o wielce trafnej nazwie new.

  • Ostatni rodzaj zwie się placement new, co nie ma żadnego specjalnie dobrego tłumaczenia na język polski. Użycie tego operatora wymaga podania wskaźnika na już zaalokowany kawałek pamięci. Działanie operatora new ogranicza się wtedy do skonstruowania obiektu w tym właśnie miejscu, na które pokazuje przekazany wskaźnik. Tak więc w tym przypadku new tak naprawdę niczego nie alokuje; jest to po prostu najzupełniej legalny sposób na wywołanie konstruktora bez robienia czegokolwiek innego. Jakkolwiek może to się wydawać przydatne, zdecydowanie odradzam korzystania z tego mechanizmu w sposób nieprzemyślany, bo można przy tym popełnić “ciekawe” błędy.

Mamy więc aż cztery różne warianty new, ale raczej nie powinno to rodzić dylematów w rodzaju “Który z nich wybrać?”. W praktyce i tak nieczęsto zachodzi potrzeba skorzystania z któregokolwiek poza pierwszym. Co nie znaczy rzecz jasna, że nie warto znać pozostałych – podobnie jak całej masy innych kruczków języka C++ :]

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

O układaniu kontrolek

2008-05-19 20:57

Najprostszym sposobem na rozmieszczanie kontrolek w oknie programu jest oczywiście… podanie ich pozycji i rozmiarów dosłownie. Ta metoda sprawdza się dobrze jednak tylko w prostych przypadkach: gdy mamy stałą (i raczej niewielką) liczbę obiektów, gdy rozmiary okna są stałe, gdy nie dostosowujemy rozmiaru interfejsu do rozdzielczości ekranu, itd. W wielu przypadkach to jednak nie wystarcza i dlatego systemy GUI oferują też inne narzędzia pozycjonowania elementów interfejsu. Niektóre z nich są całkiem pomysłowe. Oto kilka przykładów:

  • Przykład dokowania kontrolek
    Przykład dokowania

    Dokowanie (docking), zwane też wyrównywaniem (align). Polega to na “przyklejeniu” kontrolki do którejś z krawędzi nadrzędnego pojemnika (np. okna) w jednym wymiarze oraz do obu krawędzi w wymiarze prostopadłym. Komponentowi pozostaje wówczas jeden stopień swobody. Przykładowo, kontrolka zadokowana po lewej stronie może się jedynie rozszerzać w prawo (lub zmniejszać w lewo), zaś jej górna i dolna krawędź pokrywają się zawsze z odpowiednimi krawędziami zawierającego ją pojemnika.
    Ten sposób pozycjonowania jest idealny chociażby dla pasków stanu (status bar), które zawsze umiejscowione są w dolnej krawędzi okna.

  • Ustawianie zakotwiczania
    Ustawianie zakotwiczania
    w Visual Studio

    Zakotwiczanie (anchoring) polega z kolei na ustawieniu stałej odległości pewnych krawędzi kontrolki od odpowiednich krawędzi kontrolki nadrzędnej. Domyślnie na przykład każdy komponent jest zakotwiczony z lewej i z góry. Gdy zmieniamy rozmiar zawierającego go okna, dystans między lewą krawędzią tegoż okna i lewą krawędzią kontrolki pozostaje stały; podobnie jest z górnymi brzegami. Zmieniając ustawienia zakotwiczania możemy uzyskać efekt kontrolki “wyrównanej” w podobny sposób jak tekst w edytorze, np. do prawej strony okna zamiast do lewej.
    Anchoring można uznać za nieco lepszą wersję dokowania, ale w istocie różni się on od niego dość znacznie (zwłaszcza jeśli dokujemy wiele kontrolek po jednej stronie). Jest on jednak przydatny wtedy, gdy kontrolka ma w określony sposób zmieniać rozmiar wraz z oknem.

  • Układ typu flow
    Układ typu flow

    Układy (layouts) kontrolek. Są to bardziej wyspecjalizowane narzędzia, których jedynym zadaniem jest układanie elementów interfejsu w określony sposób. Do najbardziej przydatnych należy układ typu flow oraz układ tabelkowy. Ten pierwszy polega na rozmieszczaniu kontrolek po kolei (w ustalonym porządku) tak, jak wypisuje się tekst w kolejnych wierszach. (Jak widać, analogie tekstowe w projektowaniu interfejsu pojawiają się całkiem często ;]). Z kolei układ tabelkowy, jak nazwa wskazuje, dzieli zadany obszar na wiersze i kolumny tak, że w każdej z powstałych w ten sposób komórek mieści się dokładnie jedna kontrolką i wypełnia ją całkowicie.
    Układy mają spore zastosowanie zwłaszcza wtedy, gdy rozmieszczane kontrolki są tworzone dynamicznie w trakcie działania programu. Potrafią wówczas zaoszczędzić dużo pracy z ręcznym przeliczaniem ich pozycji i/lub rozmiarów.

To stwierdzenie odnosi się zresztą do każdego z tych trzech sposobów na określanie wymiarów elementu interfejsu. Obecnie bardzo często okazuje się, że nawet stosunkowo zaawansowany układ kontrolek, który musi dostosowywać się do zmian rozmiaru okna, da się stworzyć bez obsługiwania zdarzeń typu OnResize i ręcznego rozmieszczania komponentów. A to może oszczędzić kilku kłopotów i pozwala łatwiej skoncentrować się na tych częściach programu, których wprawdzie nie widać, ale które są o wiele ważniejsze :)

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

Laboratoria kontra projekty

2008-05-17 15:41

Studia techniczne – a więc także i informatyka – różnią się od innych choćby tym, że tutaj nie obejdzie się bez dużej ilości zadań praktycznych. Brudzenie tablicy pyłem kredowym na wykładach i ćwiczeniach to bowiem za mało – zwłaszcza, jeśli mówimy o uczelniach technicznych, a nie uniwersytetach.
Mamy więc dwa dodatkowe rodzaje zajęć: laboratoryjne i projektowe. Oba mają na celu wykorzystanie zdobytej przez studenta wiedzy w praktyce oraz nabranie pewnej porcji doświadczenia. Cel chwalebny, wykonanie niestety nieco gorsze…

Dotyczy to głównie laboratoriów, które w pewnej formie są według mnie kompletnym nieporozumieniem, jeśli chodzi o większość dziedzin informatyki – zwłaszcza tych związanych bezpośrednio lub pośrednio z programowaniem. Zajęcia takie wyglądają mniej więcej tak: student otrzymuje takie-a-takie zadanie – którym jest zwykle napisanie programu – i do końca zajęć ma się z niego wywiązać, czyli rzeczony program napisać. Potem wyniki jego pracy są oceniane przez prowadzących.
W czym ta formuła jest zła? Ano można jej wytknąć całkiem sporo mankamentów:

  • Nieżyciowość. W pracy nikt przecież nie koduje na akord: wiadomo, że z ośmiogodzinnego dnia programista na efektywną pracę przeznacza najwyżej 4 do 5 godzin. Kogo zresztą byłoby stać na to, by każdy projekt rozparcelować na małe kawałki, które potem byłyby pisane przez koderów w ściśle określonych ramach czasowych? To zapewne całkowicie niemożliwe, a co najmniej wysoce nieopłacalne.
  • Złe nawyki. Jeśli mamy tylko godzinę lub dwie, by oddać działający kawałek kodu, na bardzo daleki plan schodzą takie kwestie jak jego czytelność, odpowiedni stopień nasycenia komentarzami, elastyczność, możliwość ponownego wykorzystania, i tak dalej. Krótko mówiąc, produkuje się wtedy kod “tylko do zapisu”, zgodnie z zasadą 3Z: Zakodź, Zalicz, Zapomnij :) Nabyte przy okazji przyzwyczajenia do programistycznego niechlujstwa mogą się odbijać czkawką jeszcze przez bardzo długi czas.
  • Stres. Takie laboratoria to tak naprawdę nic innego, jak kartkówka wklepywana na klawiaturze. Nie jest to nic przyjemnego i w (zbyt) dużych dawkach prawdopodobnie może nawet skutecznie odstręczyć od kodowania jako takiego.

W przeciwieństwie do laboratoriów, projekty właściwie nie mają wymienionych wyżej wad. Są też na pewno bardziej pouczające, szczególnie wtedy gdy należy je wykonywać w grupach. Zdobyte przy okazji doświadczenie jest z pewnością o wiele cenniejsze, bo obejmuje przecież także organizowanie pracy i planowanie kolejnych jej etapów. Czegoś takiego nie można raczej powiedzieć o zadaniach wykonywanych na szybko na laboratoriach.

Tags: ,
Author: Xion, posted under Studies, Thoughts » 16 comments

Triki z PowerShellem #1 – GUI

2008-05-15 14:41

Windows PowerShell jest oczywiście powłoką tekstową z wierszem poleceń. Wszystkie więc czynności, jakie możemy w nim wykonać, wiążą się z wpisywaniem tychże poleceń i odczytywaniem wyników, podawanych także w postaci tekstowej. Prosto, minimalistycznie i naturalnie.

I właśnie dlatego jednym z pierwszym eksperymentów, jakie przy użyciu tego narzędzia wykonałem, była… próba wyświetlenia okienka z GUI :) Najpierw miało to być skromne okienko komunikatu (message box), jednak szybko okazało, że nie będzie to wcale łatwe. Domyślnie w PowerShellu nie jest bowiem załadowane assembly System.Windows.Forms, które to jest niezbędnie potrzebne do zabawy z okienkami. Nie oznacza to na szczęście, że oprócz jego załadowania należy też korzystać z niewygodnego interfejsu refleksji, aby cokolwiek przy jego użyciu zrobić (np. stworzyć obiekt klasy i wywołać jego metodę). Wystarczy po prostu jedno wywołanie w stylu:

  1. [System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms")
  2. # Od .NET 2.0 ta metoda jest deprecated, ale tutaj nam to chwilowo nie przeszkadza ;-)

Okno komunikatu w PowerShellui od tej pory można już zupełnie normalnie korzystać z Windows.Forms… No, prawie. Możemy w każdym razie uraczyć się okienkiem komunikatu:

  1. [System.Windows.Forms.MessageBox]::Show("Hello world!", "Message",
  2.     [Windows.Forms.MessageBoxButtons]::OK,
  3.     [Windows.Forms.MessageBoxIcon]::Information)

A gdy już na nie popatrzymy, to pewnie zechcemy też czegoś więcej – na przykład “prawdziwego” okna, czyli zwykłej formy. To także jest do zrobienia:

  1. $form = New-Object System.Windows.Forms.Form
  2. $form.Text = "GUI!"
  3. [System.Windows.Forms.Application]::Run($form)

Bez większego problemu można by zresztą dodać do formy kontrolki potomne, jak przyciski czy pola tekstowe… Jest jednak jeden poważny problem: właściwie nie ma sposobu na podpięcie pod nasze kontrolki procedur zdarzeniowych – nawet mimo tego, że przy użyciu wyjątkowo perfidnej sztuczki da się w PowerShellu tworzyć delegaty. Stworzone okienka pozostaną więc na wpółmartwe i specjalną funkcjonalnością nas nie zaskoczą.

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

Przerywanie działania wątku

2008-05-12 21:17

Jednym z powodów używania wątków jest możliwość przerwania wykonywanych przezeń czynności właściwie w dowolnym momencie. Dzięki temu można na przykład wyposażyć aplikację okienkową w magiczny przycisk Anuluj obok paska postępu. Przez to zaś użytkownik ma wrażenie, że – nawet jeśli musi (dłuższą) chwilę zaczekać – nadal jest panem sytuacji :)
Jak można więc przerwać wątek, jeśli zachodzi taka potrzeba? Sposobów jest kilka:

  1. Najelegantszym jest sprawienie, aby zakończył się on “sam z siebie” i poczekanie na niego. Polega to na ustawieniu flagi sygnalizującej wątkowi konieczność zakończenia, którą oczywiście musi on regularnie sprawdzać. Stąd też główne pętle występujące w procedurach wątków są często w stylu while (!Terminating) { /* ... */ }. Dla porządku warto też (a w niektórych środowiskach trzeba), po ustawieniu rzeczonej flagi na wątek zaczekać. Czyni się to zwykle funkcją z join (‘złącz’) w nazwie: Thread.Join/join w .NET/Javie, pthread_join w POSIX-ie i… WaitForSingleObject w Windows API :)
  2. Jeśli wątek sporą część czasu poświęca na czekanie lub chociaż co jakiś czas przechodzi przynajmniej na krótko w stan uśpienia, wówczas można mu przerwać (interrupt). W .NET/Javie robi się to poprzez, niespodzianka, Thread.Interrupt/interrupt. Wówczas przy następnym wejściu do funkcji czekającej w rodzaju Thread.Sleep/sleep rzucany jest wyjątek (Thread)InterruptedException, który odwija stos wątku, kończąc w ten sposób jego działanie. W natywnych platformach nie może być oczywiście dokładnego odpowiednika podobnej operacji, lecz można ją symulować czekaniem na jakimś obiekcie synchronizacyjnym i sygnalizowaniem go, gdy chcemy to czekanie przerwać. W POSIX-ie funkcja pthread_cancel działa aczkolwiek w dość podobny sposób do Interrupt, pozwalając na posprzątanie zasobów w trakcie anulowania wątku.
  3. Najmniej eleganckim sposobem jest wreszcie brutalne przerwanie wątku, czyli próba jego natychmiastowego zakończenia w dowolnym momencie. Zarówno .NET, jak i Java rozwiązały to w podobny sposób: po wywoływaniu odpowiedniej metody (Thread.Abort/stop), w wątku rzucany jest bardzo specjalny wyjątek (ThreadAbortException lub ThreadDeath). Ma on tę cechę, że… przechodzi przez (prawie) wszystkie bloki catch – w .NET właściwie nie można go “zdusić”. To jednak sprawia, że taki wyjątek może wykonać po drodze wszystkie bloki finally, co pozwala zwolnić zawłaszczone zasoby i blokady oraz przywrócić obiekty synchronizujące do właściwego stanu. Występująca w Windows API funkcja TerminateThread takiej możliwości już nam nie daje, przez co jej użycie może prowadzić do różnych kłopotów.

Wnioski z tych trzech sposobów są mniej więcej takie, że pisząc kod wykonywany w osobnym wątku powinniśmy zawsze przewidzieć “czysty” sposób na jego zakończenie. Poleganie na przerywaniu lub brutalnym zakańczaniu wątków może się bowiem źle skończyć.

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

Potęga w 4 kilobajtach

2008-05-09 19:49
Screen z demka 4k autorstwa Charibo
Screen z demka 4k
autorstwa Charibo

Ostatnio na Warsztacie zapanował niecodzienny pęd do sceny – demosceny, rzecz jasna. Jego główną przyczyną, a może i skutkiem, jest oczywiście rozpoczynający się dzisiaj konkurs na najlepsze demo 4k. Pod tym tajemniczym skrótem kryje się maksymalny rozmiar takiego dema (co obejmuje plik wykonywalny i wszystkie wykorzystywane dane), który nie może przekroczyć granicy 4096 bajtów.
“A co można napisać na takim malutkim kawałeczku?”, pewnie chciałoby się spytać. Okazuje się, że całkiem sporo – pod warunkiem, że znamy kilka sztuczek oraz korzystamy z odpowiednich narzędzi. Jest to na przykład specjalny linker, który dokonuje wyjątkowo efektywnej kompresji wynikowego pliku. Oprócz należy też odpowiednio skonfigurować kompilator, włączając wszelkie optymalizacje rozmiaru generowanego kodu (kosztem jego szybkości) oraz pozbywając się niepotrzebnych dodatków.

Takich dodatkiem jest na przykład biblioteka czasu wykonania (runtime). Jej wykluczenie sprawia jednak, że tracimy kilka mechanizmów języka C++, jak chociażby część funkcji matematycznych. Do szczególnie dotkliwych może należeć na przykład brak operacji potęgowania w jakiejkolwiek formie – zarówno funkcji pow, jak i exp. Jeśli ich potrzebujemy, musimy sami je sobie zapewnić, co w przypadku niecałkowitego wykładnika może być kłopotliwe. Chyba że odwołamy się bezpośrednio do jednostki zmiennoprzecinkowej – na przykład tak:

  1. float pow(float a, float b)  // a^b
  2. {
  3.     __asm
  4.     {
  5.         fld1
  6.         fld a
  7.         fyl2x
  8.         fld b
  9.         fmulp ST(1), ST(0)    // na wierzchu b * log2(a)
  10.        
  11.         // obliczenie 2^(b * log2(a)), czyli a^b
  12.         fst ST(1)
  13.         frndint
  14.         fld1
  15.         fscale
  16.         fxch ST(1)
  17.         fsubr ST(0), ST(2)
  18.         f2xm1
  19.         fld1
  20.         faddp ST(1), ST(0)
  21.         fmulp ST(1), ST(0)
  22.         fstp ST(1)
  23.     }
  24. }

Samodzielnie wyprodukowanie tego kawałka nie było aczkolwiek takie proste (głównie ze względu na kłopotliwe wymagania co do argumentu instrukcji F2XM1, która dokonuje potęgowania). Dlatego prezentuję go tu dla potomności i pożytku wszystkich tworzących małe dema, licząc, że komuś może się przydać :)

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


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