Einen der wichtigsten Aspekte beim Programmieren stellt für C++-Entwickler die Ressourcenverwaltung dar. Zum Glück gibt es dafür Richtlinien.
Die C++ Core Guidelines bieten Regeln für die Ressourcenverwaltung im Allgemeinen an, aber auch solche für das Anfordern und Freigeben von Speicher und Smart Pointern im Besonderen. Doch am Anfang steht die Frage: Was genau ist eine Ressource?
Dabei handelt es sich gewöhnlich um externe Objekte, beispielsweise um Speicher- und Compute-Ressourcen, auf die der Code zugreifen möchte. In der Regel sind diese Ressourcen knappe Güter oder brauchen Schutz. Zum Beispiel kann eine Anwendung nur eine begrenzte Menge an Speicher, Sockets, Prozessen oder auch Threads anfordern.
Eine Ressource gilt es zu verwalten. Das bedeutet, dass ein Programm eine Ressource anfordern und wieder freigeben muss. Nur genau ein Prozess darf dabei auf seine Ressourcendatei zugreifen, nur ein Thread darf seine geteilte Ressource zu einem Zeitpunkt verändern. Wer diese Grenzen ignoriert, den erwarten viele böse Überraschungen:
- Einem Programm geht der Speicher aus, weil es ihn nicht freigegeben hat.
- Im Programm steckt ein Data Race, weil es auf eine geteilte Variable zeitgleich lesend zugreifen will.
- Ein Programm enthält einen Deadlock, weil es einen Mutex nicht freigegeben hat.
Data Races und Deadlocks sind aber keine Domäne geteilter Variablen. Sie kommen immer dann zustande, wenn verschiedene Akteure auf eine gemeinsame Variable modifizierend zugreifen möchten. Bei diesen Akteuren kann es sich um Prozesse handeln, die eine Datei schreiben wollen oder verteilte Netzwerkabfragen auf Datenbanken vornehmen.
Wer ist der Besitzer?
Denkt der Entwickler intensiver über Ressourcenverwaltung nach, reduziert sich die Anforderung auf eine einfache Frage: Wer ist der Besitzer? Er lässt sich mit modernem C++ sehr explizit adressieren. Vereinfachend formuliert, unterscheidet C++ sechs verschiedene Besitzverhältnisse:
- Lokale Objekte: Die C++-Laufzeitumgebung (Stack) als Besitzer verwaltet automatisch den Lebenszyklus ihrer Ressourcen. Das gilt auch für globale Objekte oder Mitglieder einer Klasse. Die Guidelines nennen diese lokalen Objekte Scoped Objects.
- Nackte Zeiger: Ein nackter Zeiger ist kein Besitzer, sondern hat sich die Ressource nur ausgeliehen. Als Nicht-Besitzer darf er die Ressource nicht freigeben. Der Entwickler muss den Zeiger vor jedem Einsatz überprüfen, da es sich um einen Null-Zeiger handeln kann.
- Referenzen: Bei einer Referenz handelt es sich ebenfalls nicht um einen Besitzer. Sie hat sich die Ressource, die nicht null sein kann, nur ausgeliehen.
- »std::unique_ptr«: Ein »std::unique_ptr« fungiert als exklusiver Besitzer der Ressource. Verliert er seine Gültigkeit, räumt er automatisch auf.
- »std::shared_ptr«: Ein »std::shared_ptr« agiert als teilhabender Besitzer der Ressource. Der letzte Teilhabende, der sein Besitzverhältnis an der Ressource aufgibt, räumt auch automatisch auf.
- »std::weak_ptr«: Ein »std::weak_ptr« ist kein Besitzer einer Ressource, sondern hat sie sich nur geliehen. Er kann zum Teilhaber werden, indem er die Methode »std::weak_ptr::lock« aufruft.
Dieses fein abgestufte Konzept von Besitzverhältnissen zeichnet modernes C++ aus. Die Programmiersprache C bildet zum Beispiel sämtliche Besitzverhältnisse mit Ausnahme lokaler Objekte durch einen Zeiger ab. Das führt dazu, dass der Besitzer eines Zeigers nicht weiß, ob er die Ressource freigeben darf. Als Besitzer muss er die Ressource löschen; ist er es nicht, darf er sie nicht löschen. C-Entwickler müssen die intendierten Besitzverhältnisse also sehr detailliert im Code abbilden und genau beschreiben, wer eine Ressource löschen darf und wer nicht.
Doch auch klassisches C++ hilft bei ungeklärten Besitzverhältnissen nicht wirklich, da es nur nackte Zeiger und Referenzen anbietet. Die sechs verschiedenen Stufen der Besitzverhältnisse ziehen sich als roter Faden durch viele Regeln der C++ Core Guidelines – ein Faden, den dieser und der nächste Artikel aufrollen.
Zunächst einmal gibt es aber einen Appetithappen für ungeduldige Leser. Die älteren Artikel “Räumkommando” [1] und “Klug aufgeräumt” [2] gehen detailliert auf die expliziten Besitzer »std::auto_ptr« und »std::unique_ptr« sowie die geteilten Besitzer »std::shared_ptr« und »std::weak_ptr« ein.
Die erste Wahl, um Besitzverhältnisse in C++ zu klären, sollten lokale Objekte sein. Basierend auf dieser Idee hat sich in der Programmiersprache ein Idiom entwickelt, das die Sprache charakterisiert: RAII.
RAII
Das Akronym RAII, das auf Bjarne Stroustrup zurückgeht, steht für Resource Acquisition Is Initialization. Das bringt den Artikel schon mitten in die erste Regel [3] der C++ Core Guidelines zum Thema.
Die RAII zugrundeliegende Idee ist verblüffend einfach: Ein Stellvertreterobjekt kümmert sich um eine Ressource; der Konstruktor des Stellvertreters fordert die Ressource an, der Destruktor gibt sie wieder frei. Die zentrale Idee des RAII-Idioms besteht darin, dass es sich bei diesem Stellvertreter um ein lokales Objekt handelt. Da die C++-Laufzeitumgebung als Besitzer lokaler Objekte fungiert, gehört ihr damit auch die Ressource.
Typische Beispiele für das RAII-Idiom sind die Container der Standard Template Library. So räumt ein »std::vector« automatisch den von ihm verwalteten Speicher auf, sobald sein Gültigkeitsbereich endet. Denselben Mehrwert bieten ein »std::string« oder die bereits zitierten Smart Pointer. Im Gegensatz dazu gibt ein Lock als eine weitere Umsetzung des RAII-Idioms seinen Mutex automatisch frei.
Die Klasse »ResourceGuard« in Listing 1 setzt exemplarisch das RAII-Idiom um. Die Ausgabe des Strings »resource« täuscht das Anfordern und Freigeben der Ressource im Konstruktor beziehungsweise Destruktor lediglich vor. Das Programm ruft den Destruktor unabhängig davon auf, ob der Lebenszyklus der »ResourceGuard«-Instanzen von »resGuard1« (Zeile 21) am Ende des »main«-Programms (Zeile 44) oder am Ende des lokalen Bereichs (Zeile 26) für »resGuard2« endet. Diese Zusicherung gilt auch, falls eine Ausnahme auftritt.
Listing 1
RAII
#include <iostream>
#include <new>
#include <string>
class ResourceGuard{
private:
const std::string resource;
public:
ResourceGuard(const std::string& res):resource(res){
std::cout << "Acquire the " << resource << "." << std::endl;
}
~ResourceGuard(){
std::cout << "Release the "<< resource << "." << std::endl;
}
};
int main(){
std::cout << std::endl;
ResourceGuard resGuard1{"memoryBlock1"};
std::cout << "\nBefore local scope" << std::endl;
{
ResourceGuard resGuard2{"memoryBlock2"};
}
std::cout << "After local scope" << std::endl;
std::cout << std::endl;
std::cout << "\nBefore try-catch block" << std::endl;
try{
ResourceGuard resGuard3{"memoryBlock3"};
throw std::bad_alloc();
}
catch (std::bad_alloc& e){
std::cout << e.what();
}
std::cout << "\nAfter try-catch block" << std::endl;
std::cout << std::endl;
}
Den Destruktor von »resGuard3« ruft das Programm automatisch auf. Dieses deterministische Destruktionsverhalten unterscheidet sich deutlich von einer allgemeinen Garbage Collection wie der in Python oder Java. Verlässt ein Objekt in einer dieser Sprachen seinen Gültigkeitsbereich, merken die Sprachen es lediglich zur Destruktion vor.
Die Ausgabe des Programms sorgt dafür, dass sich der Lebenszyklus der Instanzen von »ResourceGuard« in Abbildung 1 verfolgen lässt. Um aber den zentralen Faden dieses Artikels wieder aufzugreifen: Weder nackte Zeiger [4] noch Referenzen sollten Besitzer [5] sein.

