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:
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.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:
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
:
Wymaga to jeszcze dołączenia standardowego pliku nagłówkowego o wielce trafnej nazwie new.
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++ :]
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:
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 :)