Q: Was sind auto_ptr und wie setzt man sie ein?
Die Templateklasse auto_ptr der C++ Standardbibliothek ist eine Form eines
Smart-Pointers.
Als Smart-Pointer bezeichnet man Klassen, die Zeiger auf dynamisch erstellte Objekte
verwalten. Die Intelligenz eines Smart-Pointers liegt darin, dass er sich komplett um
das Memory-Management kümmert. Dazu gehören Dinge wie das Verhindern von Dangling-Pointern
(also Zeigern die auf nicht mehr existiernde Objekte zeigen) und die
automatische Speicher- bzw. Resourcenfreigabe für das referenzierte Objekt.
Damit sich ein Smart-Pointer wie ein nornmaler Zeiger verhalten kann, muss er die Schnittstelle
eines Zeigers besitzen. Diese besteht mindestens aus dem Dereferenzierungsoperator
(operator*) und dem Indirektionsoperator (operator->).
Da es für die Umsetzung des Memory-Managements unzählige Möglichkeiten
gibt (Besitztransfer, Referenzzählung usw.), existieren auch unzählige verschiedene Arten von
Smart-Pointer-Klassen.
Hier soll es nun nur um die Standard C++ auto_ptr gehen.
1. auto_ptr - Warum?
Ein auto_ptr wird für die Verwaltung eines mit new erzeugten Objekts verwendet.
Der auto_ptr selbst wird dabei immer lokal (also auf dem Stack) angelegt.
Bekanntlich wird der Destruktor von Stackobjekten automatisch aufgerufen, sobald
der Programmfluss den Gültigkeitsbereich solcher Objekte verläßt.
Da der Destruktor der Klasse auto_ptr auch das verwaltete Heapobjekt freigibt (über einen
Aufruf von delete), wird so der Vorteil eines Stackobjekts,
nämlich die automatische Speicherfreigabe, auf ein Heapobjekt übertragen.
Der wirkliche Vorteil eines auto_ptrs besteht aber nicht darin, dass man sich die eine Zeile delete X;
erspart. Vielmehr garantiert die Verwendung eines auto_ptrs, dass das verwaltete Objekt automatisch
auch im Falle einer Exception korrekt freigegeben wird.
Die Gefahr das es zu unerwarteten Speicher- bzw. Resourcenlöchern kommt ist also deutlich geringer.
Ein Beispiel:
#include
<memory> #include
<iostream> #include
<cstdlib> #include
<ctime> using
namespace
std;
class
X { public:
X() {cout <<
"X Standardctor" <<
endl;} X(const
X&
re) {cout <<
"X Copy-Ctor" <<
endl;} ~X()
{cout << "X Dtor" <<
endl;} void
MightThrow()
{ if
(rand() % 2)
throw
int(42); } };
//
Zwei einfache Zeilen. // Vorteil: sicher und
einfach void
UseAuto_ptr() { auto_ptr<X>
Pointer(new
X); Pointer->MightThrow(); }
//
Drei einfache Zeilen // Nachteil: Sollte
MightThrow eine Exception werfen, wird // das
Objekt auf das Pointer zeigt nicht
gelöscht. // Es entsteht also mindestens ein
Speicherloch. Hätte X einen // nicht
trivialen Destruktor, so würde zusätzlich auch noch
ein // Resourcenloch
entstehen. void
UseRawPtr() { X*
Pointer =
new
X; Pointer->MightThrow(); delete
Pointer; }
//
Nun sind es schon sechs Zeilen. // Der Code macht
das selbe wie die Funktion UseAuto_ptr ist //
dafür aber viel komplexer.
void
UseRawPtrBetter() { X*
Pointer =
new
X; try { Pointer->MightThrow(); } catch(...) { delete
Pointer; // falls eine
Exception
auftritt. throw; } delete
Pointer; // falls keine
Exception
auftritt.
} int
main() { srand(time(NULL)); try { UseAuto_ptr(); UseRawPtr(); } catch(...) { cout
<< "Holla! Eine Exception!" <<
endl; } }
Die Verwendung von auto_ptrn macht diesen Code nicht nur sicherer, sondern auch leichter zu verstehen.
Grundsätzlich haben auto_ptr die folgenden Vorteile gegenüber normalen Pointern:
- automatische Freigabe von Speicher und Resourcen
- automatische Initialisierung. Entweder mit einem Heapobjekt oder mit NULL
- korrektes Freigabe von Speicher und Resourcen auch im Falle von Exceptions
2. auto_ptr - Wie?
Ein auto_ptr besitzt das Objekt, das er verwaltet. Als Besitzer ist er für
die Zerstörung des Objekts verantwortlich. Dies führt zu dem logischen Schluß, dass niemals
zwei auto_ptr Besitzer des selben Heapobjekts sein dürfen.
Nun stellt sich natürlich die Frage, was beim Kopieren eines auto_ptr-Objekts passieren soll.
Ein normales Kopieren kann nicht stattfinden, da sonst zwei auto_ptr Besitzer des selben Objekts wären.
Um es kurz zu machen: Die Klasse auto_ptr implementiert die sogenannte move-copy bzw. move-assignment
Strategie.
Jeder Kopiervorgang führt dazu, dass der auto_ptr auf der rechten Seite (also die Quelle)
seinen Besitzanspruch an den auto_ptr auf der linken Seite (also das Ziel) abtritt. Neben dem
Besitzanspruch verliert die Quelle aber zusätzlich auch die Referenz auf das Heapobjekt. Nach dem
Kopiervorgang enthält der auto_ptr auf der rechten Seite nur noch einen NULL-Zeiger.
Besitzt der auto_ptr auf der linken Seite bereits ein Objekt, so wird dieses zuvor gelöscht.
Dieser Besitztransfer ist das Wichtigste was es bei auto_ptr zu beachten gibt.
Nach einem Kopiervorgang (Aufurf des Copy-Konstruktors oder Zuweisungsoperators) referenziert
das auto_ptr-Objekt auf der rechten Seite kein Objekt mehr!
Definiert wird die Templateklasse auto_ptr im Header <memory>. Übersetzungseinheiten die auto_ptr
verwenden wollen, müssen also diesen Header inkludieren.
Die Klasse auto_ptr besitzt drei Konstruktoren, die sich wie folgt einsetzen lassen:
//
Konstruktoren auto_ptr
<X> First
(new
X); auto_ptr
<X>
Second(First); auto_ptr
<X>
Third; // Achtung! so
geht's nicht: // auto_ptr <X> First = new
X;
Der erste Konstruktor erstellt ein auto_ptr-Objekt und initialisiert es mit einem Heapobjekt.
Der zweite Konstruktor erstellt ein auto_ptr-Objekt, das mit dem Heapobjekt eines anderen
auto_ptr-Objekts initialisiert wird. Das auto_ptr-Objekt auf der rechten Seite (hier First) verliert
dabei seien Referenz auf das Heapobjekt.
Der dritte Konstruktor erstellt ein auto_ptr-Objekt, das mit NULL initialisiert wird.
//
Zuweisung auto_ptr
<X> First
(new
X); auto_ptr
<X>
Second; Second =
First;
Der Inhalt eines auto_ptrs kann einem anderen über den Zuweisungsoperator zugewiesen werden.
Der auto_ptr auf der rechten Seite (hier First) verliert dabei seine Referenz auf das Heapobjekt.
Sollte der auto_ptr auf der linken Seite bereits im Besitz eines Objekts sein, so wird dieses
vor der Zuweisung gelöscht.
// weitere Methoden
auto_ptr
<X>
First(new
X); First.reset(new
X) if
(First.get()) { First->MemberOfX(); (*First).MemberOfX(); } delete
First.release();
Die Methode reset erlaubt die Zuweisung eines normalen Zeigers (der natürlich auf ein mit new
erstelltes Objekt zeigen muss) an einen auto_ptr.
Sollte First bereits ein Objekt besitzen, so wird dieses erst gelöscht. Ruft man reset mit NULL als
Parameter auf, wird das verwaltete Objekt freigegeben.
Die Methode get liefert eine Zeiger auf das verwaltete Objekt.
Wie oben bereits erwähnt, muss sich ein Smart-Pointer wie ein Zeiger verhalten können.
Die auto_ptr-Klasse implementiert deshalb den Dereferenzierungsoperator (operator*) und den Indirektionsoperator (operator->).
Pointerarithmetik ist mit auto_ptr allerdings nicht möglich. Dies ist aber nicht weiter schlimm,
da auto_ptr nicht für die Verwaltung von Arrays benutzt werden dürfen..
Die release-Methode liefert einen Zeiger auf das verwaltete Heapobjekt und setzt den
auto_ptr wieder auf NULL. Der auto_ptr ist danach also nicht mehr im Besitz des Heapobjekts. Die
Verantwortung für das Objekt geht in die Hände des Aufrufers über.
Standardkonforme Implementationen erlauben es auto_ptr anzulegen,
die niemals ihren Besitz transferieren. Dies geschieht durch das Voranstellen eines
const.
// Anker bleibt immer im Besitz des
Heapobjekts // Es kann kein Transfer
stattfinden const
auto_ptr<X>
Anker(new
X); auto_ptr<X>
GehtNicht(Anker); //
GEHT
NICHT! auto_ptr<X>
GehtAuchNicht; GehtAuchNicht
= Anker; // GEHT
EBENFALLS NICHT!
Ein solcher auto_ptr verhält sich also wie ein konstanter Zeiger (X* const).
Achtung: Einige Implementationen (z.B. die des VCs von Microsoft) erlauben die
Besitztransferierung bei const auto_ptr. Solche Implementationen erlauben auch fälschlicherweise
das Anlegen von STL-Containern die auto_ptr als Elemente verwalten.
Dies ist nicht standard, nicht portable und niemals eine gute Idee.
3. auto_ptr - Wann?
Eines der vielen Einsatzgebiete für auto_ptr zeigt das einleitende Beispiel.
Hier noch weitere:
- als Rückgabewert bei Funktionen die als Quelle dienen sollen:
Häufig kommt man in die Situation, eine Funktion schreiben zu wollen, die eine
bestimmte Resource (ein bestimmtes Objekt) liefert. Handelt es sich hierbei um einen Typen
für den das Kopieren teuer oder vielleicht sogar unmöglich ist,
fällt die Möglichkeit einer Wertrückgabe weg. Eine Referenz auf ein lokal erzeugtes Objekt
kann man aber auch nicht zurückgeben, da eine solche ihre Gültigkeit mit dem Funktionsende verliert (das Objekt wird ja zerstört).
Es bleibt also nur die Rückgabe eines Zeigers auf ein Heapobjekt (entweder als Rückgabewert
oder über einen out-Parameter). Dies legt aber die Verantwortung für die Freigabe des Objekts
in die Hände des Aufrufers. Dieser hatte mit der Erzeugung aber nichts zu tun, weiß im Zweifelsfall
nicht mal, dass das Objekt auf dem Heap erzeugt wurde. Vielleicht fühlt er sich aber
auch ungerecht behandelt, da er den Müll eines anderen wegräumen muss.
Wie man es auch dreht und wendet, eine schöne Lösung ist das nicht.
Anders sieht es mit man auto_ptr aus. Hier kümmert sich das auto_ptr-Objekt darum, dass
das Heapobjekt ordentlich freigegeben wird. Durch den Besitztransfer beim Kopieren wird
eine Mehrfachfreigabe verhindert. Der Destruktor des lokalen auto_ptr-Objekts muss also
nichts tun.
auto_ptr<X>
Func() {
return
auto_ptr<X>
(new
X); } int
main() {
for
(int
i = 0;
i < 10 ;
i++)
auto_ptr<X>
Pointer(Func()); Func();
}
Durch die Verwendung von auto_ptr ist immer Garantiert, dass das Heapobjekt freigegeben wird.
Dies gilt auch für den Fall, dass eine Excepetion auftritt oder dass der Rückgabewert
der Funktion ignoriert wird.
- als Wertparameter von Funktionen die als Senke dienen sollen:
Eine Funktion die einen auto_ptr als Wertparameter erwartet, arbeitet als Senke. Der Aufrufer
überträgt den Besitz an die Funktion. Sobald die Funktion endet wird das Heapobjekt automatisch
zerstört.
void
Destroy(auto_ptr<X>
Stirb) {} int
main() { for
(int
i = 0;
i < 10 ;
i++) { auto_ptr<X>
Pointer(new
X); Destroy(Pointer); cout
<< Pointer.get() <<
endl; } }
- als Attribute einer Klasse:
Klassen die Zeiger auf Heapobjekte enthalten sind zwar nicht schön, kommen
aber dennoch häufig vor.
Hier spart die Ersetzung der normalen Zeiger durch auto_ptr häufig nicht nur das Schreiben
eines Destruktors, sondern hilft auch dabei Speicher- und Resourcenlöcher zu verhindern.
Copy-Konstruktor und Zuweisungsoperator muss man aber trotzdem noch
implementieren, da Besitzübertragung normalerweise nicht das gewünschte Verhalten
beim Kopieren ist.
4. auto_ptr - Achtung!
- ein auto_ptr darf niemals ein Objekt verwalten, das nicht mit new erzeugt wurde:
// NIEMALS NIE NIE
GUT! int
main() {
X
MyX;
auto_ptr<X>
Auto(&MyX); }
- Es dürfen niemals mehrere auto_ptr das selbe Objekt besitzen:
// NIEMALS NIE NIE
GUT! int
main() {
X* pX =
new
X;
auto_ptr<X>
Auto1(pX);
auto_ptr<X>
Auto2(pX);
}
- auto_ptr dürfen nicht für Arrays verwendet werden:
// NIEMALS NIE NIE
GUT! int
main() {
auto_ptr<char>
Auto(new
char[20]); }
- auto_ptr dürfen nicht als Elemente von STL-Containern verwendet werden:
STL-Containerelemente müssen häufig kopiert werden (z.B. beim Einfügen, Sortieren usw.).
Aus diesem Grund erwartet man von ihnen, dass Kopie und Original äquivalent sind.
Dies trifft auf auto_ptr aber nicht zu, da durch die Besitzübertragung das Original
zurückgesetzt wird und nach dem Kopiervorgang auf NULL zeigt.
Standardkonformen Implementationen verweigern das Anlegen von STL-Containern
mit auto_ptr bereits zur Compilezeit.
Leider gibt es aber einige Implementationen (z.B. die des VCs von Microsoft)
die dies erlauben.
Dies ist nicht standard, nicht portable und niemals eine gute Idee.
- Beim Kopieren eines auto_ptr findet immer ein Besitztransfer statt:
// Print fungiert als Senke!
void
Print(auto_ptr<int>
pInt) {
cout << *pInt
<<
endl; }
int
main() {
auto_ptr<int>
Antwort(new
int(42));
Print(Antwort);
// KAWUMM! Antwort hat sein Objekt längst
verloren.
*Antwort =
14; }
5. auto_ptr - Fazit.
Die auto_ptr haben eine langen Weg hinter sich. Es existieren mittlerweile mindestens drei
verschiedene Varianten. Die hier beschriebenen Dinge beziehen sich auf die
auto_ptr des aktuellen C++ Standards. Diese sind sowohl nützlich, als auch sicher und einfach
zu bedienen. Andere Varianten (insbesondere die, die beim VC von Microsoft mitgeliefert wird)
erfordern vom Programmierer etwas mehr aufmerksamkeit.
Behält man aber im Hinterkopf, dass das
Kopieren eines auto_ptrs immer Besitztransfer bedeutet und das deshalb Kopie und Original
niemals gleich sein können, so kann einem eigentlich nicht mehr viel passieren.
Wie bereits erwähnt sind auto_ptr nur eine Form von Smart-Pointern. Im Netz findet man eine
Vielzahl von anderen Smart-Pointer-Klassen. Auf boost.org
findet man z.B. Smart-Pointer, die so oder ähnlich in späteren C++ Standards enthalten sein könnten.
Literatur:
- S. Meyers: "auto_ptr update"
- H. Sutter: "Exceptional C++"
- N. Josuttis: "The C++ Standard Library"
- S. Lippman, J. Lajoie: "C++ Primer"
|