Reference Counting

Martin Kompf

Reference Counting verbindet den Vorteil der automatischen Freigabe von Objektressourcen mit der beliebigen Kopierbarkeit des Objektes.

Der Crash ist vorprogrammiert

Im Artikel Zeigerautomatik mit auto_ptr wurde eine Methode zur automatischen Freigabe der von einem Objekt beanspruchten (Speicher-)Ressourcen unter Zuhilfenahme des STL Templates auto_ptr vorgestellt. Dieses Verfahren trifft dann auf seine Grenzen, wenn das per auto_ptr verwaltete Objekt kopiert werden soll, zum Beispiel bei Übergabe per Value an eine Funktion. Die Implementierung des Templates auto_ptr legt dann nämlich keine echte Kopie an, sondern transferiert lediglich das Besitzverhältnis für das verwaltete Objekt auf die neue Kopie. Diese ist dann für das Zerstören des Objektes beim Verlassen des Gültigkeitsbereiches zuständig.

Sehen wir uns dazu das folgende Testprogramm an (die Definition der Klasse Buffer steht in Zeigerautomatik mit auto_ptr):

#include <memory>
#include <exception>
#include <iostream>

using namespace std;

void doit( auto_ptr<Buffer> p)
{
    p -> transcode(1);
}

int main()
{
    try {
        auto_ptr<Buffer> b (new Buffer());
        // ...
        doit( b);
        b -> transcode(2);
        // ...
    }
    catch (const exception& e) {
        cerr << "Program exception\n";
        exit(1);
    }
}

Neu an diesem Programm ist, dass das auto_ptr Objekt b per Value an die Funktion doit() übergeben wird. Dort wird eine lokale Kopie p angelegt, die gemäß dem oben gesagten nun für die Zerstörung des Bufferobjektes verantwortlich ist. Dies passiert dann auch beim Verlassen von doit(). Leider nur wird die ursprüngliche Instanz b nochmals im Hauptprogramm beim Aufruf von b -> transcode(2) verwendet. Jetzt ist aber das Bufferobjekt schon zerstört! Ein Zugriff auf dieses wird unter Umständen fatale Folgen haben. Lassen wir das Programm einmal ablaufen:

a new Buffer has been constructed
1 transcoded
the Buffer was destroyed
Speicherzugriffsfehler

Der Versuch, die Funktion transcode() für einen zerstörten Buffer aufzurufen, quittiert das Betriebssystem (hier Linux 2.2.13) mit der lakonischen Meldung "Speicherzugriffsfehler" und bricht die Programmausführung ab.

Abzählen hift

Ein Ausweg aus diesem Dilemma könnte sein, das Template so zu erweitern, dass es mitzählt, wie oft eine Kopie angelegt wird. Beim Zerstören einer Kopie wird der Zähler dann wieder dekrementiert. Erst wenn dieser Zähler auf Null geht, das heisst wenn die letzte Kopie zerstört wurde, kann das verwaltete Objekt ebenfalls zerstört werden. Diese Technik bezeichnet man als Reference Counting.

Eine mögliche (einfachste) Implementierung dieses Algorithmus zeigt das Template refcnt_ptr (Download refcnt_ptr.h):

template <class T> class refcnt_ptr {
protected:
    // a helper class that holds the pointer to the managed object
    // and its reference count
    class Holder {
    public:
        Holder( T* ptr) : ptr_(ptr), count_(1) {};
        ~Holder() { delete ptr_;};
        
        T* ptr_;
        unsigned count_;
    };
    
    Holder* h_;
            
public:
    // ctor of refcnt_ptr (p must not be NULL)
    explicit refcnt_ptr( T* p) : h_(new Holder(p)) {}
    // dtor of refcnt_ptr
    ~refcnt_ptr() { if (--h_->count_ == 0) delete h_; }
    // copy and assignment of refcnt_ptr
    refcnt_ptr (const refcnt_ptr<T>& right) : h_(right.h_) {
        ++h_->count_;
    }
    refcnt_ptr<T>& operator= (const refcnt_ptr<T>& right) {
        ++right.h_->count_;
        if (--h_->count_ == 0) delete h_;
        h_ = right.h_;
        return *this;
    }
    refcnt_ptr<T>& operator= (refcnt_ptr<T>& right) {
        ++right.h_->count_;
        if (--h_->count_ == 0) delete h_;
        h_ = right.h_;
        return *this;
    }
    // access to the managed object
    T* operator-> () { return h_->ptr_; }
    T& operator* ()  { return *h_->ptr_; }
};

Die Idee der Implementierung ist, einen Zeiger auf das zu verwaltende Objekt zusammen mit dem Referenzzähler in einer inneren Klasse namens Holder zu speichern. Die äußere Klasse refcnt_ptr stellt im wesentlichen Konstruktor, Destruktor, Copy Konstruktoren und Zuweisungsoperatoren zur Verfügung. Diese nehmen das eigentliche Reference Counting vor. Über die Operatoren * und -> schließlich erfolgt der Zugriff auf das verwaltete Objekt. Damit kann analog zu auto_ptr der Aufruf einer Memberfunktion des verwalteten Objektes direkt über den refcnt_ptr erfolgen.

Auf ein Neues

Das obige Beispiel kann nun unter Verwendung des refcnt_ptr Templates umgeschrieben werden:

#include <exception>
#include <iostream>

using namespace std;

void doit( refcnt_ptr<Buffer> p)
{
    p -> transcode(1);
}

int main()
{
    try {
        refcnt_ptr<Buffer> b (new Buffer());
        // ...
        doit( b);
        b -> transcode(2);
        // ...
    }
    catch (const exception& e) {
        cerr << "Program exception\n";
        exit(1);
    }
}

Bei der Ausführung des Programms zeigt sich anhand der Ausgabe die korrekte Funktion unseres neuen Templates:

a new Buffer has been constructed
Buffer transcoded (1)
Buffer transcoded (2)
the Buffer was destroyed

Dem Leser sei es überlassen, das Programm nun noch mit negativen Parametern der transcode() Aufrufe ablaufen zu lassen, um die korrekte Funktion auch im Falle von Exceptions zu testen.

Der Weisheit letzter Schluss?

Auch dieses Template kann wahrscheinlich nicht sämtliche denkbaren Situationen abhandeln, dürfte aber für die meisten praktischen Anwendungsfälle ausreichend sein. Auch wird die Übergabe eines NULL Pointers an das Template nicht abgefangen, einen entsprechenden Test sollte man noch einbauen. Der interessierte Anwender sei auf folgende Links verwiesen: