[11] Destruktory
(Część C++ FAQ Lite, Copyright © 1991-2002, Marshall Cline, cline@parashift.com)


FAQ - sekcja [11]:


[11.1] O co właściwie chodzi z tymi destruktorami?

Destruktor daje obiektowi ostatnie namaszczenie.

Destruktory są używane do zwalniania zasobów alokowanych wcześniej przez obiekt. Np. hipotetyczna klasa Lock mogłaby zamykać semafor [obiekt sterujący dostępem do pewnych danych lub fragmentu kodu - przyp. tłum.], zaś jej destruktor - otwierać. Najczęściej jest tak, że konstruktory tworzą składowe obiektu przy użyciu operatora new, natomiast destruktory niszczą je operatorem delete.

Destruktory są metodami typu "przygotuj się na śmierć". W języku angielskim, zamiast słowa "destruktor" często stosuje się skrót "dtor".

GóraDółPoprzednia sekcjaNastępna sekcjaSzukaj w FAQ ]


[11.2] W jakiej kolejności obiekty podlegają destrukcji?

W odwrotnej, niż były konstruowane: pierwszy utworzony = ostatni zniszczony.

W poniższym przykładzie, najpierw zostanie wywołany destruktor obiektu b, a następnie destruktor obiektu a.

 void userCode()
 {
   Fred a;
   Fred b;
   
// ...
 }

GóraDółPoprzednia sekcjaNastępna sekcjaSzukaj w FAQ ]


[11.3] W jakiej kolejności podlegają destrukcji obiekty wewnątrz tablicy?

W odwrotnej niż były konstruowane: pierwszy utworzony = ostatni zniszczony.

W poniższym przykładzie destruktory zostaną wywołane kolejno dla obiektów: a[9], a[8], ..., a[1], a[0]:

 void userCode()
 {
   Fred a[10];
   
// ...
 }

GóraDółPoprzednia sekcjaNastępna sekcjaSzukaj w FAQ ]


[11.4] Czy mogę przeciążyć destruktor zdefiniowanej przeze mnie klasy?

Nie.

Możesz utworzyć tylko jeden destruktor dla klasy Fred. Musi się on nazywać Fred::~Fred(). Nie może on pobierać żadnych parametrów, ani też zwracać jakiejkolwiek wartości.

Nie możesz przekazać destruktorowi żadnych parametrów, gdyż nigdy nie powinno się jawnie wywoływać destruktora (właściwie to prawie nigdy).

GóraDółPoprzednia sekcjaNastępna sekcjaSzukaj w FAQ ]


[11.5] Czy powinienem(-nam) jawnie wywoływać destruktor dla zmiennej lokalnej?

Nie!

Destruktor zostanie wtedy ponownie wywołany pod koniec bloku (znak "}"), w którym zmienna lokalna została utworzona. Gwarantuje to specyfikacja języka; "przepisowa" destrukcja następuje automagicznie i nie można jej w żaden sposób powstrzymać. Jeżeli wywołasz destruktor po raz drugi na tym samym obiekcie, możesz odczuć naprawdę poważne tego skutki. Bum! Nie żyjesz!

GóraDółPoprzednia sekcjaNastępna sekcjaSzukaj w FAQ ]


[11.6] Co zrobić w sytuacji, gdy chcę "ubić" lokalny obiekt przed końcem bloku, w którym jest on zdefiniowany (przed znakiem })? Czy mogę jawnie wywołać destruktor lokalnego obiektu jeśli naprawdę tego potrzebuję? UPDATED!

[Recently wordsmithed (in 9/02). Click here to go to the next FAQ in the "chain" of recent changes.]

Nie! [Dla wyjaśnienia czemu, proszę przeczytać poprzedniego faqa].

Przypuśćmy, że ubocznym (ale jednocześnie celowym) efektem destrukcji obiektu klasy "Plik" jest zamknięcie pliku obsługiwanego przez ten obiekt. Następnie załóżmy, że masz obiekt p klasy "Plik" i chcesz zamknąć "Plik" p przed końcem bloku, w którym zdefiniowano ten obiekt (przed znakiem }):

 void jakisKod()
 {
   Plik p;
 
   
...kod do wykonania po otwarciu pliku p...
 
   
// <— Tutaj plik p ma zostać zamknięty (wywołanie destruktora p)
   
   
...kod do wykonania po zamknięciu pliku p...
 }

