Dieser Artikel wurde von peterchen erstellt.


Folgende Themen werden von diesem Artikel berührt:


Druckversion des Artikels


Inhalt

Smart Pointer können komplexe Anwendungen in C++ wesentlich vereinfachen. Sie bieten eine automatische Speicherverwaltung ähnlich derer in restriktiveren Sprachen (z.B. VB, C#), sind aber auch in anderen Situationen hilfreich.

Was sind Smart Pointer?

Ein Smart Pointer verhält sich wie ein Zeiger, gibt das referenzierte Objekt aber automatisch frei, wenn es nicht mehr benötigt wird.

"Nicht mehr benötigt" lässt sich in C++ schwer definieren. Daher gibt es verschiedene Smart Pointer-Implementationen, die auf die häufigsten Szenarien angepasst sind. Neben dem automatischen Freigeben sind natürlich noch andere Anwendungen möglich.

Smart Pointer-Implementationen findet man in vielen Bibliotheken - jede mit ihren eigenen Vorzügen und Problemen. Dieser Artikel verwendet die Smart Pointer der Boost-Bibliotheken, einer hochwertigen OpenSource-Bibliothekssammlung, von denen einige für die Einbindung in den nächsten C++-Standard vorgesehen sind.

Boost bietet die folgenden Implementationen:



Ganz einfach: boost::scoped_ptr<T>

scoped_ptr ist der einfachste Boost Smart Pointer. Er garantiert automatisches Löschen, wenn der Zeiger den Gültigkeitsbereich verlässt.

Hinweis zum Beispielcode:
Die Beispiele verwenden eine Hilfsklasse CSample, die für Konstruktion, Zuweisung usw. Meldungen ausgibt. Sicherlich ist es trotzdem interessant, die Beispiele unter dem Debugger nachzuvollziehen. Die Quellen enthalten die notwendigen Boost-Header (bitte "Boost installieren" weiter unten beachten!)


Beispiel 1 - mit normalen Zeigern
C++:
1
2
3
4
5
6
7
8
9
10
11
12
13
void Sample1_Plain()
{
  CSample * pSample(new CSample);
 
  if (!pSample->Query() ) // irgendeine Funktion...
  {
    delete pSample;
    return;
  }
 
  pSample->Use();
  delete pSample;
}


Beispiel 2 - mit scoped_ptr<T>
C++:
1
2
3
4
5
6
7
8
9
10
11
#include "boost/smart_ptr.h"
 
void Sample1_ScopedPtr()
{
  boost::scoped_ptr<CSample> samplePtr(new CSample);
 
  if (!samplePtr->Query() ) // irgendeine Funktion...
    return;    
 
  samplePtr->Use();
}


Einen normalen Zeiger muss man an allen Stellen freigeben, an denen man die Funktion verlässt. Das ist leicht zu übersehen, wenn man Exceptions verwendet. Ein scoped_ptr wie im zweiten Beispiel wird automatisch freigegeben - auch wenn CSample::Query eine Exception wirft!

Der Vorteil ist nicht zu übersehen: In einer komplexeren Funktion wird ein delete schnell vergessen - besonders, wenn man ein bisschen aufräumt.

scoped_ptr eignet sich für: Automatische Freigabe von lokalen Objekten und Klassendaten, verzögerte Initialisierunng, Implementierung von PIMPL und RAII
was geht nicht: Als Element in STL-Container, mehrere Zeiger auf das gleiche Objekt, andere Allokatoren als new/delete
Performance: Praktisch identisch mit normalem Zeiger

Tipp: Wer PIMPL (pointer to implementation, auch handle/body) und RAII (Resource Acquisition Is Initialization) nicht kennt, sollte ganz fix ein gutes C++-Buch zur Hand nehmen und diese wichtigen Konzepte nachholen. Smart Pointer sind nur eine (bequeme) Möglichkeit, diese zu implementieren.

scoped_ptr verbietet direkte implizite Zuweisungen:
C++:
scoped_ptr<CSample> ptrA(new CSample);
scoped_ptr<CSample> ptrB = ptrA; // Compile-Fehler!  
ptrA = new CSample;              // Compile-Fehler!

Das vermeidet die üblichen Fehler im Umgang mit Smart Pointern, die meistens am Übergang zwischen Smart Pointer und normalem Zeiger auftreten. Explizit darf man natürlich alles (und ist selbst schuld, wenn es schief geht):
C++:
T* scoped_ptr<T>::get()      // gibt den enthaltenen Zeiger zurück
scoped_ptr<T>::reset(T *)    // ersetzt den enthaltenen Zeiger mit einer neuen Instanz.
                             // die vorige Instanz wird dabei freigegeben  


Referenzzähler und shared_ptr<T>

Wenn man mehrere Smart Pointer auf das gleiche Objekt zulassen möchte, benötigt man ein anderes Kriterium für "nicht mehr benötigt".
Referenzgezählte Zeiger überwachen, wie viele Zeiger auf ein Objekt verweisen. Dazu wird jedem Objekt ein Zähler zugeordnet. Dieser wird bei einer Zeigerzuweisung erhöht, und im Zeigerdestruktor runtergezählt. Wenn der Zähler 0 erreicht, wird das Objekt gelöscht.

Boost bietet shared_ptr als referenzgezählten Zeiger und "kleinen Alleskönner". Zuerst ein kleines Beispiel:

C++:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void Sample2_Shared()
{
  // (A) Erzeugen einer CSample-Instanz mit einer Referenz
  boost::shared_ptr<CSample> p1(new CSample);
  printf("The Sample now has %i references\n", p1.use_count()); // use_count() == 1
 
  // (B) An zweiten Zeiger zuweisen:
  boost::shared_ptr<CSample> p2 = mySample;
  printf("The Sample now has %i references\n", p2.use_count()); // use_count() == 2
 
  // (C) den ersten Zeiger auf NULL setzen
  p1.reset();
  printf("The Sample now has %i references\n", p2.use_count());  // use_count() == 1
 
  // Das bei (A) allozierte Objekt wird freigegeben, wenn p2 den Gültigkeitsbereich verlässt
}

Zeile (A) erzeugt eine CSample-Instanz auf dem Heap und speichert einen Zeiger darauf in dem shared_ptr mySample. Das sieht dann so aus:



In (B) wird ein zweiter Zeiger auf das Objekt angelegt:



mySample wird mittels reset() auf NULL gesetzt (reset() ist identisch p=NULL für einen normalen Zeiger). CSample wird noch von p2 referenziert:



Erst wenn die letzte Referenz auf CSample verschwindet, wird CSample gelöscht:



Häufige Anwendungsfälle sind:


Beispiel für shared_ptr<T>: shared_ptr im STL-Container
Viele Container-Klassen (u.a. STl-Container) erfordern Kopieroperationen, z.B. wenn ein existierendes Element in eine Liste oder einen Vektor eingefügt werden soll. Das ist für komplexe Klassen ungünstig und für manche Klassen sogar unmöglich. In diesem Fall weicht man üblicherweise auf einen Container von Zeigern aus:

C++:
std::vector<CMyLargeClass *> vec;
vec.push_back( new CMyLargeClass("dicke fette Zeichenkette") );


Damit ist man aber als "Nutzer" wieder verantwortlich für die Speicherverwaltung. Mit einem shared_ptr wird es wieder einfacher:

C++:
typedef boost::shared_ptr<CMyLargeClass>  CMyLargeClassPtr;
std::vector<CMyLargeClassPtr> vec;
vec.push_back( CMyLargeClassPtr(new CMyLargeClass("bigString")) );


Sehr ähnlich, aber nun werden die Container-Elemente wieder automatisch mit dem Container freigegeben - es sei denn, es gibt noch einen anderen shared_ptr auf ein Element, wie im Folgenden demonstriert:

C++:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void Sample3_Container()
{
  typedef boost::shared_ptr<CSample> CSamplePtr;
 
  // (A) Vektor mit CSample-Zeigern:
  std::vector<CSamplePtr> vec;
 
  // (B) drei Elemente hinzufügen
  vec.push_back(CSamplePtr(new CSample("A"));
  vec.push_back(CSamplePtr(new CSample("B"));
  vec.push_back(CSamplePtr(new CSample("C"));
 
  // (C) Einen Zeiger auf das zweite Element "behalten":
  CSamplePtr anElement = vec[1];
 
  // (D) den Vektor freigeben:
  vec.clear();  // "A" und "C" werden hier freigegeben
 
  // (E) das zweite Element "B" existiert noch:
  anElement->Use();
  printf("fertig.\n");
 
  // (F) anElement verlässt den Gültigkeitsbereich und CSample("C") wird freigegeben
}


shared_ptr<T> - Die wichtigsten Eigenschaften

Es gibt unzählige Smart Pointer-Implementationen. Einige wichtige Eigenschaften machen boost::shared_ptr empfehlenswert:



Was kann schief gehen?

Zwar sind Smart Pointer um einiges robuster als gewöhnliche Zeiger, aber wie immer gibt es einige Dinge, die man unbedingt wissen muss:

Regel 1: Zuweisen und Halten - Ein mit new erzeugtes Objekt sollte sofort an einen Smart Pointer zugewiesen und ausschließlich über diese referenziert werden. Die Smart Pointer sind nun Eigentümer des Objekts. Dadurch vermeidet man versehentliches vorzeitiges Löschen

Regel 2: Ein _ptr<T> ist kein T * - genauer gesagt: es gibt keine implizite Konvertierung zwischen einem T * und einem shared_ptr<T>. Dadurch wird eine versehentliche Verletzung von Regel (1) ausgeschlossen. "Gefährliche" Operationen müssen daher explizit gemacht werden:


Regel 3: keine temporären shared_ptr - immer schön eine Variable vergeben. (Notwendig für korrekte Exception-Behandlung, Details findet man unter boost: shared_ptr best practices)

Regel 4: Keine zirkulären Referenzen - mehr dazu sofort.
Generell sind Boost Smart Pointer auf Sicherheit getrimmt - potenziell gefährliche Operationen müssen explizit geschrieben werden.

Zyklische Referenzen

Referenzzählung erlaubt bequemes und effektives automatisches Ressourcenmanagement, hat aber eine Schwachstelle: Objekte, die sich (direkt oder indirekt) gegenseitig referenzieren. Ein einfaches Beispiel:

C++:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct CDad;
struct CChild;
 
typedef boost::shared_ptr<CDad>   CDadPtr;
typedef boost::shared_ptr<CChild> CChildPtr;
 
struct CDad   : public CSample { CChildPtr myBoy;  };
struct CChild : public CSample { CDadPtr myDad;    };
 
CDadPtr   parent(new CDadPtr);
CChildPtr child(new CChildPtr);
 
// absichtlich eine zirkuläre Referenz erstellen:
parent->myBoy = child;
child->myDad = dad;
 
// einen Zeiger zurücksetzen...
child.reset();

Sowohl die CChild-Instanz als auch parent referenzieren die CDad-Instanz, welche wiederum die CChild-instanz referenziert:



Wenn man jetzt dad.reset() aufruft, verlieren die beiden Objekte jeglichen Kontakt zur Außenwelt, halten sich aber gegenseitig am Leben - ein klassisches Leck! Im schlimmsten Fall werden dadurch kritische Ressourcen nicht mehr freigegeben.

Diese Problem ist nicht (bzw. nur mit inakzeptablen Restriktionen) innerhalb der shared_ptr-Implementation lösbar - man muss den Kreis erkennen und selbst auflösen. Dazu gibt es verschiedene Wege:



Die Lösungen (1) und (2) können nicht immer eingesetzt werden, funktionieren aber mit allen Smart Pointer-Bibliotheken. (3) ist eigentlich eine Verallgemeinerung von (2), die aber ohne eine Anforderung an die Lebensdauer auskommt.

Zyklische Referenzen mit weak_ptr aufbrechen

Man unterscheidet starke und schwache Referenzen: Eine starke Referenz verhindert das Löschen eines Objekts, eine schwache Referenz tut dies nicht.
In diesem Sinne ist boost::shared_ptr<T> eine starke und T * eine schwache Referenz. Mit T * kann man aber nicht prüfen, ob das Objekt noch existiert (daher die Lebensdauer-Anforderung in Lösung (2) oben).

boost::weak_ptr<T> ist ein Smart Pointer, der eine schwache Referenz implementiert. Bei Bedarf kann man sich von einer solchen eine starke Referenz geben lassen. Falls das Objekt nicht mehr existiert, ist diese 0. Die starke Referenz darf natürlich nur solange wie nötig behalten werden.
Obiges Beispiel mit eine weak_ptr:

C++:
1
2
3
4
5
6
7
8
9
10
11
12
struct CBetterChild : public CSample
{
  weak_ptr<CDad> myDad;
 
  void BringBeer()
  {
    shared_ptr<CDad> strongDad = myDad.lock(); // eine starke Referenz beantragen
    if (strongDad)                      // ist das Objekt noch existent?
      strongDad->SetBeer();
    // strongDad wird freigegeben, sobald sein Gültigkeitsbereich verlassen wird; die schwache Referenz wird behalten.
  }
};


Komplexe Objektstrukturen erfordern immer noch eine sorgfältige Analyse, an welchen Stellen zirkuläre Referenzen auftreten und welche davon durch einen weak_ptr aufgebrochen werden können (im Beispiel kann man z.B. nicht den Zeiger auf CChild durch einen weak_ptr ersetzen!) Hat man aber einmal eine ordentliche Struktur aufgebaut, muss man sich um die Freigabe keinen Kopf mehr machen - und gerade bei komplexen Strukturen ist das sehr angenehm.

intrusive_ptr<t> - Das Leichtgewicht

shared_ptr bietet eine Menge mehr als einen "normalen" Zeiger. Das hat natürlich einen kleine Preis: er ist größer, ein wenig langsamer, und benötigt zu jedem referenzierten Objekt ein separates "Tracking"-Objekt mit Zähler und Deleter. Das kann man fast immer vernachlässigen.

Wenn nicht, bietet boost::intrusive_ptr eine interessante Alternative: Den "leichtmöglichsten" referenzgezählten Zeiger - allerdings muss man sich um das Zählen selbst kümmern. Das ist gar nicht so schlimm: will man seine eigenen Klassen Smart Pointer-kompatibel machen, packt man den Referenzzähler gleich mit rein. Dafür bekommt man weniger Allokationen und einen fixen Smart Pointer.

Um einen Typ intrusive_ptr<T> zu verwenden, muss man zwei Funktionen definieren: intrusive_ptr_add_ref und intrusive_ptr_release. Das folgende Beispiel zeigt, wie:
C++:
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
#include "boost/intrusive_ptr.hpp"
 
class CRefCounted
{
  private:
    long    references;
    friend void intrusive_ptr_add_ref(T * p);
    friend void intrusive_ptr_release(T * p);
 
  public:
    CRefCount() : references(0) {}   // references mit 0 initialisieren
};
 
// die zwei Funktionsüberladungen müssen bei den meisten Compilern im boost-Namespace stehen:
namespace boost  
{
  void intrusive_ptr_add_ref(CRefCounted * p)
  {
    // Referenzzähler für das Objekt *p inkrementieren
    ++(p->references);
  }
 
  void intrusive_ptr_release(CRefCounted * p)
  {
   // Referenzzähler dekrementieren und, wenn der Zähler 0 erreicht hat, Objekt löschen
   if (--(p->references) == 0)
     delete p;
  }
} // namespace boost

Das ist die einfachste (und nicht threadsichere!) Implementation. Eine generische Implementation findet man nach kurzem Stöbern in der Boost Mailing List.

Tipp:
Um das Beispiel unter Windows/VC threadsicher zu bekommen, verwendet man Folgendes:

C++:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
extern "C" _InterlockedIncrement(LPLONG lpAddend);
extern "C" _InterlockedDecrement(LPLONG lpAddend);
#pragma intrinsic(_InterlockedIncrement, _InterlockedDecrement)  // werden inline expandiert!
 
namespace boost
{
  void intrusive_ptr_add_ref(CRefCounted * p)
 
  {
    _InterlockedIncrement(&(p->references));
  }
 
  void intrusive_ptr_release(CRefCounted * p)
  {
   if (_InterlockedDecrement(&(p->references)) == 0)
     delete p;
  }
}


scoped_array und shared_array

Die beiden sollen nicht unerwähnt bleiben - sie sind einem scoped_ptr bzw. shared_ptr sehr ähnlich, Syntax und Verhalten ähnelt aber einem mit new[] allozierten Zeiger (z.B. operator[], delete[] als Standard-Deleter). Hinweis: Keiner von beiden merkt sich die Länge, sie sind also als "Array-Ersatz" nur bedingt geeignet.

Boost installieren

Die aktuellen Boost-Bibliotheken können von boost.org heruntergeladen werden. Für die Verwendung der Smart Pointer-Bibliothek ist keine Übersetzung notwendig.
Das Boost-Wurzelverzeichnis enthält Release Notes und eine Menge Verzeichnisse für Dokumentation, Build Tools usw. Die eigentlichen Quellen befinden sich in dem Unterverzeichnis boost\.

Ich füge das boost-Wurzelverzeichnis zu den Standard-Include-Pfaden hinzu.
in VC6: Extras/Einstellungen, "Verzeichnisse"-Reiter, "Verzeichnisse für... Include-Dateien"
in VC7: Extras/Einstellungen, Projekte/VC++-Verzeichnisse, "Verzeichnisse für... Include-Dateien"

Die #includes sehen dann so aus:
C++:
#include <boost/smart_ptr.hpp>


Was ist mit std::auto_ptr?

std::auto_ptr ist der einzige Smart Pointer im aktuellen Standard. Leider - denn auto_ptr erfüllt zwar seine Zweck, ist aber unnötig komplex und wird schnell falsch eingesetzt.

auto_ptr in zwei Sätzen: Die letzte auto_ptr-Instanz, der der Zeiger zugewiesen wurde, gibt das referenzierte Objekt frei. Das ist aber nicht notwendigerweise die letzte vorhandene auto_ptr-Instanz.

In den beiden häufigsten Anwendungsfällen kann und sollte man auf andere Smart Pointer ausweichen:

1) lokale Objekte / PIMPL ==> scoped_ptr
C++:
class CBar;
class CFoo
{
   auto_ptr<CBar>   m_pBar;  // besser: scoped_ptr
   // ...
}

scoped_ptr verbietet die bei auto_ptr mögliche Zuweisung (die einen u.U. gefährlichen, weil versteckten Transfer of Ownership bedeutet)

2) Rückgabewert aus Factory ==> shared_ptr
C++:
class CBar;
auto_ptr<CBar> MakeBar() { ... }  // besser: shared_ptr


shared_ptr erlaubt die hier notwendige Zuweisung - und ist auch bei Mehrfachzuweisungen sicher. Das folgende Beispiel soll das illustrieren:

C++:
auto_ptr<CFont> GetFont(EMyFavoriteFonts emff);
...
auto_ptr<CFont> font = GetFont(emffCoolShadowedFont);
widget1->font = font;  // ??
widget2->font = font;  // ??


Wer gibt das CFont-Objekt wieder frei? widget2 - selbst wenn widget1 noch existiert. Hier ist ein Referenzzähler sinnvoll - also verwendet man besser shared_ptr oder intrusive_ptr.

Links (alle in Englisch)
Boost Smart Pointer Bibliothek
Smart Pointer - Tipps & Tricks
Herb Sutter über Smart Pointers
Originaler Artikel
Kreativer Missbrauch von shared_ptr<T>


_________________________________________________________________________________

Sept 05, 2004: Erste Version
Aug. 2005: Deutsche Übersetzung

Sie können Kommentare zu diesem Artikel im Forum schreiben. (Eine Registrierung ist nicht notwendig.)

Logo-Design: MastaMind Webdesign