Prosty garbage collector w C++ [PL]

Samodzielne zarządzanie pamięcią nie jest w ogólności zadaniem prostym. W pierwszej chwili można by uznać, że przecież nie tak trudno jest zapobiec powstawaniu wycieków (memory leaks) czy próbom wielokrotnego zwalniania zaalokowanych bloków pamięci. Jednak w praktyce trzymanie pamięci operacyjnej w ryzach nie jest wcale prostym zadaniem.

Wstęp

W tym artykule postaram się zająć tym problemem poprzez zaprezentowanie sposobu na implementację prostego mechanizmu odśmiecania pamięci (garbage collecting) w C++ – języku, który obecnie takiego mechanizmu nie posiada. Wcześniej przytoczę też kilka argumentów na rzecz stosowania technik automatycznego zarządzania pamięcią oraz opiszę kilka popularnych algorytmów odśmiecania z wyjaśnieniem, dlaczego niestety większość z nich nie może być zastosowana w tworzeniu własnego GC w C++. W podsumowaniu wskażę też na możliwe usprawnienia, jakie można dodać do pokazanej w artykule przykładowej implementacji.
Tekst jest przeznaczony dla osób dość dobrze orientujących się w meandrach języka C++, gdyż konieczna jest tu znajomość także bardziej zaawansowanych jego elementów: szablonów, przyjaźni, przeciążania operatorów i podstaw STL.

Typowe błędy związane z ręczną (de)alokacją pamięci

Konieczność samodzielnego administrowania alokacją i dealokacją pamięci jest zwykle tym trudniejsza, im bardziej złożony jest program, gra czy biblioteka, którą tworzymy. Wraz ze wzrostem rozmiaru projektu, zwiększa się prawdopodobieństwo popełnienia błędów związanych z zarządzaniem pamięci. Należą do nich między innymi:

  • Wycieki pamięci, czyli brak wystąpienia instrukcji niszczącej nieużywane już obiekty, takiej jak np. operator delete w C++ czy funkcja free w C. Bez tego faktycznie nieużywana porcja pamięci nie jest zwracana do systemu, przez co nie może być ponownie wykorzystana w kolejnych alokacjach. Skutkiem jest oczywiście stopniowe zmniejszanie się ilości pamięci dostępnej dla programu i systemu.
    Wycieki występujące w dużych ilościach mogą być uciążliwe dla aplikacji, ale na szczęście systemy operacyjne potrafią sobie z nimi . Po zakończeniu “cieknącego” procesu każdy porządny system potrafi bowiem bez problemu zwolnić całą pamięć, którą ten zaalokował, dzięki czemu wycieki nie wpływają na jej późniejszą ilość dostępną dla pozostałych programów. Jeśli jednak aplikacja działa przez dłuższy czas (np. w tle), systematyczne generowanie wycieków może poważnie wpłynąć na wydajność całego systemu.
  • Użycie zwolnionych obiektów jest być może poważniejszym rodzajem błędu – a z pewnością takim, który szybciej daje o sobie znać. Polega on na próbie dostępu do obiektu, który został już zniszczony, i co za tym idzie – którego pamięć została już zwolniona. Kończy się to zazwyczaj błędem odmowy dostępu (access violation), zwanym też błędem segmentacji (segmentation fault).
  • Wielokrotne niszczenie obiektów jest z kolei przeciwieństwem wycieków pamięci. O ile tam instrukcje alokacji nie są zawsze “domykane” instrukcjami zwalniania, o tyle tutaj tych drugich jest zbyt wiele. Błąd ten może mieć różne konsekwencje: od zupełnie żadnych, jeśli po zwolnieniu pamięci wskazywanej przez wskaźnik zerujemy go, po całkiem poważne, równoznaczne z opisanym wyżej dostępem do zniszczonych obiektów.

Zauważmy, że aby całkowicie zapobiec występowaniu powyższych błędów związanych z dynamiczną alokacją i zwalnianiem pamięci, musimy być w stanie jednoznacznie odpowiedzieć sobie na kilka pytań. W prostych programach jest to łatwe zadanie, lecz w bardziej skomplikowanych może być nie tyle trudne, co wręcz niemożliwe. W przypadku bibliotek programistycznych możliwość udzielenia satysfakcjonujących odpowiedzi często oznacza przerzucenie części obowiązków związanych z zarządzaniem pamięcią na końcowego użytkownika (który musi np. samemu zwalniać obiekty tworzone przez funkcje biblioteczne).
Rzeczona lista kontrolna problemów przedstawia się mniej więcej tak:

  • Czy każdy używany przez nas obiekt ma jasno określony czas życia, czyli moment w którym powinien zostać stworzony oraz zniszczony?
  • Czy każdy obiekt ma jednoznacznie określonego właściciela, który zajmuje się jego tworzeniem i niszczeniem?
  • Czy pozostałe fragmenty programu, mające dostęp do obiektu, nie próbują się do niego dostać zanim został on stworzony przez właściciela lub po tym, jak został on już usunięty?
  • Czy odpowiedzi na trzy powyższe pytania są pozytywne również w sytuacjach występowania błędów niezależnych od kodu, obsługiwanych na przykład poprzez wyjątki?

Głębsze zastanowienie się nad problemem nawet w przypadku aplikacji średnich rozmiarów może oznaczać spory wysiłek. Widać więc, że bezbłędne zarządzanie pamięcią operacyjną we własnym zakresie nie jest wcale taką prostą sprawą, jak można by przypuszczać.

Ręczne zarządzanie kontra odśmiecanie

Dlatego też od kilku ładnych lat – zwłaszcza od czasu dużego wzrostu popularności Javy i .NET – zwolenników zdobywają metody automatycznego zarządzania pamięcią. Oznacza to najczęściej użycie tzw. odśmiecacza pamięci (garbage collector, w skrócie GC) – specjalnego modułu, zajmującego się automatycznym wykrywaniem, które porcje pamięci są już nieużywane, i ich późniejszym zwalnianiem.
Zastosowanie GC zasadniczo eliminuje więc konieczność śledzenia czasu życia obiektów. Gdy używamy mechanizmu odśmiecania, obiektów nie trzeba już jawnie zwalniać, ponieważ zostaną one “automagicznie” usunięte wtedy, gdy nie są już potrzebne. Niekiedy zresztą w środowiskach używających GC ręczne zwalnianie obiektów jest wręcz utrudnione, niemożliwe albo tylko pozorne (obiekty są jedynie “oznaczone jako usunięte”). Jest tak dlatego, iż istnienie możliwości ręcznego zwalniania obiektów może negatywnie wpływać na efektywność lub skuteczność działania odśmiecacza.

