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:
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:
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:
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ć.
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 :]
Modyfikator static
ma w C++ kilka różnych funkcji, a jedną z nich jest deklarowanie składników statycznych w klasach. Jak doskonale wiadomo taki składnik jest wspólny dla wszystkich obiektów tejże klasy i można go używać nawet, jeśli takie obiekty w ogóle nie istnieją.
Zwykle nie ma większych problemów ze statycznymi metodami, natomiast w przypadku pól sprawa często potrafi się skomplikować. Zwłaszcza, że statyczne elementy zmienne i stałe deklarujemy nieco inaczej…
Przede wszystkim należy pamiętać, że statyczne zmienne są właściwie pewnym rodzajem zmiennych globalnych, różniącym się prawie wyłącznie pod względem składniowym. A w przypadku zmiennych globalnych (deklarowanych w nagłówkach poprzez extern
) konieczne jest ich przypisanie do jakiegoś modułu kodu, czyli pliku .cpp. Dzięki temu linker może potem powiązać odwołania do tej zmiennej z nią samą niezależnie od miejsca jej użycia.
To samo dotyczy statycznych pól:
Jest jednak drobna różnica między statycznymi stałymi a zmiennymi polami. W tym drugim przypadku możemy zmiennej nadać początkową wartość; robimy to już w pliku .cpp przy jej definicji – tak jak powyżej. Zrobienie tego od razu w deklaracji jest niedopuszczalne.
Natomiast dla stałych wartość podać musimy i zwykle czynimy to dla odmiany właśnie w pliku nagłówkowym, jeszcze wewnątrz bloku class
:
Wówczas z kolei nie możemy wręcz inicjalizować stałej ponownie w pliku .cpp!
Dość dziwne, prawda? Ale w miarę łatwo to wyjaśnić. Otóż stała ma, jak sama nazwa wskazuje stałą wartość. Dzięki temu, że jest podana w nagłówku, kompilator może zoptymalizować każde miejsce jej normalnego użycia. Zamiast odwoływania się do pamięci, wpisze po prostu wartość stałej “na sztywno” w kodzie wynikowym. Będzie więc to podobne do działania #define
.
Tym niemniej jest to pewna niedogodność, jeśli statyczne stałe i statyczne zmienne definiuje się inaczej. Ale przecież to nie jedyna dziwna cecha C++, która sprawia, że tak ten język lubimy ;-)