Jak wiadomo, do konstruktorów w C++ możemy doczepić listy inicjalizacyjne, pozwalające nadawać początkowe wartości polom macierzystej klasy (i nie tylko zresztą). Związany jest z tym pewien pozornie zaskakujący fakt, dotyczący kolejności inicjalizacji tych pól:
Są one zawsze ustawiane w porządku zgodnym z ich deklaracjami w klasie. Tak więc powyżej inicjalizowane będzie najpierw pole a
, zaś potem b
– mimo że w konstruktorze kolejność jest odwrotna. Oczywiście w takim prostym przypadku nie ma to znaczenia, ale jeśli między polami występują zależności, wtedy może to być przyczyną “dziwnych” błędów. Porządniejsze kompilatory ostrzegają na szczęście przed rozbieżnymi kolejnościami pól w klasie i na liście w konstruktorze.
Można naturalnie zapytać, dlaczego w ogóle jest to zorganizowane tak nieintuicyjnie. Pierwsza nasuwająca się odpowiedź – “bo to C++” – nie jest bynajmniej zadowalająca :) Tak naprawdę przyczyną jest to, iż konstruktorów może być naturalnie więcej niż jeden:
i mogą mieć one różną kolejność pozycji na liście inicjalizacyjnej. Gdyby to ona liczyła się bardziej, wtedy różne obiekty tej samej klasy byłyby konstruowane na różne sposóby. Co gorsza, sposób tej konstrukcji (kolejność inicjalizacji pól) musiałby zostać gdzieś zapamiętany na czas życia obiektu, jako że jego pola powinny być później zniszczone w kolejności odwrotnej do konstrukcji – zgodnie z zasadami języka.
Krótko mówiąc, zastosowanie “intuicyjnego” podejścia skutkowałoby nie tylko znacznie większymi niejasnościami, ale i dodatkowymi kłopotami zarówno dla programisty, jak i kompilatora.
W C++ konstruktor domyślny jest generowany automatycznie, jeśli w klasie nie zostanie zdefiniowany żaden inny. W przeciwnym wypadku nie jst on tworzony, co może prowadzić do sytuacji, gdy klasa posiada jedynie takie konstruktory, które wymagają podania parametrów. Jest ona niekorzystna przynajmniej z dwóch powodów:
Kiedy jednak zmieni się postać konstruktora klasy bazowej, wówczas modyfikacja będzie musiała dotyczyć wszystkich bezpośrednich potomków tej klasy. W nowej wersji standardu C++ ma być aczkolwiek wprowadzone nowe zadanie dla słowa kluczowego using
, które pozwoli na automatyczne utworzenie konstruktorów “przekaźnikowych” – takich, których jedyną rolą jest wywołanie konstuktorów bazowych z takimi samymi parametrami jak te otrzymane w konstruktorze pochodnym. Jeśli jednak przy okazji chcemy coś zmienić czy pominąć, to nie ma rady: trzeba całą tę “sztafetę” zakodować ręcznie.
Dlatego też klasy będące przede wszystkim klasami bazowymi w dziedziczeniu dobrze jest wyposażać w konstruktory bezparametrowe. Może to aczkolwiek wymagać wyróżnienia w obiekcie stanów Zainicjowany-Niezainicjowany, które powinny być sprawdzane w jego metodach i w razie potrzeby odpowiednio sygnalizowane.
Wiadomo, że konstruktor to specjalna metoda, która jest wywoływana przy tworzeniu nowego obiektu i ma za zadanie go zainicjalizować. Okazuje się, że w innej wersji “konstruktor” (albo coś, co go przypomina) może służyć do inicjalizacji nie obiektu, lecz samej klasy. A co takiego ma klasa, że należy czasem zadbać o jej prawidłową inicjalizację? Ano chociażby składowe statyczne – wspólne dla wszystkich obiektów.
W C# możemy sobie zdefiniować statyczny konstruktor, który może się tym zająć – jak również (niemal) czymkolwiek innym. Taki konstruktor, podobnie jak zwykły ma postać metody o tej samej co klasa nazwie, lecz jest opatrzony modyfikatorem static
. Zostaje on wywołany przed stworzeniem pierwszego obiektu klasy lub próbą dostępu do jakiejkolwiek składowej statycznej. Całkiem praktyczne, prawda?
Coś podobnego ma również Java, a zwie się to oryginalnie ‘statycznym inicjalizatorem’. W odróżnieniu od wariantu z C#, inicjalizator ten jest wywoływany wcześniej – już w momencie załadowania klasy przez maszynę wirtualną. Właściwie jednak nie ma to wielkiego znaczenia, bo efekt jest analogiczny: jeśli ów inicjalizator ma zrobić coś przed pierwszym użyciem klasy, na pewno zostanie to zrobione. Jego kod pisze się w definicji klasy, w bloku otoczonym nawiasami klamrowymi i opatrzonym – a jakże – słówkiem static
.
A jak pod tym względem wypada C++? No cóż, jak zwykle blado :) Kruczkiem standardu jest fakt, że wszelkie składowe statyczne klas mają niezdefiniowaną kolejność inicjalizacji, jeśli są przypisane do odrębnych modułów (plików .cpp). Jeśli więc takie składowe zależą od siebie, to wynikiem mogą być tylko kłopoty.
Poradzić na to można dwojako – zależnie od tego, którą zaletę statycznych konstruktorów chcemy wykorzystać. Jeżeli na przykład chcemy, aby jakaś zmienna statyczna lub globalna była zainicjowana niezależnie od tego, gdzie i kiedy chcemy ją użyć, najlepiej uczynić ją lokalną statyczną zmienną w funkcji:
Jeśli z kolei musimy mieć pewność, że przed stworzeniem obiektu naszej klasy zostaną wykonane jakieś dodatkowe czynności, to możemy to częściowo osiągnąć za pomocą statycznej flagi:
Niezależnie jednak od tego, co jest nam akurat potrzebne, nie powinniśmy nigdy zapominać o rozważeniu zależności między składowymi statycznymi klas i/lub zmiennymi globalnymi w naszym kodzie, jeśli chcemy uniknąć niezdefiniowanego zachowania. Bo wtedy po prostu wszystko się zdarzyć może :]
Konstruktor to taka dziwna funkcja składowa, która jest wywoływana w trakcie tworzenia obiektu. Jest więc w zasadzie odpowiedzialna za jego przygotowania do użytku, czyli odpowiednią inicjalizację. Tak przynajmniej jest w teorii.
W rzeczywistości jednak konstruktor powinien robić jak najmniej. Jest tak z kilku powodów:
Pewne duże obiekty mogą oczywiście wymagać skomplikowanej inicjalizacji. Wówczas, zamiast pakować całą tę logikę do konstruktora, lepiej jest prawdopodobnie oddzielić inicjalizację od właściwego tworzenia obiektu. W tym celu można go zaopatrzyć w metodę w rodzaju Create
czy Load
, która dokonuje właściwego przygotowania obiektu do działania.
Może wydawać się to lekkim zawracaniem głowy, ale dla nietrywialnych obiektów ma sens. Pozbywamy się wad wymienionych powyżej, a jednocześnie zyskujemy dodatkowe możliwości (choćby dlatego, że wspomniana metoda może być wirtualna).
Przychodzi czasem ochota, aby zapewnić dostęp do tych samych danych na różne sposoby. Ten pomysł można zrealizować w sposób poprawny albo i nie :) Dzisiaj właśnie przekonałem się ostatecznie, dlaczego stosowana przeze mnie metoda należała do tej drugiej kategorii.
Otóż dość powszechnie wykorzystuję pewną strukturę o nazwie SMouseEventArgs
, która docelowego zawiera informacje o jakimś zdarzeniu związanym z myszą (klik, przesunięcie, itd.). Wśród tych informacji jest między innymi pozycja kursora w momencie zajścia zdarzenia, która początkowo była opisana po prostu przez dwa pola X
i Y
.
W pewnym momencie zauważyłem jednak, że dość często stosuję konstrukcję podobną do tej:
gdzie POINT2F
jest strukturą opisującą punkt na płaszczyźnie. Aby zaoszczędzić sobie pisania (i konstrukcji tymczasowego obiektu) postanowiłem w przypływie kreatywności dodać do SMouseEventArgs
drugi sposób dostępu do tych współrzędnych – pole Position
. Jak? Otóż… przy pomocy unii:
Sęk w tym, że POINT2F
jest porządną strukturą i zawiera między innymi kilka konstruktorów. A jest niedozwolone, by obiekt klasy posiadającej zdefiniowany przez programistę konstruktor albo nietrywialny destruktor mógł być elementem unii.
Kompilator raczył mnie więc błędem, ale wówczas w kolejnym przypływie kreatywności stosowałem “objeście” w postaci anonimowej struktury:
I byłem święcie przekonany, że przecież skoro POINT2F
nie alokuje własnej pamięci, nie otwiera plików ani nie robi żadnych innych tego typu czynności, po których trzeba by sprzątać, to przecież nic złego nie może się stać…
Naturalnie byłem w błędzie :) Dalszym elementem układanki jest konstruktor SMouseEventArgs
, przyjmujący kilka parametrów i inicjalizujący nimi strukturę:
Na oko niby wszystko jest w porządku. Tylko dlaczego cokolwiek byśmy przekazali jako x
i y
, w wynikowej strukturze zawsze i tak współrzędne będą zerami?!
Ot, kolejna nierozwiązania zagadka Wszechświata. Przynajmniej do chwili, gdy uświadomimy sobie dwie rzeczy:
Aplikując te zasady do powyższego przypadku, mamy bardzo ciekawy scenariusz. Mianowicie pola X
i Y
są poprawnie wypełniane parametrami konstruktora SMouseEventArgs
, lecz w chwilę potem to samo miejsce pamięci jest… nadpisywane przez konstruktor POINT2F
. Dlaczego? Ano dlatego, że pole Position
też musi zostać zainicjowane, a domyślny konstruktor POINT2F
wstępnie ustawia je na punkt (0,0).
Mniej więcej takie są skutki prób bycia mądrzejszym od twórców języka i kompilatora :) Można by oczywiście brnąć dalej w to rozwiązanie, zmieniając kolejność deklaracji pól albo jawnie inicjalizować pole Position
zamiast X
i Y
. Cały czas jednak jest to stąpanie po cienkim lodzie.
Dlatego chyba najwyższy czas ograniczyć swoją kreatywność i następnym razem zastosować może mało efektowne, ale za to stuprocentowo bezpieczne metody dostępowe :)
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 :)