Jakie więc korzyści płyną z wykorzystania garbage collectora i jak wygląda porównanie wad i zalet GC oraz ręcznego zarządzania pamięcią? Przedstawia je poniższa tabelka:

Ręczne zarządzanie Odśmiecanie
Zalety:

  • Niewielki lub wręcz zerowy narzut (czasowy i pamięciowy) na operacje na pamięci
  • Możliwość dokładnej kontroli czasu życia (a zwłaszcza niszczenia) obiektów
Zalety:

  • Prostota i wygoda użytkowania
  • Eliminacja większości przypadków, w których mogą występować wycieki pamięci
  • Zabezpieczenie przed wielokrotną dealokacją pamięci
Wady:

  • Możliwość powstawania wycieków pamięci
  • Konieczność ustalania relacji własności między obiektami
  • Możliwość powstawania błędów wielokrotnych prób zwalniania obiektów
Wady:

  • Dodatkowy narzut (na operacje wskaźnikowe lub jako osobne zadanie w tle)
  • Praktyczny brak kontroli nad momentem niszczenia obiektu

Widać więc, że odśmiecacz jest swego rodzaju transakcją wiązaną. W zamian za pewną część zasobów (czas procesora, ewentualnie dodatkowa pamięć) dostajemy do ręki narzędzie, które wyręcza w jednym z bardziej uciążliwych programistycznych zadań. Korzyści ze stosowania tego mechanizmu są zwykle – jak już wspomniałem – proporcjonalne do rozmiaru programu, więc nie we wszystkich przypadkach muszą się objawiać. W pewnym sensie można zatem uważać GC za mechanizm ułatwiający programowanie podobny chociażby do szablonów w C++: można się bez nich obyć, lecz ich wykorzystanie opłaca się w wielu, acz nie we wszystkich sytuacjach.

Sposoby odśmiecania pamięci

Skoro mamy wyposażyć C++ w autorski system odśmiecania pamięci, powinniśmy przyjrzeć się temu, jak taki mechanizm może działać. Bo niby skąd na przykład GC wie, które obiekty są rzeczywiście nieużywane i nadają się do bezpiecznego zwolnienia?… Sposób, w jaki odśmiecacz potrafi to stwierdzić łącznie z samym “sprzątaniem” to głownie kryteria, na podstawie których wyróżniamy trzy podstawowe metody implementacji GC.

Zliczanie referencji

W tej metodzie z każdym zaalokowanym kawałkiem pamięci związana jest dodatkowa informacja o liczbie odwołań do tego obiektu z innych miejsc w programie. Odwołaniem jest tu każdy wskaźnik lub referencja, które pozwalają na uzyskanie dostępu do obiektu. Obecność chociaż jednego takiego odwołania oznacza, że docelowy obiekt jest gdzieś potrzebny i że wobec tego nie można go jeszcze zwolnić. Jeśli zaś ów licznik spadnie w końcu do zera, obiekt będzie się kwalifikował do automatycznego usunięcia, czyli jego pamięć będzie mogła być odzyskana przez garbage collector.

Brzmi to całkiem prosto, jednak z takim sposobem implementacji odśmiecacza wiążą się dwa problemy. Pierwszym jest stały narzut na wszelkie operacje wskaźnikowe, który jest związany z utrzymaniem prawidłowej wartości licznika referencji dla każdego obiektu. Przykładowo, jeśli dokonujemy przypisania między dwoma zmiennymi typu wskaźnikowego w postaci A = B, to należy wykonać dwie dodatkowe czynności. Trzeba mianowicie zmniejszyć o jeden licznik referencji dla obiektu wskazywanego przez A (który po przypisaniu nie będzie już na niego wskazywał) oraz zwiększyć tenże licznik dla obiektu wskazywanego przez B. Oznacza to około dwukrotne wydłużenie czasu wykonywania operacji wskaźnikowych.
Drugim i znacznie poważniejszym problemem jest to, że zliczanie referencji nie zawsze działa. Problemem są referencje cykliczne: sytuacje, gdy jakiś obiekt odwołuje się do innego, a ten inny – bezpośrednio lub pośrednio – odwołuje się do tego pierwszego. Taka sytuacja nie musi zdarzyć się wyłącznie wtedy, gdy reprezentujemy w pamięci grafy, które mają cykle. Wystarczy chociażby zwykłe drzewo (np. najprostsze z nich – binarne), w którym każdy węzeł pamięta odwołania do swojego węzła nadrzędnego i jednocześnie do swoich węzłów podrzędnych. W takich przypadkach obiekty nawzajem “podbijają” swoje liczniki referencji i nie zostaną zwolnione w zwykły sposób, opisany wyżej.

Problem cykli w zliczaniu referencji
Przykład odwołania cyklicznego (liczby są wartościami liczników referencji)
[Źródło: http://www.antlr.org]

Istnieją oczywiście sposoby na pozbycie się tego mankamentu, ale niektóre z nich są na tyle skomplikowane, że niwelują największą zaletę GC opartego na zliczaniu referencji: prostotę. Za chwilę zobaczymy zresztą, że metoda ta ma z naszego punktu widzenia także inne, cenne zalety. Nie należy więc spisywać ją na straty :)

Zaznacz-i-zamieć

Innym wyjściem w implementacji odśmiecacza pamięci jest podejście do problemu w sposób bardziej bezpośredni. Ponieważ nieużywanymi obiektami są te, do których nie istnieją żadne odwołania “z zewnątrz”, można by po prostu od czasu do czasu wyszukiwać takie obiekty i zwalniać je. Tak właśnie działają tzw. śledzące odśmiecacze pamięci (tracing garbage collectors), a jednym z wariantów takiego podejścia jest metoda znana jako zaznacz-i-zamieć (mark-and-sweep).

