Schlaue Zeiger (behind the scenes)
Wie funktioniert eigentlich der std::shared_ptr hinter den Kulissen? Viel ist
(zumindest im Prinzip) nicht dazu: ein wenig Operatorüberladung, ein wenig
Zeigergeschiebe und ein Referenzzähler.
Video
Code
#include <iostream>
template <typename T> class SmartPointer
{
struct Storage
{
T* mPointer;
unsigned int mCounter;
};
Storage *mStorage;
void release()
{
mStorage->mCounter--;
std::cerr << "SmartPointer freigegeben. Weitere Referenzen: " <<
mStorage->mCounter << std::endl;
if (mStorage->mCounter == 0) {
delete mStorage->mPointer;
delete mStorage;
}
}
public:
explicit SmartPointer(T *p)
: mStorage(new Storage)
{
mStorage->mPointer = p;
mStorage->mCounter = 1;
}
SmartPointer(SmartPointer const &other)
: mStorage(other.mStorage)
{
mStorage->mCounter++;
}
~SmartPointer()
{
release();
}
SmartPointer &operator=(SmartPointer const &other)
{
release();
mStorage = other.mStorage;
mStorage->mCounter++;
return *this;
}
T *operator->() const
{
return mStorage->mPointer;
}
unsigned int useCount() const
{
return mStorage->mCounter;
}
};
class Test
{
public:
~Test()
{
std::cerr << "Test zerstört\n";
}
void sayHello() const
{
std::cerr << "Hallo!\n";
}
};
int main()
{
SmartPointer<Test> t(new Test);
std::cerr << "Zähler: " << t.useCount() << std::endl;
t->sayHello();
SmartPointer<Test> t2(new Test);
std::cerr << "Zähler t2: " << t2.useCount() << std::endl;
t2 = t;
std::cerr << "Zähler t2 nach Zuweisung: " << t2.useCount() << std::endl;
std::cerr << "Zähler t nach Zuweisung: " << t.useCount() << std::endl;
}
Erklärung
Die SmartPointer-Klasse ruht auf drei Säulen: zum einen den C++-Templates als
Mittel zur Entwicklung generischer Datenstrukturen (in unserem Fall: zur
Anpassung an den Datentyp, auf den der Pointer zeigen soll), zum zweiten einer
eigenen Implementierung von Copy Constructor und Copy Assignment Operator und
zum dritten einer Überladung des operator->().
Die Hauptarbeit geschieht beim Kopieren eines SmartPointer-Objektes: die
eigentlichen Daten des Objektes liegen nicht in den Feldern selbst, sondern in
einer kleinen Struktur namens Storage. Diese speichert den Referenzzähler und
den eigentlich internen Pointer. Jede Kopie eines SmartPointers zeigt auf die
gleiche Instanz dieser Struktur. Auf diese Weise sind sich alle Kopien darüber
einig, worauf sie gerade zeigen und wieviele Referenzen es aktuell gibt. Alles,
was wir dazu tun müssen, ist, den Zähler in der Struktur bei jeder Kopie (im
Copy Constructor und Copy Assignment Operator) hochzählen und beim Zerstören
einer Kopie (im Destruktor) runterzählen. Irgendwann wird dieser Zähler dann 0
erreichen. Dann ist klar, dass soeben die letzte Kopie dieses SmartPointers
zerstört wird und wir den referenzierten Datenbereich freigeben können.
Um nun einen SmartPointer nutzen zu können wie die eingebauten Zeiger (nur
schlauer), überladen wir den operator->(). Dieser Operator ist in C++ recht
clever definiert: wenn wir einen Ausdruck t->x haben und t ist ein Objekt
einer Klasse, welche den operator->() implementiert, dann wird dieser Ausdruck
ersetzt durch (t.operator->())->x. Danach wird der gleiche Ersetzungsprozess
wieder versucht. Was auch immer der operator->() der Klasse von t
zurückliefert, wird wieder dieser Analyse unterzogen. Ist das wieder eine
Klasse, die den Operator implementiert, dann verlängert sich die Kette
entsprechend. Ist das ein klassischer Pointer, dann beginnt der Compiler mit dem
Lookup von x innerhalb des Objektes, auf dass dieser Pointer zeigt. So
implementieren wir hier das Verhalten eines Pointers: wir überladen den
Pfeil-Operator und lassen diesen den intern gespeicherten Pointer zurückliefern.
Da dieser vom Typ T * ist (T is unser Templateparameter. Nebenbei: T * ist
hier eine kleine Unsauberkeit, die es einem Nutzer der Klasse erlauben würde,
den referenzierten Speicherbereich zu löschen. Eigentlich müsste das T * const
sein. Mein Fehler.), kann der Compiler nun die Elemente der Klasse T
durchsuchen und nach dem schauen, was auch immer hinter -> stand. Der
SmartPointer verhält sich also wie ein klassischer Pointer.
Die Klasse ist stark vereinfacht. Das std::shared_ptr-Template kann noch
einiges mehr (vor allem im Bereich paralleler Zugriff auf den Pointer). Zur
Illustration der Prinzipien ist das aber hier hoffentlich ausreichend.