Posts tagged ‘C/C++’

O dwóch funkcjach do ścieżek

2007-07-31 14:53

Kolejnymi kawałkami kodu z poprzedniej wersji mojej biblioteki uniwersalnej, którym postanowiłem się przyjrzeć, były różnego rodzaju funkcje “systemowe”. Nazwa jest może trochę na wyrost, jako że dotyczyły one głównie pewnych operacji na plikach i ich nazwach. Wśród z nich znalazły więc procedury do pobierania nazw plików, ich rozszerzeń, sprawdzania istnienia plików, tworzenia całych ścieżek katalogów i tym podobne.

Ostały się też dwie najciekawsze funkcje o bardzo podobnych do siebie sygnaturach:

  1. String AbsoluteToRelativePath(const String& strBase, const String& strTarget);
  2. String RelativeToAbsolutePath(const String& strBase, const String& strTarget);

Wykonują one interesującą pracę: konwertują ścieżki względne na bezwzględne i odwrotnie. Jak wiadomo, ścieżka bezwzględna określa położenie w systemie plików w sposób jednoznaczny, np.:

  • C:\Program Files\Taphoo

oznacza podkatalog Taphoo w katalogu Program Files na dysku C – niezależnie od tego, gdzie się teraz znajdujemy. Natomiast ścieżka względna – taka jak ta:

  • ../images/logo.gif

mówi jedynie, że w celu dostania się do pliku logo.gif należy wyjść do nadrzędnego katalogu (..), a stamtąd przejść do katalogu images.

Do czego może przydać się konwersja między oboma typami ścieżek? Dobrym przykładem jest dyrektywa #include w C(++), która akceptuje ścieżki względne:

  1. #include "../Base/StrUtils.hpp"

Preprocesor musi jest rozwinąć, aby móc wstawić zawartość dołączanego pliku. Z użyciem wymienionej wyżej funkcji RelativeToAbsolutePath można łatwo dodać podobną dyrektywę do języka opisu lub skryptowego.

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

Obiekty są pamiętliwe

2007-07-24 14:47

Czasami mylne wyobrażenie o pewnych elementach języka programowania potrafi ściągnąć na nas nie lada kłopoty. Właśnie ostatnio mogłem o się o tym przekonać, a sprawa dotyczy niezbyt często wykorzystywanego elementu C++, a mianowicie placement new.

Cóż to takiego? Jest to sposób na podanie operatorowi new adresu miejsca w pamięci, który ma wykorzystać. Może to brzmieć niedorzecznie, bo przecież new ma za zadanie właśnie alokację pamięci. To jednak tylko część prawdy, bowiem ten operator wywołuje też konstruktor tworzonego obiektu. W przypadku wersji placement będzie to więc jedyna czynność, jaką wykona.
Składnia placement new wygląda mniej więcej tak:

  1. CFoo* = new (pvAddress) CFoo(...);

Na czym jednak polega problem? Otóż tego specjalnego wariantu operatora new nie można wywoływać bezkarnie. W szczególności nie jest to sposób na “przezroczyste” wywołanie konstruktora dla jakiegoś obiektu.

Pytanie oczywiście brzmi: do czego było mi potrzebne użycie tego rzadkiego mechanizmu języka? No cóż, teraz już wiem, że do niczego, jednak wcześniej myślałem, że będzie to niezły sposób na wczytywanie zasobów.
W skrócie: w mojej bibliotece zasoby takie jak np. tekstury czy bufory tworzą dość prostą hierarchię dziedziczenia. Parametry niektórych z nich – jak np. tekstur – można wczytywać z pliku tekstowego – w tym przypadku chodzi chociażby o nazwę pliku graficznego z obrazkiem tekstury.
Sęk w tym, że zasób tekstury (reprezentowany przez klasę CTexture) jest też zasobem DirectX (dziedziczy po IDxResource). A z tym związane są kolejne parametry, jak np. pula pamięci czy sposób użycia zasobu (to akurat nie jest mój wymysł, tylko DirectXa ;P). One również mogą być zapisane w pliku i trzeba je uwzględnić przy wczytywaniu tekstury.

W moim “genialnym” rozwiązaniu wymyśliłem więc, że podczas tworzenia obiektu CTexture zostanie najpierw stworzony obiekt IDxResource (dzięki czemu zostanie załatwiona kwestia “DX-owych” parametrów). Następnie w tym samym miejscu pamięci – placement new! -skonstruujemy obiekt CTexture, który zajmie się załadowaniem pozostałych danych.
I to rzeczywiście działało, dopóki się nie zorientowałem, że zapomniałem dopisać do menedżera zasobów kodu, który zwalniałby wszystko przy kończeniu programu. Wtedy to w ruch poszły destruktory, a wtedy pojawiły się… przerwania systemowe informujące o uszkodzeniu sterty.

Powodem jest fakt, że obiekty (a ściślej mechanizm ich alokacji) pamiętają sposób, w jaki zostały stworzone. Jeżeli było to zwykłe new, to zniszczenie obiektu pociąga za sobą zwolnienie pamięci. Lecz jeżeli było to placement new, to operator delete zakłada, że nic nie wie o obszarze pamięci, w którym obiekt rezyduje – więc go nie zwalnia. W istocie FAQ C++ wyraźnie pisze, by przy używaniu placement new samemu wywoływać destruktor, a potem samodzielnie zwalniać pamięć.
placement new nie jest po prostu sposobem na wywołanie konstruktora – z jego użyciem wiążą się określone konsekwencje. Jak widać czasami można przekonać się o nich w mało przyjemny sposób :)

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