Abbildung 1: Ein Beispiel für den Lebenszyklus von »ResourceGuard«-Instanzen.
Einmaleins von Fabrikfunktionen
Eine Fabrikfunktion veranschaulicht die verschiedenen Konzepte von Besitzverhältnissen. Diese spezielle Funktion erzeugt ein neues Objekt und gibt es zurück. Die Frage lautet nun: Soll eine Fabrikfunktion einen nackten Zeiger, ein Objekt, einen »std::unique_ptr« oder einen »std::shared_ptr« zurückgeben? Der Codeschnipsel in Listing 2 stellt vier Variationen vor.
Listing 2
Fabrikfunktionen mit mehreren Rückgabewerten
Widget* makeWidget(int n){
auto p = new Widget{n};
// [...]
return p;
}
Widget makeWidget(int n){
Widget g{n};
// [...]
return g;
}
std::unique_ptr<Widget> makeWidget(int n){
auto u = std::make_unique<Widget>(n);
// [...]
return u;
}
std::shared_ptr<Widget> makeWidget(int n){
auto s = std::make_shared<Widget>(n);
// [...]
return s;
}
[...]
auto widget = makeWidget(10);
Wer soll der Besitzer der Widgets sein – der Aufrufer oder der Aufgerufene? Diese Frage bleibt für den nackten Zeiger (Zeile 1) unentschieden. Das bedeutet, es bleibt unklar, wer das »Widget« letztlich löschen soll.
Im Gegensatz dazu sind die Fälle in den Zeilen 7, 13 und 19 offensichtlich. Im Fall des Objekts oder des »std::unique_ptr« ist der Aufrufer der Besitzer, beim »std::shared_ptr« teilen sich der Aufrufer und der Aufgerufene das Besitzrecht. Es gibt jedoch noch mehr Argumente, die für die eine oder andere Lösung sprechen:
- Eine Fabrikfunktion, die einen virtuellen Konstruktor implementieren soll, muss als Rückgabetyp einen Smart Pointer verwenden.
- Soll der Aufrufer der Besitzer des Widgets sein, bieten sich ein Objekt oder ein »std::unique_ptr« an. Für das Objekt als Rückgabewert spricht, dass es sich effizient kopieren lässt.
- Will der Aufgerufene (die Fabrikfunktion) den Lebenszyklus seiner Widgets verwalten, muss der Entwickler einen »std::shared_ptr« einsetzen.
Der feine Unterschied
Mehrere Regeln der C++ Core Guidelines beschäftigen sich zudem mit »new« und »delete«. So empfiehlt die Regel R.10 [6], Speicheranforderungen mittels »malloc()« und Speicheranfragen via »free()« zu vermeiden. Um die Frage beantworten zu können, warum das so ist, braucht man ein wenig Hintergrundwissen.
Das Erzeugen eines Objekts mit »new« besteht in C++ aus zwei Schritten. Im ersten fordert das Programm Speicher für das Objekt an; im zweiten initialisiert es das Objekt im zuvor angeforderten Speicherbereich. Die Operatoren »new« oder »new []« übernehmen den ersten Schritt, der Konstruktor den zweiten. Dieselbe Strategie greift beim Zerstören des Objekts, allerdings in umgekehrter Reihenfolge: Zuerst ruft das Programm den Destruktor auf, dann gibt es den Speicher mittels des Operators »delete« oder »delete []« frei. Da stellt sich die Frage nach dem Unterschied zwischen »new« und »malloc()« beziehungsweise »delete« und »free()«.
Die C-Funktionen »malloc()« und »free()« erledigen nur die Hälfte ihres Jobs. Erstere fordert lediglich den Speicher an, den Letztere wieder freigibt. Weder ruft »malloc()« den Konstruktor auf noch »free()« den Destruktor. Wer also ein Objekt verwendet, das »malloc()« erzeugt hat, erhält ein undefiniertes Verhalten. Listing 3 bringt diese entscheidende Differenz auf den Punkt.
Listing 3
malloc() versus new
#include <iostream>
#include <string>
struct Record{
Record(std::string na = "Record"): name(na){}
std::string name;
};
int main(){
std::cout << std::endl;
Record* p1 = static_cast<Record*>(malloc(sizeof(Record)));
std::cout << p1->name << std::endl;
auto p2 = new Record;
std::cout << p2->name << std::endl;
std::cout << std::endl;
}
Zeile 13 fordert lediglich Speicher für ein »Record«-Objekt an. Als Ergebnis erzeugt die Ausgabe »p1->name« in Zeile 14 undefiniertes Verhalten. Das bedeutet schlicht, dass sich keine verbindlichen Aussagen mehr zum Programmverhalten treffen lassen. Beim Autor generiert das Programm einen Core Dump (Abbildung 2). Im Gegensatz dazu ist der Ausdruck in Zeile 16 wohldefiniert und stößt den Konstruktor in Zeile 5 an.

Abbildung 2: Dank undefinierten Verhaltens beendet sich das Programm vorzeitig mit einem Core Dump.
Ausblick
Diese Folge der Reihe setzte sich relativ allgemein mit dem sorgfältigen Umgang mit Ressourcen auseinander. Der nächste Artikel der Serie widmet sich deutlich detaillierter den Smart Pointern. (kki)
Der Autor
Der C++- und Python-Trainer Rainer Grimm hat bei O’Reilly und Leanpub zahlreiche Bücher zu C++ veröffentlicht, zuletzt “The C++ Standard Library” und “Concurrency with modern C++”.
Infos
-
C++11: Rainer Grimm, “Räumkommando”, LM 02/2013, S. 90, https://www.linux-magazin.de/27527
-
C++11: Rainer Grimm, “Klug aufgeräumt”, LM 04/2013, S. 104, https://www.linux-magazin.de/28308
-
Regel R.1: http://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Rr-raii
-
Regel R.3: http://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Rr-ptr
-
Regel R.4: http://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Rr-ref
-
Regel R.10: http://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Rr-mallocfree