Istnieje proste rozwiązanie tego problemu. Ale zanim je poznasz, zapamiętaj: nie wywołuj destruktora jawnie!

GóraDółPoprzednia sekcjaNastępna sekcjaSzukaj w FAQ ]


[11.7] W porządku: nie będę wywoływał(a) destruktora jawnie; ale w takim razie jak rozwiązać powyższy problem?

[Dla wyjaśnienia o co chodzi, proszę przeczytać poprzedniego faqa].

Po prostu utwórz nowy, "sztuczny" blok {...} i w nim zdefiniuj obiekt:

 void jakisKod()
 {
   {
     Plik p;
     
...kod do wykonania po otwarciu pliku p...
   }
 
// ^— tutaj destruktor p jest wywoływany "automagicznie"
 
   
...kod do wykonania po zamknięciu pliku p...
 }

GóraDółPoprzednia sekcjaNastępna sekcjaSzukaj w FAQ ]


[11.8] Co mam zrobić, jeśli nie mogę umieścić obiektu w takim "sztucznym" bloku?

W większości przypadków możesz ograniczyć "czas życia" (ang. lifetime) lokalnego obiektu umieszczając go w sztucznym bloku ({...}). Jeśli z jakiegoś powodu nie możesz tego zrobić, zdefiniuj metodę, której działanie będzie podobne do destruktora. Ale nie wywołuj destruktora bezpośrednio!

Przykładowo, do klasy "Plik" możnaby dodać metodę zamknij() (tak jak w poniżej przedstawionym kodzie). W większości przypadków destruktor po prostu wywoływałby tę metodę. Zauważ, że metoda zamknij() musi w jakiś sposób zaznaczać, że "Plik" został zamknięty, tak aby późniejsze wywołania tej metody nie próbowały go ponownie zamknąć. Na przykład, metoda ta, po pomyślnym zamknięciu pliku, mogłaby ustawiać pole deskryptor_ na jakąś nonsensowną wartość, powiedzmy -1, a przed próbą zamknięcia pliku sprawdzałaby, czy deskryptor_ został już wcześniej ustawiony na -1.

 class Plik {
 public:
   void zamknij();
   ~Plik();
   
// ...
 private:
   int deskryptor_;   
// deskryptor_ >= 0 tylko gdy plik jest otwarty
 };
 
 Plik::~Plik()
 {
   zamknij();
 }
 
 void Plik::zamknij()
 {
   if (deskryptor_ >= 0) {
     
...kod zlecający systemowi operacyjnemu zamknięcie pliku...
     deskryptor_ = -1;
   }
 }

Zwróć uwagę, że niektóre metody klasy "Plik" być może będą potrzebowały sprawdzać, czy deskryptor_ jest równy -1 (tzn., sprawdzać czy "Plik" jest otwarty).

Ponadto wszystkie konstruktory, których działanie nie doprowadziłoby do otwarcia pliku [np. na skutek błędu - przyp. tłum.], muszą ustawić deskryptor_ na -1.

GóraDółPoprzednia sekcjaNastępna sekcjaSzukaj w FAQ ]


[11.9] Czy mogę jawnie wywołać destruktor w przypadku obiektu alokowanego operatorem new?

Prawdopodobie nie.

Jeżeli obiekt nie został alokowany przy użyciu placement new, to powinno się go usuwać raczej poprzez operator delete niż jawne wywołanie destruktora. Na przykład, załóżmy że alokowałeś(-aś) pewien obiekt przy użyciu "zwyczajnego new":

 Fred* p = new Fred();

W takim przypadku destruktor Fred::~Fred() zostanie wywołany automagicznie podczas usuwania obiektu operatorem delete:

 delete p;  // Automagicznie wywołuje p->~Fred()

W powyższym przykładzie nie powinno się jawnie wywoływać destruktora, gdyż nie spowoduje to zwolnienia obszaru pamięci zajmowanego przez obiekt klasy Fred. Zapamiętaj: delete p wykonuje dwie czynności: wywołuje destruktor obiektu p i zwalnia pamięć zajmowaną przez ten obiekt.

GóraDółPoprzednia sekcjaNastępna sekcjaSzukaj w FAQ ]


[11.10] Co to jest "placement new" i do czego można go użyć?

Istnieje wiele zastosowań "placement new". Najprostszym z nich jest umieszczenie obiektu w ściśle określonym miejscu w pamięci. W tym celu podaje się wskaźnik do tego miejsca jako parametr dla operatora new:

 #include <new>        // Aby użyć "placement new" musisz dołączyć ten nagłówek
 #include "Fred.h"     
// Deklaracja klasy Fred
 
 void someCode()
 {
   char memory[sizeof(Fred)];     
// Wiersz #1
   void* place = memory;          
// Wiersz #2
 
   Fred* f = new(place) Fred();   
// Wiersz #3 (zobacz: "UWAGA" poniżej)
   
// Wskaźniki f i place są równe (wskazują na to samo miejsce
   
// w pamięci)
 
   
// ...
 }

W wierszu #1 zostaje utworzona tablica mająca rozmiar sizeof(Fred) bajtów, czyli wystarczająco duża na zmieszczenie w niej obiektu klasy Fred. W wierszu #2 tworzony jest wskaźnik wskazujący na początek tej tablicy (doświadczeni programiści C zauważą z pewnością, że ten krok jest w zupełności zbędny; ma on tylko uczynić kod bardziej oczywistym). W wierszu #3 nastąpi po prostu wywołanie konstruktora Fred::Fred() [operator new nie będzie alokował żadnego obszaru w pamięci, ponieważ podano mu już miejsce, w którym ma zostać utworzony obiekt - przyp. tłum.]. Wskaźnik this w konstruktorze Freda będzie równy place. Stąd wskaźnik f również będzie równy place.

PORADA: Nie używaj "placement new" chyba że musisz. Stosuj je tylko wtedy, gdy naprawdę zależy Ci na tym, aby obiekt znajdował się w konkretnym miejscu w pamięci. Przykładem może być sytuacja, gdy programowany przez Ciebie sprzęt posiada wbudowany układ czasowy, który jest mapowany w pewnym miejscu w pamięci (ang. memory-mapped I/O), a ty chcesz w tym miejscu umieścić obiekt klasy Zegar [który np. odczytywałby aktualną godzinę i datę z tego układu czasowego - przyp. tłum.].

[Przypis tłumacza - Mapowanie urządzeń wejścia/wyjścia w pamięci polega na tym, że urządzenia te widziane są jako fragment pamięci operacyjnej. Typowym przykładem w architekturze PC może być karta graficzna - jej pamięć zawierająca wyświetlany obraz (tzw. "bufor ramki") widziana jest przez procesor jako fragment pamięci RAM. Podczas przesyłania danych do "bufora ramki" procesorowi "wydaje się", że nawiązuje komunikację z pamięcią RAM, ale w rzeczywistości dane są przesyłane między nim a kartą graficzną. W ten sposób sterowanie urządzeniem wygląda jak dostęp do "zwykłego" obiektu w pamięci.]

UWAGA: Bierzesz całkowitą odpowiedzialność za to, że wskaźnik będący parametrem operatora new wskazuje na obszar pamięci odpowiednio wyrównany (ang. aligned) i wystarczający na zmieszczenie nowego obiektu. Ani kompilator ani biblioteka czasu pracy programu (ang. run-time system) nie będą sprawdzać, czy wskaźnik spełnia te wymagania. Jeżeli klasa Fred wymagałaby wyrównania do 4-bajtowego bloku, a dostarczony wskaźńik wskazywałby na obszar pamięci niewyrównany w ten sposób, mogłoby to narazić Twój program na poważną katastrofę (jeżeli nie wiesz co oznacza "wyrównanie miejsca w pamięci", proszę nie stosuj "placement new"). Zostałeś(aś) ostrzeżony(a)!

Ponadto bierzesz całkowitą odpowiedzialność za destrukcję obiektu utworzonego przy użyciu "placement new". Przeprowadza się ją przez jawne wywołanie destruktora:

 void someCode()
 {
   char memory[sizeof(Fred)];
   void* p = memory;
   Fred* f = new(p) Fred();
   
// ...
   f->~Fred();   
// Jawne wywołanie destruktora obiektu
 }

Jest to prawdopodobnie jedyna sytuacja, w której będziesz potrzebował(a) jawnie wywołać destruktor.