Tam, gdzie kończy się C++

2007-07-20 19:26

Pod jednym z ostatnich postów, dotyczącym kwestii rysowania dwuwymiarowych sprite’ów, rozpętała się całkiem ciekawa dyskusja. Jej głównym tematem było to, czy i jak implementowany system 2D będzie przydatny w renderowaniu elementu niezbędnego każdej grze – choćby była do bólu trójwymiarowa. Chodzi naturalnie o graficzny interfejs użytkownika, czyli GUI.

Wyświetlanie elementów takiego interfejsu (na który składać się będą różnego rodzaju kontrolki, jak przyciski, pola tekstowe czy listy rozwijalne) to dość złożone zagadnienie. Można je rozwiązać łatwo acz nieefektywnie lub trochę trudniej, ale prawie optymalnie :) Oprócz tego GUI prezentuje sobą też bardzo poważne wyzwanie projektowe, które wymaga dokładnego zaplanowania występujących w nim klas i ich składowych.
Niestety w C++ dochodzi do tego jeszcze jeden problem, który w dodatku jest nieco krępujący. Nie lubimy się nim chwalić w towarzystwie i w sumie chcielibyśmy o nim zapomnieć, zakopać jak najgłębiej, by tylko usunąć go z pola widzenia. Ale rzeczywistość skreczy i uciekanie od kłopotu go nie rozwiąże… Trzeba wypowiedzieć go na głos. Otóż – w C++ nie ma delegatów i w związku z tym obsługa zdarzeń generowanych przez kontrolki GUI zaczyna być problemem.

‘Delegat’ to taka trochę myląca dla prostego i wręcz niezbędnego w obiektowym języku programowania mechanizmu. Delegatem nazywamy bowiem pewien rodzaj wskaźnika, który pokazuje na konkretną metodę w konkretnym obiekcie. Taki wskaźnik jest konieczny do wygodnego realizowania mechanizmu callback, czyli powiadamiania (poprzez wywołanie owej metody), że coś się zdarzyło.
W programowaniu strukturalnym nie ma z tym problemu, jako że istnieją wskaźniki na zwykłe funkcje. Najbardziej znanym jest chyba wskaźnik na procedurę zdarzeniową okna w Windows API:

  1. typedef LRESULT (CALLBACK *WNDPROC)(HWND, UINT, WPARAM, LPARAM);

Mimo pokrętnej składni, wskaźniki do funkcji dobrze spełniają swoje zadanie. Problem polega na tym, że w programowaniu obiektowym nie życzymy sobie obecności “luźnych” funkcji, skoro wszystko zamykamy w klasy. Ten rodzaj wskaźników jest więc kompletnie nieprzydatny.

C++ ma też inny rodzaj wskaźników na kod. Są to w gruncie rzeczy bardzo dziwne twory, dla których samodzielnego zastosowania póki co nie udało mi się znaleźć (co oczywiście nie znaczy, że ono nie istnieje). Te wskaźniki służą do pokazywania na metodę o danej sygnaturze, lecz nie w obiekcie, tylko w klasie. Jak już mówiłem, są to bardzo wyjątkowo nietypowe twory (C++ jest chyba jedynym językiem posiadającym coś podobnego), a towarzysząca im składnia jest zdecydowanie odpychająca.
Ten egzotyczny mechanizm można jednak użyć do implementacji własnego systemu delegatów w C++. Kiedyś nawet popełniłem artykuł wyjaśniający, jak można to zrobić. Przedstawiony tam sposób jest jednak mało elegancki i dlatego jest raczej wątpliwe, bym bo użył.

Skłaniam się raczej do zastosowania zewnętrznej biblioteki, co jest według mnie dość przykrą koniecznością. Ciężko bowiem pogodzić się z tym, że używany język programowania zmusza do uciekania się do zewnętrznych i nieustandaryzowanych rozwiązań tylko po to, by w jakiś w miarę sensowny sposób symulować to, czego mu ewidentnie brakuje. W dodatku kłóci się to z moją filozofią konstrukcji kodu “bibliotecznego” (do którego należy silnik graficzny lub silnik gry), która każe eliminować zewnętrzne powiązania. Czasem jednak stajemy pod ścianą.
Opcje są właściwie dwie. Pierwsza to użycie bibliotek Function i Bind wchodzących w skład Boosta. Mimo że Boost jest rzecz jasna świetnie opracowanym zestawem bibliotek rozszerzających C++, chciałbym jednak jej uniknąć ze względów wydajnościowych. Dokładniej chodzi o obciążenie kompilatora (z którego właściwie każda część Boosta wyciska ostatnie soki) oraz na znacznie ważniejszą kwestię wywołań delegatów w czasie wykonania programu. Dlatego skłaniam się raczej ku innej bibliotece o nazwie FastDelegate, która pod tym względem nie ma sobie równych. Być może kiedyś uda mi się napisać coś równie dobrego i będę mógł z niej zrezygnować, ale chyba bardziej prawdopodobne jest, że do tego czasu C++ będzie miał już wbudowany mechanizm delegatów :)

Mógłbym jeszcze wspomnieć o innym rozwiązaniu polegającym na użyciu polimorfizmu i funkcji wirtualnych i o tym, dlaczego w C++ – ze względu na brak niestatycznych klas wewnętrznych – byłoby ono skrajnie niewygodne. Myślę jednak, że na dzisiaj moglibyśmy już sobie tego oszczędzić :D

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


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