Jej użycie polega na dwukrotnym przeglądnięciu sterty w celu znalezienia i usunięcia nieużywanych już obiektów. W pierwszym przebiegu garbage collector rozpoczyna od odwołań leżących na stosie i wśród zmiennych globalnych, a następnie “przechodzi” po kolejnych odwołaniach między obiektami. Zaznacza przy tym te obiekty, które zdążył już “odwiedzić”. W momencie, gdy nie ma już więcej ścieżek do przejścia, pierwsza faza jest zakończona. Nietrudno zauważyć, że jest to właściwie nic innego, jak dowolne przeszukiwanie grafu (wszerz lub wgłąb), gdyż w istocie obiekty można traktować właśnie jako wierzchołki, a odwołania między nimi jako krawędzie grafu.
W drugim etapie cała sterta jest już przeglądana w sposób liniowy. Wtedy to wszystkie obiekty, które nie zostały wcześniej zaznaczone, są delegowane do usunięcia. Są one bowiem nieosiągalne z żadnego miejsca w programie (gdyby było inaczej, zaznaczylibyśmy je), więc mogą być bezpiecznie zwolnione.

W przypadku GC śledzących odwołania tak, jak to opisałem powyżej, nie ma oczywiście problemu cyklicznych referencji. Jeśli taki cykl utraci połączenie z resztą obiektów, nie zostanie odwiedzony w pierwszej fazie algorytmu. Nie zostanie więc też oznaczony i składające się na niego obiekty zostaną zwolnione w fazie drugiej.
Co do wad, to łatwo zauważyć, że cały ten algorytm wymaga dość sporo czasu – potrzebne jest przecież dwukrotnie przebiegnięcie całej sterty, czyli wszystkich zaalokowanych obiektów. Co gorsza, w trakcie tej operacji jej stan nie może się zmienić, a to w praktyce oznacza konieczność zastopowania całego programu (zazwyczaj w niemożliwych do przewidzenia momentach). Na szczęście bardziej zaawansowane warianty tej techniki, tzw. inkrementacyjne (incremental mark-and-sweep) potrafią rozbić proces odśmiecania na wiele mniejszych części, z których każdy zajmuje się tylko jakimś fragmentem sterty. Program jest wtedy blokowany na znacznie krótszy okres czasu, zwykle niezauważalny dla użytkownika, przy zachowaniu takiej samej skuteczności odśmiecania.

Kopiowanie sterty

Trzecia metoda jest dość podobna do zaznaczania-i-zamiatania, ale opiera się na poświęceniu dodatkowej ilości pamięci kosztem przyspieszenia wykonywania operacji. Sterta jest tu bowiem dzielona na dwa obszary: aktywny i rezerwowy. W trakcie normalnej pracy programu pamięć alokowana jest z tego pierwszego obszaru. W momencie jego przepełnienia następuje proces odśmiecania.
Polega on na przeglądnięciu grafu odwołań między obiektami (podobnie jak w pierwszym etapie mark-and-sweep) z jednoczesnym przekopiowaniem wszystkich osiągalnych obiektów do drugiego, rezerwowego obszaru sterty. Te, które nie zostaną “uratowane” w wyniku tej operacji i pozostaną w pierwszym, nadają się do zwolnienia (gdyż, na identycznej zasadzie jak w zaznaczaniu-i-zamiataniu, nie są osiągalne z żadnego miejsca w programie). Po wykonaniu kopiowania role obu obszarów są zamieniane (dawny aktywny staje się rezerwowym i na odwrót) i program może kontynuować działanie.

Wyższość tej metody nad zaznaczaniem i zamiataniem polega na konieczności tylko jednokrotnego przeglądnięcia sterty. Nadal jednak wymagane jest, aby na czas przeprowadzania porządków program nie mógł wykonywać żadnych czynności, czyli był zastopowany. Dodatkowo, konieczność zmiany odwołań w trakcie kopiowania obiektów wymaga, by nie były one fizycznymi adresami w pamięci operacyjnej; konieczny jest więc dodatkowy poziom abstrakcji dla wskaźników/referencji.

Odśmiecanie w C++ i w innych językach

Jak widać, trzy opisane wyżej algorytmy odśmiecania pamięci spełniają wprawdzie swoje zadanie, ale mają też pewne dość nieprzyjemne wady. Decyzja o wykorzystaniu któregoś z nich w tworzeniu własnego garbage collectora może być więc trudna. Można by zatem dodatkowo spojrzeć na istniejące w praktyce rozwiązania, czyli odśmiecacze wbudowane w języki programowania typu Java czy platformę .NET.

W rzeczywistości prawdopodobnie najlepiej sprawdzają się rozwiązania mieszane i tutaj też tak jest. Otóż zwykle jest tak, że po jakimś czasie od rozpoczęcia działania program zaczyna generować stosunkowo niewiele “śmieci”, zatem ilość pamięci do odzyskania jest niewielka. Większość obiektów jest na swoich miejscach i istnieją do nich odwołania z głównej części aplikacji.
Dlatego też stosuje się mieszaną technikę polegają na użyciu algorytmu z kopiowaniem sterty w początkowej fazie programu oraz mark-and-sweep w późniejszym czasie, gdy jego działanie się ustabilizuje i zaczyna on generować tylko niewielkie ilości śmieci. Dzięki temu unikamy kosztownego kopiowania dużej liczby “poprawnych” obiektów w celu odzyskania niewielkich kawałków pamięci. Takie podejście jest stosowane w paru implementacjach maszyny wirtualnej Javy.

Co jednak z naszym ulubionym językiem, czyli C++? Tutaj spotyka nas dosyć przykra niespodzianka. Jak wiemy już, śledzące odśmiecacze pamięci polegają na przeglądaniu odwołań między obiektami. Zasadniczy problem w takim podejściu polega na tym, że nie jest to możliwe do wykonania bez dodatkowych informacji o strukturze kodu. W szczególności nieodzowny jest mechanizm refleksji (reflection), pozwalający pozyskiwać informacji o klasach i ich polach w trakcie działania programu. C++ domyślnie takiego mechanizmu nie posiada i chociaż jego implementacja jest możliwa, jest zawsze bardzo kłopotliwa.
Stąd też w naszym prostym garbage collectorze zastosujemy technikę, która nie wymaga takiej metawiedzy i pozwala na to, by kod odśmiecacza był takim samym kodem jak reszta programu, niekorzystającym z żadnych “magicznych sztuczek”. Taką techniką jest opisane na początku zliczanie referencji. Pasuje ono do C++ o wiele lepiej także dlatego, że w implementacji GC możemy zaprząc do pracy najbardziej użyteczne mechanizmy języka, jak szablony i przeciążanie operatorów.
Pozostaje oczywiście kwestia cyklicznych referencji. Nie jest to wbrew pozorom aż taki problem w typowych układach zależności między obiektami, gdyż relacje cykliczne w dobrych projektach występują nieczęsto. Tym niemniej trzeba jakoś na ten kłopot zaradzić; najprostszym wyjściem jest po prostu obowiązkowe wykonanie planowej dealokacji wszystkich obiektów na koniec działania programu. W podsumowaniu przedstawię też inne możliwe rozwiązanie, które można zastosować w tych szczególnych przypadkach, kiedy nasze obiekty są powiązane większą liczbą odwołań cyklicznych.

Własny garbage collector w C++

Po tym nieco przydługawym wstępie teoretyczno-zapoznawczym, pora w końcu przejść do rzeczy. Przypomnijmy jeszcze tylko, że naszym celem jest napisanie mechanizmu odśmiecania pamięci działającego w oparciu o zliczanie referencji do obiektów. Piszemy go w C++ i korzystamy jedynie ze standardowych mechanizmów, jakie ten język oferuje. Jak się za chwile okaże, dzięki jego elastyczności będzie to całkowicie wystarczające.

Nasz garbage collector będzie się składał z trzech części:

  1. Struktur przechowujących informacje o zaalokowanych blokach pamięci. Podstawową częścią każdej takiej struktury będzie oczywiście sławetny licznik referencji, który trzeba będzie zmieniać przy każdej operacji wskaźnikowej.
  2. Specjalnej klasy wskaźnika, dzięki której wszystkie niezbędne czynności będą mogły być przeprowadzane w sposób całkowicie przezroczysty składniowo. To rzecz jasna zasługa możliwości przeciążania operatorów w języku C++.
  3. Funkcji sprzątających, których wywołanie pozwoli na zadziałanie całej “magii” odśmiecacza – czyli zajmujących się usuwaniem nieużywanych już obiektów.

Ostatecznie napiszemy rzecz jasna każdą z tych trzech części, chociaż w trakcie będziemy dość często przeskakiwać między nimi. Będzie tak głównie ze względu na sposób, w jaki należy wykorzystać pewne mechanizmy języka C++, by otrzymać satysfakcjonujący nas rezultat.
Zaczniemy więc od tego, co dla późniejszego użytkownika-programisty będzie prawdopodobnie najważniejsze – od klasy wskaźnika, który będzie przechowywał odwołania obiektów mogących być sprzątanymi GC.

Co musi umieć wskaźnik?

Dlaczego potrzebujemy takiej klasy? Ano dlatego, że wbudowane język C++ wskaźniki (int*, float*, SomeClass*, itp.) nie mają natywnie żadnych mechanizmów kontrolnych i nijak nie możemy ich w takowe wyposażyć. I dlatego też je opakujemy, tworząc nowy rodzaj wskaźnika w postaci naszej własnej klasy. Aby móc skorzystać z dobrodziejstw odśmiecania pamięci, odwołania do obiektów trzeba będzie konstruować, posługując się właśnie tym specjalnym wskaźnikiem. Napiszemy go jednak tak, by niemal w niczym nie różniło się to od używania zwykłych wskaźników obecnych w C++.

Wskaźniki są sprytne

Rodzajów wskaźników jest wbrew pozorom bardzo dużo, a spora część z nich rości sobie prawo do… inteligencji. Istnieje bowiem wygodne pojęcie ‘inteligentnego’ lub ‘sprytnego wskaźnika’ (smart pointer) na określenie obiektu, który zachowuje się jak wskaźnik, ale przy okazji robi (lub może robić) pewne dodatkowe, “sprytne” czynności.
Jest możliwe, że miałeś już do czynienia z tego typu obiektami. Sama biblioteka standardowa C++ (czyli część języka) zawiera klasę std::auto_ptr, będącą właśnie takim sprytnym wskaźnikiem. Jego wartością dodaną jest tu fakt, że potrafi ona zwolnić pamięć, na którą pokazuje, w momencie wyjścia wskaźnika poza zakres:

  1. void Function()
  2. {
  3.     std::auto_ptr<Foo> pFoo(new Foo());   // tworzymy obiekt
  4.     pFoo->DoSomething();
  5.     // (zwolnienie obiektu Foo przy kończeniu funkcji)
  6. }

Na pierwszy rzut oka może to wydawać się bezużyteczne (bo przecież można “zwyczajnie” użyć delete). Trzeba jednak wziąć pod uwagę fakt, że wykonanie funkcji może w praktyce zakończyć się właściwie w każdej chwili, gdyż w kodzie C++ istnieje przecież możliwość wyrzucenia wyjątku. To zaś powoduje operację nadzwyczajną o nazwie odwijania stosu; gdybyśmy tutaj użyli zwykłego wskaźnika (Foo*) i delete, skończyłoby to się wyciekiem pamięci.

Nasz własny wskaźnik

Klasa, którą zaraz częściowo napiszemy, też będzie sprytnym wskaźnikiem, lecz o zgoła odmiennych właściwościach. Jej dodatkową funkcjonalnością – względem zwykłych wskaźników – będzie dbanie o właściwą wartość licznika referencji dla wskazywanego obiektu.
Oprócz tego musi on jednak zachowywać się dokładnie tak, jak każdy inny wskaźnik – w szczególności obsługiwać operatory * (dereferencja) i -> (wyłuskanie składnika). Dla uproszczenia pominiemy operator tablicowy [] oraz operatory arytmetyczne + i –. Założymy bowiem, że będziemy mieli do czynienia wyłącznie z pojedynczymi obiektami, a nie dynamicznie alokowanymi tablicami. Nie jest to specjalnie krzywdzące założenie, zwłaszcza że w ogromnej większości przypadków użycie standardowej klasy std::vector jest znacznie lepszym pomysłem niż samodzielne alokowanie tablic.

Spójrzmy więc na pierwszą wersję definicji naszej klasy wskaźnika:

  1. template <typename T> class Ptr
  2. {
  3.     private:
  4.         T* m_pPtr;   // adres docelowego miejsca w pamięci
  5.  
  6.     public:
  7.         // konstruktory i destruktor
  8.         Ptr(T* = 0);
  9.         Ptr(const Ptr&);
  10.         ~Ptr();
  11.  
  12.          // operatory przypisania
  13.          T* operator = (T*);
  14.          Ptr& operator = (const Ptr&);
  15.  
  16.          // operatory logiczne
  17.          operator bool () const { return m_pPtr != 0; }
  18.          bool operator ! () const { return !m_pPtr; }
  19.  
  20.          // operatory wskaźnikowe
  21.          T& operator * () { return *m_pPtr; }
  22.          const T& operator * () const { return *m_pPtr; }
  23.          T* operator -> () { return m_pPtr; }
  24.  
  25.          // operator konwersji na natywny wskaźnik
  26.          operator T* () { return m_pPtr; }
  27.          operator const T* () const { return m_pPtr; }
  28.  
  29.          // pobranie licznika referencji
  30.          int GetRefCount() const;
  31. };

Jest to oczywiście szablon, aby można go było zastosować względem dowolnego typu docelowego. Mamy tu wbudowany wskaźnik, który opakowujemy (pole m_pPtr), deklaracje odpowiednich konstruktorów oraz operatorów. Chcemy bowiem zapewnić, aby obiekt klasy Ptr zachowywał się jak wskaźnik, stąd definicje operatorów dereferencji (*) i wyłuskania (-<), których zazwyczaj nie przeciąża się dla zwykłych obiektów.
Poza operatorami klasa właściwie nie ma interfejsu, chociaż dla formalności dodajemy jeszcze metodę GetRefCount, pozwalającą podejrzeć licznik referencji dla obiektu, na który pokazuje aktualnie nasz wskaźnik. Nie da się jednak ukryć, że aktualnie nie mamy takiego licznika, co zresztą uniemożliwia nam napisanie implementacji większości metod klasy Ptr. Z tego też powodu zajmiemy się teraz kwestią przechowywania informacji o utworzonych obiektach, co obejmuje między innymi ów wielce ważny licznik referencji.

Informacje o zaalokowanych blokach

Dane o pojedynczym bloku pamięci będziemy przechowywali w specjalnej, acz bardzo prostej strukturze o nazwie BlockInfo:

  1. template <typename T> struct BlockInfo
  2. {
  3.     const T* Ptr;   // wskaźnik na zaalokowany blok pamięci
  4.     int RefCount;   // licznik referencji
  5.  
  6.     // konstruktor
  7.     explicit BlockInfo(T* ptr) : Ptr(ptr), RefCount(1) { }
  8. };

Wystarczy nam jedynie adres bloku (czyli wskaźnik) oraz, oczywiście, licznik odwołań do niego. Inicjujemy go jedynką, gdyż zakładamy, że powyższa struktura będzie tworzona w momencie kreowania jakiegoś obiektu klasy Ptr. A skoro tak, to właśnie ten wskaźnik typu Ptr będzie pierwszym odwołaniem na blok pamięci, który opisuje struktura BlockInfo. Struktura ta będzie istniała, dopóki licznik referencji nie spadnie do zera i docelowy kawałek pamięci nie zostanie odzyskany przez funkcję odśmiecającą (którą napiszemy za chwilę).

OK, ale gdzie będziemy te wszystkie struktury przechowywać?… Potrzebujemy do tego jakiejś “globalnej” kolekcji, w której przechowamy wszystkie struktury BlockInfo z informacjami o zarządzanych przez garbage collector obiektach typu T.
“A dlaczego nie o wszystkich obiektach wszystkich typów?”, można rzecz jasna spytać. Otóż zarówno klasa Ptr, jak i BlockInfo, są szablonami przynajmniej z jednego bardzo konkretnego powodu. Nie możemy mianowicie zatracić informacji o docelowym typie danych, bowiem chcemy, by składniowo nasze wskaźniki (współpracujące z GC) były prawie zupełnie przezroczyste. To zaś wymaga kontroli typów w trakcie kompilacji, gdyż w języku C++ jest ona dokonywana także dla natywnych wskaźników. Stąd też musimy rozpatrywać każdy typ osobno, ale jednocześnie nie wiemy dokładnie, z iloma i jakimi typami docelowymi będziemy mieli ostatecznie do czynienia w naszym odśmiecaczu. Zależy to w końcu całkowicie od programisty, który będzie z niego później korzystał.
To wszystko sprawia, że całe zadanie jest idealne dla szablonów i dlatego jak dotąd nasze klasy i funkcję są wszystkie szablonowe.

Teraz natomiast potrzebujemy jednego pojemnika na wszystkie struktury BlockInfo<T>, przechowujące informacje o zarządzanych przez GC obiektach typu T. Jako że jest to szczegół implementacji odśmiecacza, nie chcemy, by kontener ten był widoczny na zewnątrz. Możemy też podejrzewać, że będzie on nam potrzebny jedynie w klasie Ptr, gdyż jest to jedyna “aktywna” część naszego garbage collectora i jak na razie nie planujemy dodawać kolejnych.
Biorąc to wszystko pod uwagę, tworzymy kolekcję struktur BlockInfo jako prywatną składową statyczną klasy Ptr:

  1. #include <map>
  2.  
  3. template <typename T> class Ptr
  4. {
  5.     private:
  6.         // typ kolekcji z informacjami o zaalokowanych blokach
  7.         typedef std::map<T*, BlockInfo<T>*> Blocks;
  8.  
  9.         // rzeczona kolekcja
  10.         static Blocks ms_Blocks;
  11.  
  12.         // informacje o bloku pamięci, na który pokazuje wskaźnik
  13.         BlockInfo<T>* m_pInfo
  14.  
  15.     public:
  16.         // zwraca aktualną wartość licznika referencji
  17.         int GetRefCount() const { return m_pInfo ? m_pInfo->RefCount : 0; }
  18.     // (reszta klasy)
  19. };
  20. template <typename T> Ptr<T>::Blocks Ptr<T>::ms_Blocks;

Używamy tutaj mapy z STL, jako że będziemy za chwilę potrzebowali operacji wyszukiwania informacji o bloku pamięci na podstawie wskaźnika do niego. Jednocześnie dodajemy jeszcze drugie niestatyczne pole do naszego wskaźnika: odwołanie do struktury BlockInfo, opisującej porcję pamięci, na którą aktualnie nasz wskaźnik pokazuje.

Implementacja operacji wskaźnikowych

Teraz możemy już dopisać definicje zaprezentowanych wcześniej metod klasy Ptr – konstruktorów, destruktora oraz operatora przypisania. Posłużymy się w tym celu pomocniczą metodą o nazwie GetBlockInfo:

  1. template <typename T> class Ptr
  2. {
  3.     private:
  4.         // tworzy lub zwraca istnejącą strukturę BlockInfo na podstawie wskaźnika
  5.         static BlockInfo<T>* GetBlockInfo(T* p)
  6.         {
  7.             // szukamy struktury w kolekcji
  8.             BlockInfo<T>* pBI;
  9.             Blocks::iterator it = ms_Blocks.find(p);
  10.  
  11.             // jeżeli nie znaleziono, tworzymy nową i dodajemy ją do kolekcji
  12.             if (it == ms_Blocks.end())
  13.                 ms_Blocks.insert (std::make_pair(p, pBI = new BlockInfo<T>(p)));
  14.             else
  15.             {
  16.                 // w przeciwnym razie zwiększamy licznik znalezionego bloku
  17.                 pBI = it->second;
  18.                 ++pBI->RefCount;
  19.             }
  20.  
  21.             return pBI;
  22.         }
  23.  
  24.     // (reszta klasy)
  25. };

