Smart Pointer

ein intelligenter Umgang mit Zeigern...

Sven Johannsen
16.1.2013
Version: 1.0
C++ User Group Meeting NRW

sven@sven-johannsen.de
www.sven-johannsen.de
@svenjohannsen

Abstract

In C++ ist der Umgang mit dynamischem Speicher nicht unproblematisch.

Nicht freigegebener Speicher (Memory leaks) und Zugriffsverletzung können jederzeit Probleme verursachen.

Die Verwendung von Smart-Pointern kann diese Problematik entschärfen, in dem sie die Verwendung von dynamisch angefordertem Speicher vereinfacht.

Inhalt

Code

Die meisten Beispiele setzen die Klassen MyObject, OtherObject und die freie Funktion maybeThrowing() voraus.

Die Rückgabe von getOtherObject() muss mit delete freigegeben werden.

Es ist nicht klar ob maybeThrowing() eine Exception wirft oder nicht.

Warum brauche ich Smart-Pointer?

Oder: Was ist das Problem mit (Raw-) Pointer?

Memory Leaks

Speicherleck (englisch memory leak, gelegentlich auch Speicherloch oder kurz memleak) bezeichnet einen Fehler in einem Computerprogramm, der dazu führt, dass ein laufender Prozess einen Speicherbereich zwar belegt, diesen jedoch weder freigeben noch nutzen kann.

(Wikipedia)

Memory Leaks

Dynamischer Speicher (Speicher, der auf dem Heap allokiert wurde) muss wieder freigegeben werden.

Wenn p nicht freigegeben wird, steht der Speicher dem Programm in Zukunft nicht mehr zu Verfügung.

Auch wenn p überschrieben kann der ursprüngliche Speicher nicht freigegeben werden.

Jeder Aufruf klaut dem Programm (und System) freien Speicher.

Memory Leak a: Ein Programm allokiert Speicher und gibt ihm nicht mehr dem System zurück und

Memory Leak b: verliert die Referenz auf den Speicher und kann daher ihn nicht mehr dem System zurückgeben oder nutzen.

Memory Leaks

Eigentlich ist alles ganz einfach:

Jedem Aufruf von new (new[], oder malloc) muss ein Aufruf von delete (delete[] bzw. free) gegenüberstehen:

Memory Leaks

Aber nicht alle Funktionen verlaufen linear…

Memory Leaks

Aber selbst, wenn man alles richtig macht, ist der Code nicht optimal.

(1) und (2) = doppelter Code, der nicht exakt gleich ist.

Memory Leaks

In C++ können Funktionsrückgaben ignoriert werden.

Die Funktion f() gibt Speicher zurück, der freigegeben werden muss.

Dieser Rückgabewert kann ignoriert werden. -> Memory Leak!

Memory Leaks

Und manchmal sind die Datenstrukturen und der Code so komplex, dass nicht klar ist war, wann und wo den Speicher frei geben müsste.

Exceptions Safety

Memory Leaks++

Der Programmablauf ist nicht einfach zu durchschauen, wenn Exceptions ins Spiel kommen.

Exceptions Safety

Eine Behandlung der Exceptions macht das Programm nicht leserlicher.

Der Quellcode nicht einfacher zu lesen, man baut zusätzliche Abhängigkeiten ein, die man eigentlich durch die Verwendung von Exceptions vermeiden wollte und wieder wurde Code dupliziert.

Exceptions sollten für eine Trennung von Programmlogik und Fehlerbehandlung sorgen. Dies funktioniert bei (Raw-)Pointern nicht!

Exceptions Safety

Der Destruktor einer Klasse wird nur dann aufgerufen, wenn die Klasse vollständig erzeugt wurde, also der Konstruktor vollständig durchlaufen wurde.

(1) Was passiert, wenn loadPixelData eine Exception auslöst?

Pointer müssen initialisiert werden

Pointer besitzen nur 2 gültige Zustände:

Der Zustand "nicht initialisiert" ist für Pointer nicht vorgesehen. Ein Zeiger, der nicht initialisiert wurde kann nicht auf Gültigkeit geprüft werden und darf nicht freigegeben werden!

alter code:
sofort initialisiert:

Pointer müssen initialisiert bleiben

delete gibt nur den Speicher frei, verändert jedoch nicht den Zeiger.

Nach dem Aufruf von delete muss der Zeiger auf 0 gesetzt werden.

Lösungsvorschläge?

What about crashes, buffer overflows, memory leaks?

These issues are mostly in past for modern C++ development. Smart pointers, STL, Boost and other decent programming tools allow to write safe code easily and fast.

http://cppcms.com/wikipp/en/page/rationale

Was sind Smart-Pointer?

Smart-Pointer sind

Wie kann sich eine Klasse wie ein Zeiger verhalten?

C++-Klassen können wie Zeiger verwendet werden, wenn

überschrieben worden sind.

Das Überladen der Operatoren * und -> sind keine außergewöhnliche C++-Tricks.

Gleiche Technik wie bei den Iteratoren der STL!

So können die Objekte einer Smart-Pointer Klasse, überall dort eingesetzt werden, wo bisher Zeiger verwendet wurden.

Und als Klasse sorgt der Konstruktor für die Initialisierung

Wie "smart" ist ein Smart-Pointer? 1/2

Der Smart-Pointer ist der Besitzer für den allokierten Speicher

und damit Verantwortlich für die korrekt Freigabe.

Smart-Pointer verwenden das Resource Acqusiation is initialisation (RAII)-Prinzip um sicherzustellen, dass der Speicher wieder freigegeben wird.

RAII (Einschub)

Resource Acquisition Is Initialization

Bei der Initialisierung einer Instanz (object) wird eine Ressource angefordert. Die Ressource wird bei der Zerstörung der Instanz wieder freigegeben.

Beispiel:

Umständlich: (old style) Einfacher: (RAII)

RAII (Einschub)

Resource Acquisition Is Initialization

Unter C++ wird der Destruktor immer, in einer definierten Reihenfolge und "sofort" aufgerufen. Das gilt für:

Damit wird sichergestellt, dass eine Ressource zuverlässig freigegeben wird.

Wie "smart" ist ein Smart-Pointer? 2/2

Bei den Smart-Pointer liegt der Schwerpunkt im 2. Teil von RAII:

Der Destruktor sorgt zuverlässig für die Freigabe des Speichers!

Ein wichtiger Aspekt ist das Regeln der Besitzverhältnisse ("Ownership"): Also wer löscht den Speicher.

Beispiel

Anmerkung:

Beispiel für die Benutzung:

Wo findet man Smart-Pointer?

Oder anders gefragt: Muss sich jetzt jeder seine eigene Smart-Pointer programmieren?

Viele moderne (C++) Klassen Bibliotheken bringen Smart-Pointer mit:

oder

Welche Smart-Pointer gibt es?

Die Smart-Pointer können nach ihrem Kopierverhalten unterschieden werden. Dies sind:

3 klassische Typen

1 speziellen Typ

(Kopie im Sinne von "flache Kopie". Es wird der Zeiger und nicht der Inhalt kopiert)

Scoped-Pointer

Keine Kopie

Smart-Pointer, die keine Kopien oder Zuweisung zulassen.

Ohne Zuweisung und Kopie muss auch "nichts" zusätzlich verwaltet werden: Ein Smart-Pointer kümmert sich um einen Speicherbereich. Damit ist ein scoped_ptr so effektiv wie Hand-Programmierter Code.

Verwendung:

Der scoped_ptr wird zum Aufbewahren von Speicher verwendet, welcher beim Verlassen des Scopes wieder freigeben werden soll. Z.B.:

Beispiele:

Scoped-Pointer

std::unique_ptr

(scoped_ptr mit R-Value Unterstützung)

Objekt in STL Container müssen Kopierbar oder Verschiebbar sein. Der unique_ptr kann "verschoben" werden.

Verwendung: STL Container Funktionsrückgabe.

Shared-Pointer

