Autopointer auto_ptr<T>

Als Motivation für das eigentliche Thema dieses Abschnitts werden ein paar gefährliche, aber durchaus übliche Programmfragmente gezeigt. Durch die starke Verdichtung auf das Wesentliche sind die Probleme hoffentlich leicht zu erkennen; aber in realem Quelltext werden sie oft nicht erkannt, wodurch Speicherlecks entstehen (allokierter Speicher wird nicht freigegeben) oder es wird illegal auf ungültigen Speicher zugegriffen (weil ein Objekt beispielsweise bereits vor dem Zugriff freigegeben wurde). Letzteres führt -falls man Glück hat- zu einem Programmabbruch, oder zu fehlerhaften Daten.

Relativ harmlos erscheint es meistens, in einem Block einen Zeiger auf ein dort bekanntes Objekt zu richten (beispielsweise auf neu allokierten Speicher), über den Zeiger etwas mit dem Objekt zu machen, und praktisch zeitgleich mit dem Ende der Lebensdauer des Zeigers auch das Objekt zu zerstören, auf das der Zeiger verweist:

   {
     Cwassweissich *ptr = new Cwassweissich;
   
     tuwas( ptr );
     // ...
   
     delete ptr;   // hier endet die Lebensdauer des allokierten Objekts
   }               // hier endet die Lebensdauer von ptr

Der Programmierer dieses Quelltextes gibt den allokierten Speicher sauber frei, bevor der Block verlassen wird.

Probleme treten regelmäßig auf, wenn der Programmfluß nicht so schön linear verläuft wie in dem obigen Beispiel, oder wenn die Lebensdauer des Zeigers und des darauf verwiesenen Objekts unterschiedlich sind.

Beispiele für heikle Fälle:

Alle beschriebenen Probleme (außer dem letzten) lassen sich überraschend einfach lösen, wenn man den Zeiger in eine eigene Klasse ,,verpackt``:

class Cwassweissich_ptr_t
{
public:

  // Konstruktor
  Cwassweissich_ptr_t()
    : ptr( NULL )
  {
  }

  // Konstruktor mit einem Zeiger auf Cwassweissich:
  Cwassweissich_ptr_t( Cwassweissich *rechteSeite )
    : ptr( rechteSeite )
  {
  }

  // Destruktor
  ~Cwassweissich_ptr_t()
  {
    delete ptr;
  }

  // Zuweisung Cwassweissich_ptr_t = Cwassweissich*
  Cwassweissich_ptr_t & operator=( Cwassweissich *rechteSeite )
  {
    // bei mehreren Zuweisungen nacheinander die
    // vorherigen Zeiger freigeben:
    delete ptr;

    // neuen Zeiger merken:
    ptr = rechteSeite;

    return *this;
  }

  // Damit man ein *Cwassweissich_ptr_t anstelle eines
  // *Cwassweissich verwenden kann, bauen wir eine
  // passende Typkonvertierung:
  operator Cwassweissich*() const
  {
    return ptr;
  }

private:

  // der eigentliche Zeiger auf den allokierten Speicher:
  Cwassweissich * ptr;

}; // class Cwassweissich_ptr_t

Die Verwendung ändert sich geringfügig, weil statt eines Cwassweissich* jetzt ein Cwassweissich_ptr_t verwendet werden muß, wodurch ein manuelles Freigeben überflüssig wird (und auch gar nicht mehr möglich ist):

   {
     Cwassweissich_ptr_t ptr = new Cwassweissich;
      
     tuwas( ptr );             // Ausnahme? Kein Problem!
     if( einProblem ) return;  // return? Kein Problem!
     // ...
    
    // spätestens hier endet die Lebensdauer von ptr,
    // allokierter Speicher wird im Destruktor automatisch
    // freigegeben!
   }

Durch diesen Kunstgriff meisterlicher Hand stören Unterbrechungen im linearen Programmfluß nicht mehr weiter:

Durch das Einbetten des Zeigers in ein automatisches Objekt auf dem Stack wird also die Lebensdauer eines Heap-Objekts (Cwassweissich) an die Lebensdauer des Stackobjekts (Cwassweissich_ptr_t) gebunden, und die Speicherverwaltung wird -ohne weiteres Zutun des Aufrufers- wieder konsistent.

Das letzte der geschilderten Probleme (Allokieren in einer Funktion, nötige Freigabe beim Aufrufer) läßt sich mit wenig Mehraufwand lösen. Wenn in einer Funktion ebenfalls ein Cwassweissich_ptr_t erzeugt und als Rückgabewert an den Aufrufer geliefert wird, müssen an der Klasse Cwassweissich_ptr_t nur wenige Änderungen gemacht werden:

Mit einem so erweiterten Cwassweissich_ptr_t kann man in einem Unterprogramm ein Cwassweissich allokieren, und als Cwassweissich_ptr_t verpackt zurückgeben:

   Cwassweissich_ptr_t up()
   {
     Cwassweissich_ptr_t ret = new Cwassweissich;   // up() allokiert
     // ...

     // hier wird ein temporäres Cwassweissich_ptr_t-Objekt
     // geschaffen, was den Besitz am allokierten Speicher übernimmt.
     // Deshalb gibt der Destruktor von ret keinen Speicher frei!
     return ret;
   }

   void Aufrufer()
   {
     // bei der Initialisierung geht der Besitz am allokierten
     // Speicher an ptr über.
     // Anschließend wird das temporäre Objekt freigegeben,
     // und der allokierte Speicher nicht freigegeben, weil
     // das temporäre Objekt keinen Besitz mehr hat.
     Cwassweissich_ptr_t ptr = up();

     tuwas( ptr );
     // ...

    // spätestens hier endet die Lebensdauer von ptr,
    // allokierter Speicher wird im Destruktor automatisch
    // freigegeben, weil ptr den Besitz am allokierten Speicher
    // hat!
   }
Für die Rückgabe aus dem Unterprogramm wird der Compiler ein temporäres Objekt vom Typ Cwassweissich_ptr_t schaffen und mit einem copy-Konstruktor initialisieren; dabei geht der Besitz auf das temporäre Objekt über.

Der Aufrufer wird das Ergebnis des Funktionsaufrufs an eine automatische Variable vom Typ Cwassweissich_ptr_t zuweisen; dabei geht durch den operator=() der Besitz an diese über.

Mit dem Ende der Lebensdauer der automatischen Variable wird dann auch der allokierte Speicher freigegeben.

Soweit so gut. Mit dem bisherigen Wissen kann man also alle geschilderten Probleme leicht umgehen, wenn man sich für jeden Zeiger auf irgendeinen Typ die Mühe macht, und eine passende Verpackung baut und statt des Zeigers verwendet.

Weil diese Verpackung aber außer dem geschilderten Mechanismus mit delete und dem Besitz am allokierten Speicher nichts machen muß, und insbesondere nichts über den verpackten Datentyp wissen muß, bietet sich dafür eine template-Klasse an (siehe Klassenschablonen (template-Klassen)).

Und weil die Problematik sehr gängig ist und die beschriebene Lösung sehr hilfreich, gibt es eine solche template-Klasse bereits in der STL (in <memory>).

Die Klasse heißt auto_ptr<T> und hat genau das oben beschriebene Verhalten.

Beispielverwendung:

   #include <memory>
   using namespace std;

   // ...

   auto_ptr<Cwassweissich> up()
   {
     auto_ptr<Cwassweissich> ret( new Cwassweissich );   // up() allokiert
     // ...

     return ret;
   }

   void Aufrufer()
   {
     auto_ptr<Cwassweissich> ptr = up();

     tuwas( ptr );
     // ...

   } // hier automatische Freigabe mit delete!

Achtung! Auch hier gibt es wieder eine Fallgrube. Durch die Tatsache, daß beim Anlegen einer Kopie der Besitz auf die Kopie übergeht, muß man das Erzeugen temporärer Kopien durch den Compiler vermeiden. Diese werden beispielsweise beim Aufruf von Unterprogrammen angelegt, wenn ein Parameter als Wert übergeben wird (call by value). Mit dem Löschen dieses temporären Objekts würde auch der allokierte Speicher freigegeben werden.

Beispiel:

   void up( Cwassweissich parameter )
   {
     // ...
   }
   
   // ...
   
     auto_ptr<Cwassweissich> a( new Cwassweissich ); // a hat Besitz
   
     // für die Übergabe wird eine temporäre Kopie von a erzeugt,
     // dafür wird der copy-Konstruktor aufgerufen und der Besitz
     // am allokierten Speicher geht auf die temporäre Kopie über.
     // Die Kopie wird in up() als Parameter verwendet.
     up( a );
     // Mit dem Ende der Funktion up() wird die temporäre Kopie
     // freigegeben, und der oben mit new allokierte Speicher wird
     // freigegeben, weil sie den Besitz daran hat.
   
     // Ab hier darf nicht mehr auf a zugegriffen werden!

Aus diesem Grund dürfen auto_ptr<T>-Objekte niemals als Wert an ein Unterprogramm übergeben werden, sondern nur als Referenz!

Zum Erzeugen eines auto_ptr<T>-Objekts existieren folgende Konstruktoren:

Daneben existiert noch wie bereits oben erwähnt eine Zuweisung, die ein auf der linken Seite eventuell vorhandenes Objekt freigibt (falls die linke Seite den Besitz daran hat), dann das Objekt von der rechten auf die linke Seite kopiert, und dann den Besitz überträgt (falls auf der rechten Seite vorhanden). Der Typ des auf der rechten Seite angegebenen auto_ptr darf auch eine vom Typ Objekts auf der linken Seite abgeleitete Klasse sein (oder es muß eine andere automatische Typumwandlung bestehen).

Mit einer Memberfunktion get() kann man einen Zeiger auf das verwaltete Objekt bekommen (oder NULL, wenn kein solches existiert).

release() löscht den Besitz eines auto_ptr, ohne das Objekt zu zerstören. Rückgabewert ist ein Zeiger auf das enthaltene Objekt, oder NULL wenn kein solches existiert.

Mit operator* und operator-> kann man auf das verwiesene Objekt oder seine Elemente zugreifen.

AnyWare@Wachtler.de