Notka: istnieje wygodniejsza, ale bardziej skomplikowana metoda przeprowadzania destrukcji obiektów.

GóraDółPoprzednia sekcjaNastępna sekcjaSzukaj w FAQ ]


[11.11] Czy, gdy piszę destruktor, muszę jawnie wywoływać destruktory pól składowych mojej klasy?

Nie. Nigdy nie ma potrzeby jawnego wywoływania destruktorów (z wyjątkiem placement new).

Destruktor klasy automagicznie wywołuje destruktory wszystkich pól składowych. Pola są niszczone w odwrotnej kolejności niż występują w deklaracji klasy.

 class Member {
 public:
   ~Member();
   
// ...
 };
 
 class Fred {
 public:
   ~Fred();
   
// ...
 private:
   Member x_;
   Member y_;
   Member z_;
 };
 
 Fred::~Fred()
 {
   
// Kompilator automagicznie wywołuje z_.~Member()
   
// Kompilator automagicznie wywołuje y_.~Member()
   
// Kompilator automagicznie wywołuje x_.~Member()
 }

GóraDółPoprzednia sekcjaNastępna sekcjaSzukaj w FAQ ]


[11.12] Czy w destruktorze klasy potomnej muszę umieścić wywołanie destruktora klasy bazowej?

Nie. Nigdy nie ma potrzeby jawnego wywoływania destruktorów (z wyjątkiem placement new).

Destruktor klasy potomnej (nieważne czy go jawnie zdefiniujesz czy też nie) automagicznie wywołuje destruktory dla każdej klasy bazowej. Najpierw niszczone są pola należące do klasy potomnej, a następnie niszczone są kolejne klasy bazowe. Jeżeli klasa posiada kilka bezpośrednich klas bazowych (dziedziczenie wielokrotne) to są one niszczone w odwrotnej kolejności, niż zostały one zapisane w liście dziedziczenia klasy.

 class Member {
 public:
   ~Member();
   
// ...
 };
 
 class Base {
 public:
   virtual ~Base();     
// Wirtualny destruktor
   
// ...
 };
 
 class Derived : public Base {
 public:
   ~Derived();
   
// ...
 private:
   Member x_;
 };
 
 Derived::~Derived()
 {
   
// Kompilator "automagicznie" wywoła x_.~Member()
   
// Kompilator "automagicznie" wywoła Base::~Base()
 }

Notka: Kolejność w przypadku dziedziczenia wirtualnego jest bardziej zagmatwana. Jeżeli Twój kod ma polegać na kolejności inicjalizacji/deinicjalizacji w dziedziczeniu wirtualnym, będziesz potrzebował(a) znacznie więcej informacji niż jest to zawarte w tym faqu.

GóraDółPoprzednia sekcjaNastępna sekcjaSzukaj w FAQ ]


[11.13] Czy mój destruktor powinien wyrzucać wyjątek w razie wystąpienia problemu?

Wystrzegaj się!!! Wyjaśnienia znajdziesz tutaj.

GóraDółPoprzednia sekcjaNastępna sekcjaSzukaj w FAQ ]


[11.14] Czy można zmusić operator new aby alokował pamięć z określonego obszaru?

Tak. Dobrą wiadomością będzie z pewnością fakt, że tego rodzaju "pule pamięci" (ang. "memory pool") mają wiele zastosowań. Zła wiadomość jest natomiast taka, że zanim omówimy te wszystkie zastosowania, będziemy musieli przebyć grzęzawisko wiedzy na temat "jak to działa?". Jednak z drugiej strony, jeśli teraz nic nie wiesz na temat "pul pamięci", to przebrnięcie przez tego faqa może się okazać warte zachodu — być może nauczysz się czegoś użytecznego!

Na początku, przypomnijmy sobie do czego służy alokator pamięci - ma on za zadanie zwrócić niezainicjowany osbzar w pamięci; nie tworzy on nowych obiektów [tylko po prostu przygotowuje dla nich miejsce w pamięci - przyp. tłum.]. W szczególności alokator nie ustawia wskaźnika do tablicy wirtualnej (ang. virtual pointer) ani jakiegokolwiek pola obiektu, gdyż jest to zadaniem konstruktora wywoływanego tuż po alokatorze. Mając prostą funkcję spełniającą rolę alokatora, np. allocate(), możnaby użyć placement new do skonstruowania obiektu w obszarze przygotowanym przez alokator. Innymi słowy, poniższy kod jest równoważny instrukcji "new Foo()":

 void* raw = allocate(sizeof(Foo));  // linia 1
 Foo* p = new(raw) Foo();            
// linia 2

OK, zakładając, że uzyłeś(-aś) placement new i przetrwałeś(-aś) powyższe dwie linijki kodu, możemy przejść do kolejnego kroku - zamiany funkcji-alokatora na obiekt. Tego rodzaju obiekt nazywa się "pulą pamięci" lub "areną pamięci" (ang. memory arena). Rozwiązanie to pozwala użytkownikowi na posiadanie kilku pul (lub "aren", jak kto woli), z których może on alokować pamięć. Każdy z tych obiektów-aren, przy użyciu jakiegoś wywołania systemowego, najpierw alokuje duży obszar pamięci (np. z pamięci współdzielonej, z pamięci programu, ze stosu itd.; zobacz niżej) a następnie udostępnia go programowi w formie mniejszych fragmentów, w zależności od potrzeb. Twoja klasa obsługująca pulę pamięci mogłaby wyglądać mniej więcej tak:

 class Pool {
 public:
   void* alloc(size_t nbytes);
   void dealloc(void* p);
 private:
   ...dane używane przez obsługę Twojej puli pamięci...
 };
 
 void* Pool::alloc(size_t nbytes)
 {
   ...miejsce na Twój algorytm...
 }
 
 void Pool::dealloc(void* p)
 {
   ...miejsce na Twój algorytm...
 }

Teraz jeden z użytkowników Twojej klasy mógłby mieć pulę o nazwie pool, z której alokowałby obiekty w ten sposób:

 Pool pool;
 ...
 void* raw = pool.alloc(sizeof(Foo));
 Foo* p = new(raw) Foo();

Lub prościej:

 Foo* p = new(pool.alloc(sizeof(Foo))) Foo();

Zdefiniowanie puli pamięci jako klasy jest o tyle dobrym rozwiązaniem, że pozwala ono użytkownikom na utworzenie N różnych pul pamięci zamiast jednej dużej współdzielonej przez wszystkich użytkowników. Dzięki temu użytkownicy mogą robić różne, dziwne rzeczy. Na przykład, jeśli fragment programu alokowałby jak dziki mnóstwo różnych obiektów, to mógłby je później wszystkie usunąć za jednym zamachem, usuwając po prostu pulę w której te obiekty zostały alokowane (zamiast usuwać każdy z nich osobno używając operatora delete). Innym przykładem jest założenie puli w obszarze "pamięci współdzielonej" (tzn. pamięci wspólnej dla kilku procesów) i pobieranie pamięci z obszaru współdzielonego zamiast z "lokalnej pamięci procesu". Jeszcze inny przykład: wiele kompilatorów posiada dodatkową funkcję alloca(), która alokuje blok pamięci ze stosu a nie ze sterty. Naturalnie blok ten jest automatycznie dealokowany po opuszczeniu funkcji, co eliminuje konieczność jawnego kasowania poszczególnych obiektów operatorem delete. Ktoś może zatem użyć funkcji alloca(), aby ta "dostarczyła" dla puli odpowiednio dużą ilość pamięci. Następnie wszystkie obiekty alokowane przy użyciu tej puli będą się zachowywać jak zmienne lokalne: automatycznie "znikną" po opuszczeniu funkcji. Oczywiście w ten sposób nie zostaną wywołane destruktory tych obiektów; jeśli destruktory te miały wykonywać jakieś ważne czynności, nie będziesz mógł (mogła) zastosować tej techniki, ale w niektórych przypadkach, gdy destruktory jedynie dealokują pamięć, technika "pul pamięci" może się okazać przydatna.

OK, zakładając, że przetrwałeś(-aś) od 6 do 8 linii kodu potrzebnych do zamiany Twojej funkcji-alokatora w metodę klasy Pool, przystępujemy do następnego kroku - zmiany składni służącej do alokowania obiektów. Celem jest przejście z raczej niewygodnego zapisu new(pool.alloc(sizeof(Foo))) Foo() do prostszej składni new(pool) Foo(). Aby urzeczywistnić ten plan, potrzebujemy dodać jeszcze dwie linijki kodu tuż pod definicją Twojej klasy Pool:

 inline void* operator new(size_t nbytes, Pool& pool)
 {
   return pool.alloc(nbytes);
 }