Smart-Pointer mit interner Referenz-Zählung

Smartpointer, die Kopien erlauben, bzw. sich die "Ownership" teilen.

Wenn der letzte Zeiger für eine Speicheradresse stirbt, wird der Speicher freigegeben. ("Der letzte macht das Licht aus!")

Die übliche Implementierungstechnik verwendet einen internen Zähler (Reference counter).
Dabei kann dieser Smart-Pointer-Typ auch ohne Zähler implementiert werden.
(Beispiel: zyklische Kette, siehe Modern C++ Design Kapitel "Reference Linking".)

Beispiele:

Shared-Pointer

Verwendung

Die shared_ptr können vielfältig verwendet werden:

Eigentlich können shared_ptr fast überall dort verwendet werden, wo bisher herkömmliche Zeiger verwendet wurden. Die einzige Ausnahmen sind Zeiger auf Felder.

Shared-Pointer

Shared-Pointer

Shared-Pointer

Performance

Der std::shared_ptr kann sehr universell und flexibel eingesetzt werden.

Dies bedingt einen gewissen Overhead:

Lösung :

Eine "hand-programmierte" Speicherwaltung bringt ebenfalls ihren Overhead und ist ggf. Fehlerbehaftet.

weak_ptr

Smart-Pointer, der keine Verantwortung übernehmen will

Verweist auf den gleichen Speicher wie ein shared_ptr, ohne dafür Verantwortlich zu sein.

Indirekter Zugriff auf Speicher:

weak_ptr

Shared-Pointer

Smart-Pointer mit externer Referenz-Zählung

Smart-Pointer für COM-Pointer

AddRef und Release müssen unterstützt werden.

Der Smart-Pointer gibt den Speicher selbst nicht frei – er ruft AddRef() und Release() korrekt auf.

Das COM-Object muss mit einem internen Zähler erkennen, dass kein Zeiger mehr das Objekt referenziert.

Beispiele:

Smart-Pointer mit externer Referenz-Zählung

Smart-Pointer mit externer Referenz-Zählung

http://msdn.microsoft.com/en-us/library/vstudio/hh279683.aspx

std::auto_ptr

Besitzerwechsel beim kopieren

Besitz-Semantik: Der Besitzer gibt Speicher frei

Beim Kopieren wechselt der Besitz.

Wenn der Besitzer zerstört wird können andere auto_ptr auf ungültigen Speicher zeigen. (z.B. In STL Containeren nach dem Aufruf von std::sort)

std::auto_ptr (deprecated)

Besitzerwechsel beim kopieren

Vorteil:

Wird mit jedem C++-Compiler ausgeliefert!

Anmerkung:

Smart-Pointer für Felder

Smart-Pointer für Felder heißen

Smart-Pointer für Felder

Naja, nicht ganz. Natürlich sind die aufgezählten Typen Klassen für Felder und keine SP.

Selbstverständlich gibt es spezielle Smart-Pointer für Felder. Diese geben den verwalteten Speicher mit 'delete[]' statt 'delete' frei.

(Echte) Smart-Pointer (für Felder) verwalteten nur den Speicher, aber nicht die Größe des verwalteten Speichers.

Container verwalten Speicher und Größe. (z.B. size(), capacity(), …)

Smart-Pointer für Felder

1:1 Umstellung

std::vector und std::string müssen den Speicher am Stück allokieren. Daher eignen sie besonders gut sich für 1:1 Umstellungen, da die Adresse des 1. Element dem Zeiger auf den Buffer entspricht. API-Aufrufe, die einen Zeiger auf Felder erwarten, können so aufgerufen werden. (oder vector::data(), C++11)

String Semantik beachten

Zeiger auf ein char-Feld, das einen String repräsentieren, ersetzt man am sinnvollsten mit der Klasse std::string oder einer vergleichbaren String-Klasse.

Statt string::data() sollte string.c_str() verwendet werden (Null-Terminierung).

Verwenden von Smart-Pointer

Voraussetzungen

muss:
sollte:

Grenzen und Gefahren

zyklische Referenzen bei shared_ptr.

A zeigt auf B; B zeigt auf A

Export über Modul-Grenzen ist problematisch.

Der Speicher sollte von der gleichen C-Runtime freigegeben werden, die den Speicher allokiert hat.

Beispiele:

(Vergleichbare Probleme gibt es auch für Exceptions.)

Empfehlungen

Speicher im Konstruktor des Smart-Pointers anlegen.

Beispiel:

Empfehlungen

In STL-Container Smart-Pointer mit Referenz-Zählung verwenden

In (STL-)Container nur Smart-Pointer mit Referenz-Zählung verwenden (z.B. boost::shared_ptr). Alles andere ist gefährlich und nicht zukunftssicher.

Alternativen: (C++11: std::unique_ptr. Boost Pointer Container Library)

Container geben nur den eigenen Speicher frei, die Speicherfreigabe bei Container von Pointern ist fehlerträchtig. (Siehe Exception Safety)

Empfehlungen

Die semantisch richtigen Zeiger verwenden

Der shared_ptr könnte eigentlich alle vorhandenden Pointer ersetzen.

Durch die Verwendung eines scoped_ptr/unique_ptr kann man dem Pointer ein weiteres Attribut mit geben, das einen Hinweis gibt, dass dieser Zeiger diese Funktion / Klassen nie verlassen wird.

Damit hat man ein semantisches Mittel, dass die Verwendung des Zeigers beschreibt.

Empfehlungen

Funktionsrückgabe

In C und C++ kann der Rückgabewert einer Funktion ignoriert werden. D.H. man ruft eine Funktion (Unterprogramm mit Rückgabewert) auf und behandelt die Funktion wie eine Prozedure bzw. Subroutine (Unterprogramm ohne Rückgabewert).

Mit diesem Wissen darf man eigentlich keine Funktion programmieren, welche dynamisch erzeugten Speicher als Rückgabewert zurückgibt. Wenn der Rückgabewert ignoriert wird, kann dieser Speicher nicht mehr freigegeben werden.

Keine ungekapselten Pointer verwenden

Die Verwendung von rohem Speicher (echte C++/C-Pointer) verzichten.

Mit der Verwendung von Smart-Pointer kann auch in größere Projekt komplett auf den Aufruf von delete verzichtet werden.

TR1 und C++11

Seit dem C++11 (bzw. TR1) enhält C++ die wichtigsten Smart-Pointer.

Bis wenige Ausnahmen werden (für Smart-Pointer) keine weiteren Bibliotheken benötigt.

TR1
C++11
Gemeinsame Functionalität

TR1 und C++11

deleter and allocator

Functor-like Klasse um Benutzer definierte Freigabe-Funktion und Allokation zu steuern.

Der deleter wird von reset und dem Destruktor aufgerufen.

Beispiel:

Mit dem Allocator wird z.B. der Speicher für das Zusatzobjekt (count) beim shared_ptr angelegt.

unique_ptr

Smart-Pointer ohne Kopie-Verwaltung (scoped_ptr), aber mit RValue Unterstützung.

shared_ptr

Smart-Pointer bei dem viele Instanzen auf den gleichen Speicher zeigen können.

shared_ptr

Nicht member Funktionen

Diesmal ist MyObject die Basis Klasse von OtherObject.

weak_ptr

Teil sich mit einem shared_ptr den Speicher ohne Ownership.

enable_shared_from_this

shared_from_this: gibt einen shared_ptr from this zurück, wenn Klasse von enable_shared_from_this abgeleitet ist.

Der Originale Zeiger muss bereite durch einen shared_ptr verwaltet werden; shared_from_this erzeugt keinen neuen shared_ptr.

shared_ptr

Beispiel

(1) : d-tor : root
d-tor : first
d-tor : first
d-tor : second
d-tor : third

Beispiele

vorher: nachher:

Beispiele

vorher: nachher:

Beispiele

vorher: nachher:

Beispiele

nachher:vorher:

Links

Created the help of

/

#