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
|
|
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.