Teraz kompilator, za każdym razem gdy "zobaczy" new(pool) Foo(), wstawi w to miejsce powyższą funkcję i przekaże jej jako parametry sizeof(Foo) i pool. Jedynym miejscem, w którym będzie występował śmieszny zapis pool.alloc(nbytes), jest Twój nowy operator new.

Przejdźmy teraz do kwestii niszczenia/dealokacji obiektów Foo. Przypomnijmy sobie na czym polega "metoda siłowa" stosowana czasami w "placement new" - mianowicie na jawnym wywołaniu destruktora a następnie na jawnej dealokacji pamięci zajmowanej przez obiekt:

 void sample(Pool& pool)
 {
   Foo* p = new(pool) Foo();
   ...
   p->~Foo();  
// jawne wywołanie destruktora
   pool.dealloc(p);  
// jawna dealokacja pamięci
 }

Metoda ta stwarza kilka problemów, ale wszystkie z nich da się rozwiązać:

  1. Jeżeli konstruktor Foo::Foo() wyrzuci wyjątek to nastąpi wyciek pamięci.
  2. Składnia służąca do destrukcji i dealokacji różni się od tej, do której większość programistów zdążyła przywyknąć - stosując ją prawdopodobnie łatwiej będzie coś niechcący pochrzanić.
  3. Użytkownicy są zmuszeni do zapamiętywania który obiekt należy do której puli. Ponieważ kod alokujący i kod dealokujący znajdują się często w dwóch różnych funkcjach, programista musi przekazywać dwa wskaźniki (Foo* i Pool*), co dosyć szybko staje się obrzydzające (na przykład, co zrobić jeśli mamy do czynienia z tablicą obiektów Foo, z których każdy może pochodzić z innej puli?; bleeee).

Rozwiążemy te problemy po kolei:

Problem #1: zapobieganie wyciekom pamięci. Kiedy używasz "zwykłego" operatora new, np. Foo* p = new Foo(), kompilator wstawia w to miejsce specjalny kod obsługujący sytuacje, gdy konstruktor wyrzuci wyjątek. Kod generowany przez kompilator funkcjonalnie przypomina poniższy:

 // Funkcjonalny odpowiednik kodu zastępującego Foo* p = new Foo()
 
 Foo* p;
 
 
// nie przechwytuj wyjątkow zgłaszanych przez alokator
 void* raw = operator new(sizeof(Foo));
 
 
// przechwyć wszystkie wyjątki zgłoszone przez konstruktor
 try {
   p = new(raw) Foo();  
// wywołaj konstruktor z this = raw
 }
 catch (...) {
   
// oops, konstruktor zgłosił wyjątek
   operator delete(raw);
   throw;  
// przekaż wyjątek dalej
 }

Wniosek jest taki, że kompilator dealokuje obszar przeznaczony na obiekt, jeśli konstruktor zgłosi wyjątek. Ale w przypadku "new z parametrem" (często nazywanego "placement new"), kompilator nie będzie wiedział co zrobić, jeśli zostanie zgłoszony wyjątek, tak więc w tej sytuacji nie zrobi on nic.

 // Funkcjonalny odpowiednik Foo* p = new(pool) Foo():
 
 Foo* p;
 void* raw = operator new(sizeof(Foo), pool);
 
// powyższa funkcja po prostu wywołuje "pool.alloc(sizeof(Foo))"
 p = new(raw) Foo();
 
// jeśli w powyższej linijce wystąpi wyjątek, to
 
// pool.dealloc(raw) NIE zostanie wywołana

Wobec tego trzeba zmusić kompilator, aby ten zrobił coś podobnego jak w przypadku globalnej wersji operatora new. Na szczęście jest to proste: kiedy kompilator trafia na instrukcję new(pool) Foo(), rozpoczyna poszukiwania odpowiadającej jej wersji operatora delete. Jeżeli znajdzie taką, w miejsce instrukcji z operatoreem new wstawia kod z konstruktorem umieszczonym w bloku try, analogicznie jak w podanym wyżej przykładzie ze "zwykłym new". Zatem wystarczy zdefiniować operator delete według poniższego kodu (ostrożnie; jeśli drugi parametr operatora delete będzie miał inny typ niż w operator new(size_t, Pool&), kompilator nie będzie z tego powodu narzekał; po prostu nie wstawi on bloku try gdy użytkownik napisze new(pool) Foo()):

 void operator delete(void* p, Pool& pool)
 {
   pool.dealloc(p);
 }