Przeznaczeniem jej jest znalezienie informacji o bloku pamięci identyfikowanym poprzez podany wskaźnik p. W przypadku, gdy nie jest on jeszcze zarejestrowany w odśmiecaczu, tworzona jest dla niego nowa struktura BlockInfo. W przeciwnym wypadku używamy istniejącej, jednocześnie zwiększając jej licznik referencji.

Przy pomocy właśnie napisanej funkcji możemy bardzo łatwo zaimplementować najważniejszy konstruktor klasy Ptr, przyjmujący zwykły natywny wskaźnik:

  1. template <typename T> Ptr<T>::Ptr(T* p)
  2. {
  3.     m_pPtr = p; // zapamiętujemy wskaźnik
  4.     m_pInfo = p ? GetBlockInfo(p) : 0;  // pobieramy/tworzymy strukturę informacyjną
  5. }

Patrząc uważnym okiem można zauważyć, że w ten sposób dublujemy informacje. Docelowy adres jest bowiem dostępny zarówno bezpośrednio jako pole m_pPtr, jak i pośrednio poprzez m_pInfo->Ptr. Ten drugi sposób jest jednak wolniejszy (wymaga dodatkowej dereferencji) i dlatego adres zapisujemy też jako osobne pole. Dzięki temu możemy zminimalizować narzut na najczęstsze operacje wskaźnikowe (operatory * i ->).
Drugi konstruktor – kopiujący – też jest trywialny:

  1. template <typename T> Ptr<T>::Ptr(const Ptr<T>& ptr)
  2. {
  3.     // przepisujemy informacje
  4.     m_pPtr = ptr.m_pPtr;
  5.     m_pInfo = ptr.m_pInfo;
  6.  
  7.     // zwiększamy licznik referencji
  8.     if (m_pInfo) ++m_pInfo->RefCount;
  9. }

Przepisujemy po prostu oba pola, a następnie zwiększamy licznik referencji do bloku, jako że w wyniku kopiowania wskaźników zyskał on jedno odwołanie więcej.

Gdzie zaś licznik ten jest zmniejszany? Rzecz jasna w destruktorze, który jest wywoływany, gdy punkt wykonania programu wychodzi poza zasięg wskaźnika:

  1. template <typename T> Ptr<T>::~Ptr()
  2. {
  3.     if (m_pInfo && m_pInfo->RefCount > 0) --m_pInfo->RefCount;
  4. }

Z niezbędnych operacji wskaźnikowych, najbardziej skomplikowane i kosztowne są przypisania. Jeśli na przykład wykonujemy przypisanie obiektu klasy Ptr do innego obiektu klasy Ptr, musimy uważnie zadbać w sumie o dwa (prawdopodobnie) różne liczniki odwołań. Jeden musi być zmniejszony, a drugi – ten przypisywany – zwiększony:

  1. template <typename T> Ptr<T>& Ptr<T>::operator = (const Ptr<T>& ptr)
  2. {
  3.     // zmniejszamy licznik dla aktualnego bloku
  4.     if (m_pInfo) --m_pInfo->RefCount;
  5.  
  6.     // przepisujemy
  7.     m_pPtr = ptr.m_pPtr;
  8.     m_pInfo = ptr.m_pInfo;
  9.  
  10.     // zwiększamy licznik wskaźnika "przychodzącego"
  11.     if (m_pInfo) ++m_pInfo->RefCount;
  12.  
  13.     return *this;
  14. }

Nieco prościej jest w drugim przypadku – gdy do istniejącego wskaźnika typu Ptr przypisujemy wskaźnik natywny. Wtedy wykonujemy czynności podobne do tych, które przeprowadzamy w konstruktorze:

  1. template <typename T> T* Ptr<T>::operator = (T* p)
  2. {
  3.     // zmniejszamy licznik dla aktualnego bloku
  4.     if (m_pInfo) --m_pInfo->RefCount;
  5.  
  6.     // szukamy informacji o bloku pamięci lub tworzymy nową strukturę
  7.     m_pInfo = GetBlockInfo(p);
  8.  
  9.     // zapamiętujemy i zwracamy wskaźnik
  10.     return m_pPtr = p;
  11. }

I to już wszystko :) Klasa Ptr jest już kompletna jeśli chodzi o poprawne zarządzanie licznikami odwołań do obiektów, na które może pokazywać. Pozostaje jeszcze jedna drobna kwestia: piszemy garbage collector, który powinien automatycznie nieużywane obiekty zwalniać. Póki co potrafimy (z dokładnością do cykli) takie obiekty wykrywać, zatem teraz należałoby napisać kod, który zajmie się rzeczywistym odśmiecaniem.

Funkcja odzyskująca pamięć

Do sprzątania śmieci posłuży nam następująca funkcja Collect, zaimplementowana jako prywatna metoda statyczna klasy Ptr:

  1. template <typename T> class Ptr
  2. {
  3.     private:
  4.         // funkcja zwraca liczbę zwolnionych obiektów
  5.         static int Collect()
  6.         {
  7.             int Freed = 0;
  8.             BlockInfo<T> pBI;
  9.  
  10.             // przelatujemy po kolekcji zaalokowanych bloków tyle razy,
  11.             // aż za którymś przebiegiem nie zwolnimy żadnego obiektu
  12.             Blocks::iterator i;
  13.             do
  14.             {
  15.                 for (i = ms_Blocks.begin(); i != ms_Blocks.end(); ++i)
  16.                     // sprawdzamy, czy licznik referencji osiągnął zero
  17.                     if (!((pBI = i->second)->RefCount > 0))
  18.                     {
  19.                         // zwalniamy pamięć
  20.                         if (pBI->Ptr) { delete pBI->Ptr; ++Freed; }
  21.  
  22.                         // usuwamy strukturę z informacjami o obiekcie
  23.                         i = ms_Blocks.erase (i);
  24.                         delete pBI;
  25.  
  26.                         // rozpoczynamy przeglądanie od początku
  27.                         break;
  28.                     }
  29.             } while (i != ms_Blocks.end());
  30.  
  31.             return Freed;
  32.         }
  33. };

