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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
#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.