Teraz kompilator będzie automatycznie umieszczał wywołanie konstruktora w bloku try w miejscu instrukcji z operatorem new:

 // Funkcjonalny odpowiednik Foo* p = new(pool) Foo()
 
 Foo* p;
 
 
// nie przechwytuj wyjątków zgłoszonych przez alokator
 void* raw = operator new(sizeof(Foo), pool);
 
// powyższa instrukcja po prostu wywoła "pool.alloc(sizeof(Foo))"
 
 
// przechwyć wszystkie wyjątki zgłaszane przez konstruktor
 try {
   p = new(raw) Foo();  
// call the ctor with raw as this
 }
 catch (...) {
   
// oops, konstruktor zgłosił wyjątek
   operator delete(raw, pool);  
// to jest ta magiczna linijka!!
   throw;  
// przekaż wyjątek dalej
 }

Innymi słowy, zdefiniowanie jednoliniowej funkcji operator delete(void* p, Pool& pool) zmusiło kompilator do automagicznego zatykania wycieku pamięci. Oczywiście funkcja ta może, choć nie musi, być inline.

Problemy #2 ("niewygodna, więc przyciągająca błędy składnia") i #3 ("użytkownicy muszą ręcznie wiązać obiekty ze wskaźnikami do pól, w których były one alokowane, co może stać się przyczyną powstawania błędów w programie") można rozwiązać jednocześnie wstawiając dodatkowe 10-20 linijek kodu w jednym miejscu. Innymi słowy, umieścimy 10-20 linii kodu w jednym miejscu (w pliku nagłówkowym z deklaracją obiektu Pool) i dzięki temu uprościmy kod w wielu innych miejscach (w każdym kawałku kodu korzystającym z Twojej klasy Pool).

Pomysł polega na ukrytym powiązaniu wskaźnika Pool* z każdym alokowanym obszarem. Wskaźnikiem Pool* powiązanym z globalnym alokatorem [nie wykorzystującym puli - przyp. tłum.] mógłby być NULL; chodzi o to żeby przynajmniej koncepcyjnie każda alokacja była powiązana z jakąś pulą poprzez wskaźnik Pool*. Wtedy możesz zastąpić globalny operator delete funkcją, która sprawdzałaby z którą pulą Pool* jest powiązany usuwany obszar - jeśli pulą tą nie byłby NULL to obszar dealokowany by był metodą klasy Pool. Przykładowo, jeśli(!) "normalny" dealokator używałby funkcji free(), nasz nowy operator delete mógłby wyglądać mniej więcej tak:

 void operator delete(void* p)
 {
   if (p != NULL) {
     Pool* pool = <wskaźnik do puli powiązanej z obszarem "p">;
     if (pool == NULL)
       free(p);
     else
       pool->dealloc(p);
   }
 }

Jeżeli nie masz pewności czy normalnym dealokatorem jest free(), najprostszym rozwiązaniem jest wymiana operatora new na funkcję, która do alokacji używałaby malloc(). Funkcja ta mogłaby wyglądać tak jak poniżej (uwaga: ta definicja nie uwzględnia kilku szczegółów takich jak pętla new_handler i throw std::bad_alloc() w przypadku braku pamięci):

 void* operator new(size_t nbytes)
 {
   if (nbytes == 0)
     nbytes = 1;  
// dzięki temu każda alokacja dostanie inny adres
   void* raw = malloc(nbytes);
   <jakoś powiąż 'Pool* = NULL' z 'raw'>
   return raw;
 }