Mimo dość pokrętnej postaci, jej działanie jest w miarę proste. Przelatuje ona po prostu po wszystkich obiektach aż napotka na taki, którego licznik referencji spadł do zera. Wtedy zwalnia go, wyrzuca z kolekcji informacje o nim i rozpoczyna szukanie od początku.
Dlaczego to rozpoczynanie od nowa jest konieczne? Otóż usunięcie nieużywanego obiektu oznacza wywołanie jego destruktora. Ten zaś może potencjalnie zniszczyć jakieś inne odwołanie do innego obiektu, którego licznik dopiero po tej operacji osiągnie zero. Przeglądając kolekcję obiektów do skutku – aż niczego już nie można zwolnić – gwarantujemy sobie, że niczego nie pominiemy.

Łączymy w całość

Zobaczmy, co już udało nam się zrobić. Mamy gotową właściwie klasę Ptr, dzięki której może automagicznie i zupełnie przezroczyście odwoływać się do obiektów zarządzanych przez nasz garbage collector. Posiada on informacje o wszystkich obiektach danego typu, co pozwala też dokonać ich odśmiecania. Odpowiadającą za to funkcję szablonową też już mamy.
Pozostaje jeszcze jedna istotna kwestia. Obecnie nie mamy tak naprawdę jednego odśmiecacza pamięci, ale potencjalną możliwość posiadania bardzo wielu takich odśmiecaczy – po jednym dla każdego typu obiektów. Dzięki procesowi konkretyzacji klasy szablonowej Ptr, każdy taki typ będzie miał swoją własną, osobną kolekcję obiektów. Pojedyncza specjalizacja funkcji Collect będzie więc w stanie zwolnić pamięć zajmowaną przez obiekty tylko jednego typu.

Niezupełnie o to nam chodziło. Chcielibyśmy mieć przecież tylko jedną, globalną funkcję Collect, która posprząta nam całą nieużywaną pamięć – niezależnie od tego, z jakiego typu obiektami ma do czynienia. Czy możemy takową funkcję napisać?…
Odpowiedź jest na szczęście pozytywna :) Nie istnieje wprawdzie jakiś sposób na pobranie informacji o wszystkich specjalizacjach jakiegoś szablonu (tu: klasy Ptr), ale możemy poradzić sobie inaczej. Pomysł polega na tym, żeby pamiętać listę wszystkich specjalizacji funkcji statycznej Ptr<T>::Collect, dla wszystkich używanych typów T. Taka globalna lista była następnie przeglądana w ogólnej funkcji Collect, a jej elementy po kolei wywoływane celem posprzątania poszczególnych typów obiektów.

Brzmi sensownie? No to zrealizujmy ten pomysł. Zaczynamy od naszej listy:

  1. #include <list>
  2.  
  3. // typ wskaźnika na funkcję Ptr<T>::Collect
  4. typedef int (*CollectFunc)();
  5.  
  6. // typ listy ww. funkcji
  7. typedef std::list<CollectFunc> CollectFuncList;
  8.  
  9. // deklaracja zapowiadająca listy
  10. extern CollectFuncList g_CollectFunctions;
  11.  
  12. // definicja (w pliku .cpp)
  13. CollectFuncList g_CollectFunctions;

Zakładając chwilowo, że jest ona już wypełniona wskaźnikami do funkcji ‘szczegółowych’, możemy napisać ogólną wersję funkcji Collect, dostępną dla końcowego użytkownika naszego odśmiecacza:

  1. // funkcja odśmiecająca pamięć; zwraca liczbę zwolnionych obiektów
  2. int Collect()
  3. {
  4.     int Freed, TotalFreed = 0;
  5.  
  6.     // wywołujemy po kolei funkcje specjalizowane,
  7.     // aż za którymś razem nie zwolnimy nic
  8.     CollectFuncList::iterator i;
  9.     do
  10.     {
  11.         for (i = g_CollectFunctions.begin(); i != g_CollectFunctions.end(); ++i)
  12.            // wywołujemy funkcję i sprawdzamy, czy cokolwiek zwolniła
  13.            if ((Freed = (*i)()) > 0)
  14.            {
  15.                // tak - więc zliczamy to i zaczynamy od początku
  16.                TotalFreed += Freed;
  17.                break;
  18.            }
  19.     } while (i != g_CollectFunctions.end());
  20.  
  21.     return TotalFreed;
  22. }

Zasada jej działania jest w zasadzie identyczna jak wersji specjalizowanych. Wywołujemy po prostu po kolei funkcje z listy, a jeśli którejś uda się coś zwolnić, zaczynamy od początku. Uzasadnienie tej praktyki też jest bardzo podobne: zwolnienie jakichś obiektów skutkuje wywołaniem ich destruktorów, a te mogą zniszczyć odwołania do innych obiektów – także obiektów innych typów – i dlatego konieczny jest kolejny przebieg odśmiecania.

Musimy jeszcze tylko odpowiednio wypełniać listę g_CollectFunctions. Za każdym razem, gdy użyjemy w kodzie wskaźnika Ptr z jeszcze niewykorzystanym typem docelowym, powinniśmy zarejestrować skonkretyzowaną dla tego typu funkcję Ptr<T>::Collect. W tym celu najlepiej jest chyba użyć statycznej flagi logicznej informującej o tym, czy rejestracja dla danego typu została już przeprowadzona:

  1. template <typename T> class Ptr
  2. {
  3.     private:
  4.         static bool ms_Initialized;
  5.  
  6.     // (reszta klasy)
  7. };
  8. template <typename T> bool Ptr<T>::ms_Initialized = false;

Samej inicjalizacji dokonywać będziemy w zwykłym konstruktorze, który teraz będzie wyglądał następująco:

  1. template <typename T> Ptr<T>::Ptr(T* p)
  2. {
  3.     // wykonujemy czynności inicjalizacyjne, jeśli trzeba
  4.     if (!ms_Initialized) { g_CollectFunctions.push_back (&Ptr<T>::Collect); ms_Initialized = true; }
  5.     m_pPtr = p; // zapamiętujemy wskaźnik
  6.     m_pInfo = p ? GetBlockInfo(p) : 0;  // pobieramy/tworzymy strukturę informacyjną
  7. }

I w ten prosty sposób wyposażyliśmy się w ogólną funkcję sprzątającą. Pełen sukces!

Przykład

Na koniec czas na prosty przykład. Ponieważ nie jesteśmy wybredni, wystarczy nam sprzątanie typów podstawowych. Spróbujmy po prostu zaalokować dużą ilość takich obiektów, za każdym razem “osieracając” odwołania do nich. Następnie sprawdzamy, ile z nich zostało sprzątniętych przez GC.
Oto kod tego prostego testu:
#include
using namespace std;

#include “GC.hpp”

int main()
{
int n;
cout << "Input number of ints to allocate (pref. >10000): “;
cin >> n;

// bałaganimy
cout << "Allocating..." << endl; { Ptr p;
for (int i = 0; i < n; ++i) p = new int(i); } cout << "Allocated." << endl; // sprzątamy cout << "Collecting..." << endl; int Freed = GC::Collect(); cout << Freed << " ints collected." << endl; }[/cpp] Kilka uruchomień powinno potwierdzić, że nasz odśmiecacz faktycznie radzi sobie z porządkowaniem pamięciowych nieczystości.

Podsumowanie

Uff, udało się :) Wykonaliśmy właśnie kawał całkiem dobrej roboty, pisząc ten prosty garbage collector w C++. Przy okazji udało mi się, mam nadzieję, powiedzieć co nieco o samym mechanizmie odśmiecania, jego zaletach i wadach. Ale naprawdę zadziwiające w tym wszystkim jest to, że tak zdawałoby się fundamentalną sprawę jak obecność lub nieobecność GC możemy w C++ zmienić, wykorzystując do tego jedynie najzupełniej standardowe i przenośne możliwości języka! To chyba jak najlepiej świadczy o jego elastyczności i funkcjonalności, nawet jeśli czasami zdarza nam się na ten język narzekać :)

Możliwe usprawnienia i optymalizacje

Przedstawiona implementacja nie jest rzecz jasna doskonała i nawet nie próbuje aspirować do tego miana. Jest tu jeszcze bardzo dużo do poprawienia. Oto kilka propozycji:

  • Newralgicznym wydajnościowo punktem całego systemu jest wyszukiwanie informacji o blokach pamięci na podstawie wskaźnika. Do przechowywania kolekcji tych informacji użyłem tutaj zwykłej STL-owej mapy, jednak prawdopodobnie o wiele lepszym wyborem jest tablica mieszająca (hash table). Jest tak dlatego, że adresy alokowanych kawałków pamięci same w sobie są w dużym stopniu losowe, więc mają potencjał generowania równomiernego rozkładu kluczy. Z praktycznego punktu widzenia wyszukiwanie w takiej tablicy może więc mieć czas stały (zamiast logarytmicznego dla zwykłej mapy).
  • Optymalizacji można też poddać funkcje Ptr<T>::Collect. W tej chwili po zwolnieniu każdego obiektu rozpoczyna ona od nowa wyszukiwanie w kolekcji bloków. Przy ich niekorzystnym układzie może to prowadzić do pesymistycznej złożoności całej operacji równej O(n2). Możemy temu zaradzić, przeglądając za każdym całą kolekcję obiektów i po prostu licząc te zwalniane, by zakończyć funkcję wtedy, gdy nie uda nam się już zwolnić żadnego. Nieco innym wyjściem jest inna organizacja całej kolekcji: taka, w której informację są sortowane według liczników referencji. Wówczas te obiekty, których liczniki mają małą wartość i większą szansę na osiągnięcie zera po zwolnieniu innych obiektów, znajdowałyby się na początku. Kolejne przebiegi odśmiecania mogłyby więc szybciej je znaleźć i usunąć, jeśli to możliwe.
  • W bieżącej wersji funkcja Collect (w wersji ogólnej) zwraca sumaryczną liczbę wszystkich zwolnionych przez GC obiektów. Nie jest to specjalnie praktyczna informacja; lepszy byłoby całkowity rozmiar tychże obiektów w bajtach, dzięki czemu otrzymalibyśmy rozmiar odzyskanej w wyniku odśmiecania pamięci. Aby móc zwracać taki rezultat, funkcja Collect musiałaby znać rozmiar każdego z typów obiektów podlegających odśmiecaniu. Rozmiary te można zapamiętywać w bardzo podobny sposób jak wskaźniki do funkcji statycznych Ptr<T>::Collect.
  • Obecnie wskaźniki Ptr mają pewną wadę: nie obsługują niejawnych konwersji oferowanych przez natywne wskaźniki w C++. Jeśli przykładowo klasa B dziedziczy po klasie A, to wskaźnik typu B* daje się niejawnie przerzutować na A* (jest to powszechnie znane ‘rzutowanie w górę’). Tego samego nie można jednak powiedzieć o Ptr<B> i Ptr<A>. Dodanie takiej możliwości wymaga, by struktury BlockInfo mogły się znajdować w więcej niż jednej kolekcji ms_Blocks i jest zasadniczo dość skomplikowane – ale prawdopodobnie jak najbardziej wykonalne :)
  • Należałoby w końcu jakoś zaradzić problemowi cyklicznych referencji. Popularnym rozwiązaniem jest wprowadzenie tzw. słabych odwołań (weak references) – czyli takich referencji, których istnienie nie wpływa na postrzeganie obiektu jako używanego lub nie. W naszym przypadku tworzenie lub usuwanie takich odwołań nie zmieniałoby po prostu licznika referencji do obiektu, na który one pokazują. Żeby było zabawniej, przy założeniu środowiska jednowątkowego nie musimy nawet w żaden sposób implementować takiego mechanizmu, gdyż z naszego punktu widzenia zwykłe natywne wskaźniki C++ działają właśnie jak słabe referencje. Możemy więc w całkiem prosty sposób rozwiązać problem cyklicznych odwołań. (W praktyce dla celów przejrzystości kodu wypadałoby jednak napisać klasę WeakPtr<T>, będącą prostym opakowaniem na typ T*, i używać jej zamiast “gołego” wskaźnika).
  • Można w końcu nie odkładać zwalniania pamięci w bliżej nieokreśloną przyszłość, a robić to natychmiast, gdy licznik jakiegoś obiektu spadnie do zera. Działanie odśmiecacza i całego programu może być wówczas płynniejsze – zwłaszcza jeśli nie monitorujemy ilości zużywanej pamięci i nie możemy sobie pozwolić na okresowe przerwy spowodowane zwalnianiem dużej ilości nieużywanych obiektów.
 


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