Jedynym problemem, jaki w tym momencie pozostał, jest powiązanie wskaźnika Pool* z alokowanym obszarem. Jednym z możliwych rozwiązań, zastosowanym w conajmniej jednej komercyjnej aplikacji, jest użycie std::map<void*,Pool*>. Innymi słowy, chodzi o utworzenie tablicy przeglądowej (ang. "look-up table"), której kluczami byłyby wskaźniki do alokowanych obszarów zaś elementami wskaźniki Pool*. Z powodów które omówię za chwilę, para klucz/element nie powinna być dodawana do listy wewnątrz globalnego operatora new (np., nie wolno Ci napisać poolMap[p] = NULL w definicji globalnego operatora new). Powód jest następujący: niezastosowanie się do tej rady doprowadziłoby do powstania nieskończonej pętli — std::map prawdopodobnie wykorzystuje globalny operator new, stąd dodanie nowego elementu do listy spowodowałoby dodanie kolejnego elementu do listy itd., prowadząc do nieskończonej rekursji — bum, nie żyjesz.

Nawet uwzględniając fakt, że technika ta wymaga przeglądnięcia tablicy std::map przy każdej dealokacji, jej wydajność okazuje się akceptowalna, przynajmniej w wielu przypadkach.

Innym rozwiązaniem - szybszym ale mogącym zużywać więcej pamięci, a poza tym nieco skomplikowanym - jest umieszczenie wskaźnika Pool* tuż przed każdym alokowanym obszarem. Na przykład, jeśli nbytes byłoby równe 24, co oznaczałoby że użytkownik prosi o alokowanie 24 bajtów, moglibyśmy alokować 28 (lub 32, jeśli Twój komputer wymaga 8-bajtowego wyrównania dla danych typu double lub long long), umieścić wskaźnik do puli Pool* w pierwszych czterech bajtach alokowanego obszaru a następnie zwrócić adres o 4 (lub 8) bajtów większy od adresu tego co alokowaliśmy. Z kolei operator delete cofałby się o 4 (lub 8) bajtów, odczytywał wskaźnik Pool*, i jeśli byłby on równy NULL to dealokowałby obszar funkcją free(); natomiast gdyby wskaźnik nie był równy NULL to obszar dealokowany by był metodą pool->dealloc(). Parametrem przekazywanym do funkcji free() lub metody pool->dealloc() byłby wskaźnik powiększony o 4 (lub 8) bajtów w stosunku do oryginalnego parametru p. Jeśli(!) zdecydował(a)byś się na 4-bajtowe wyrównanie, Twój kod mógłby wyglądać mniej więcej tak (podobnie jak poprzednio pominięto obsługę braku pamięci):

 void* operator new(size_t nbytes)
 {
   if (nbytes == 0)    
// tak żeby każdy alokowany obszar 
     nbytes = 1;                    
// dostał inny adres
  
   void* ans = malloc(nbytes + 4);  
// alokuj 4 bajty więcej
   *(Pool**)ans = NULL;             
// w globalnym new używamy NULL
   return (char*)ans + 4;           
// nie pozwól użytkownikom podejrzeć Pool*
 }
 
 void* operator new(size_t nbytes, Pool& pool)
 {
   if (nbytes == 0)    
// tak żeby każdy alokowany obszar
     nbytes = 1;                    
// dostał inny adres
   void* ans = pool.alloc(nbytes + 4); 
// alokuj 4 bajty więcej
   *(Pool**)ans = &pool;            
// wstaw tu wskaźnik Pool*
   return (char*)ans + 4;           
// nie pozwól użytkownikom podejrzeć Pool*
 }
 
 void operator delete(void* p)
 {
   if (p != NULL) {
     p = (char*)p - 4;              
// cofnij się do wskaźnika Pool*
     Pool* pool = *(Pool**)p;
     if (pool == null)
       free(p);                     
// 4 bajty przed oryginalnym p
     else
       pool->dealloc(p);            
// 4 bajty przed oryginalnym p
   }
 }

Naturalnie ostatnie akapity tego faqa mają zastosowanie tylko jeżeli masz możliwość zmiany definicji globalnych operatorów new i delete. Nawet jeśli nie masz takiej możliwości, pierwsze trzy czwarte tego faqa wciąż pozostają aktualne.

GóraDółPoprzednia sekcjaNastępna sekcjaSzukaj w FAQ ]


E-Mail E-mail the author
C++ FAQ LiteSpis treściSkorowidzO autorze©Pobierz swoją własną kopię ]
Ostatnia aktualizacja Jun 17, 2002
Wersja polska: 0.1i Jul 12, 2004