Bereits Kunde? Jetzt einloggen.
Lesezeit ca. 6 Min.

C++ Core Guidelines – Folge 50: Auf Sparflamme


Linux Magazin - epaper ⋅ Ausgabe 2/2020 vom 02.01.2020

Einen der wichtigsten Aspekte beim Programmieren stellt für C++-Entwickler die Ressourcenverwaltung dar. Zum Glück gibt es auch für diesen Bereich hilfreiche Richtlinien.


Artikelbild für den Artikel "C++ Core Guidelines – Folge 50: Auf Sparflamme" aus der Ausgabe 2/2020 von Linux Magazin. Dieses epaper sofort kaufen oder online lesen mit der Zeitschriften-Flatrate United Kiosk NEWS.

Bildquelle: Linux Magazin, Ausgabe 2/2020

Volodymyr Tverdokhlib, 123RF

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 was genau ist eine Ressource?
Dabei handelt es sich gewöhnlich um externe Objekte, etwa um Speicher- und Compute-Ressourcen, auf die der Code zugreifen will. Meist sind diese knappe Güter, oder sie brauchen Schutz. So kann ...

Weiterlesen
epaper-Einzelheft 5,99€
NEWS 14 Tage gratis testen
Bereits gekauft?Anmelden & Lesen
Leseprobe: Abdruck mit freundlicher Genehmigung von Linux Magazin. Alle Rechte vorbehalten.

Mehr aus dieser Ausgabe

Titelbild der Ausgabe 2/2020 von Die Alleswisser. Zeitschriften als Abo oder epaper bei United Kiosk online kaufen.
Die Alleswisser
Titelbild der Ausgabe 2/2020 von News. Zeitschriften als Abo oder epaper bei United Kiosk online kaufen.
News
Titelbild der Ausgabe 2/2020 von Zahlen & Trends. Zeitschriften als Abo oder epaper bei United Kiosk online kaufen.
Zahlen & Trends
Titelbild der Ausgabe 2/2020 von Bericht von der VMworld 2019 in Barcelona: In Richtung Pazifik. Zeitschriften als Abo oder epaper bei United Kiosk online kaufen.
Bericht von der VMworld 2019 in Barcelona: In Richtung Pazifik
Titelbild der Ausgabe 2/2020 von Kernel 5.4: ExFAT in Staging, Lockdown-Modus: Neues Sperrgebiet. Zeitschriften als Abo oder epaper bei United Kiosk online kaufen.
Kernel 5.4: ExFAT in Staging, Lockdown-Modus: Neues Sperrgebiet
Titelbild der Ausgabe 2/2020 von Sicher auf Kubernetes zugreifen: Fallen vermeiden. Zeitschriften als Abo oder epaper bei United Kiosk online kaufen.
Sicher auf Kubernetes zugreifen: Fallen vermeiden
Vorheriger Artikel
E-Privacy-Verordnung: Ratloses Nutzertracking
aus dieser Ausgabe
Nächster Artikel Sortieralgorithmen in Go vorgestellt: Sortierte Welt
aus dieser Ausgabe

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 was genau ist eine Ressource?
Dabei handelt es sich gewöhnlich um externe Objekte, etwa um Speicher- und Compute-Ressourcen, auf die der Code zugreifen will. Meist sind diese knappe Güter, oder sie brauchen Schutz. So 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.
■Ein Programm erzeugt ein Data Race, wenn es zeitgleich mit anderen auf eine geteilte Variable zugreifen will.
■Ein nicht freigegebener Deadlock erzeugt einen Mutex.
Data Races und Deadlocks sind keine Domäne geteilter Variablen, sondern treten auch auf, wenn mehrere Akteure eine Variable simultan modifizieren. Dabei kann es sich um Prozesse handeln, die in eine Datei schreiben oder verteilte Netzwerkabfragen auf Datenbanken.

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 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.

Der Autor
Rainer Grimm ist Trainer für C++ und Python. Seine zahlreichen C++-Bücher, zuletzt „The C++ Standard Library“ und „Concurrency with modern C++“, sind bei O’Reilly und Leanpub erschienen.

■»std::weak_ptr«: Ein »std::weak_ptr« ist kein Besitzer einer Ressource, sondern hat sie nur geliehen. Er wird zum Teilhaber, wenn 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 C++ 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 zu diesem Thema.
Die RAII zugrunde liegende 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 RAIIIdioms 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.
Als typische Beispiele für das RAII-Idiom lassen sich die Container der Standard Template Library nennen. 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 im 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.

Abbildung 1: Ein Beispiel für den Lebenszyklus von »ResourceGuard«-Instanzen.


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 aufzu- greifen: Weder nackte Zeiger [4] noch Referenzen sollten Besitzer [5] sein.

Fabrikfunktionen 1x1

Eine Fabrikfunktion veranschaulicht die verschiedenen Konzepte von Besitzverhältnissen. Sie 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.
Wer soll der Besitzer der Widgets sein – der Aufrufer oder der Aufgerufene? Die Frage bleibt für den nackten Zeiger (Zeile 1) unentschieden. Es bleibt unklar, wer das »Widget« löschen soll.
Konträr dazu sind die Fälle in den Zeilen 7, 13 und 19 offensichtlich. Beim Objekt oder »std::unique_ptr« ist der Aufrufer der Besitzer, beim »std::shared_ptr« teilen sich Aufrufer und Aufgerufener das Besitzrecht. Noch weitere Argumente sprechen für die eine oder andere Lösung:
■Eine Fabrikfunktion, die einen virtuellen Konstruktor implementieren soll, muss als Rückgabetyp einen Smart Pointer verwenden.

■Soll der Aufrufer der Besitzer des Widgets sein, bietet 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 zu beantworten, warum das so ist, ist ein wenig Hintergrundwissen erforderlich.
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.
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. Im Gegensatz dazu ist der Ausdruck in Zeile 16 wohldefiniert und stößt den Konstruktor in Zeile 5 an.

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)

Infos

[1] C++11: Rainer Grimm, „Räumkommando“, LM 02/ 2013, S. 90, [https:// www. linux-magazin. de/ 27527]
[2] C++11: Rainer Grimm, „Klug aufgeräumt“, LM 04/ 2013, S. 104,
[https:// www. linux-magazin. de/ 28308]
[3] Regel R.1: [http:// isocpp. github. io/ CppCoreGuidelines/ CppCoreGuidelines# Rr-raii]
[4] Regel R.3: [http:// isocpp. github. io/ CppCoreGuidelines/ CppCoreGuidelines# Rr-ptr]
[5] Regel R.4: [http:// isocpp. github. io/ CppCoreGuidelines/ CppCoreGuidelines# Rr-ref]
[6] Regel R.10: [http://isocpp. github. io/ CppCoreGuidelines/ CppCoreGuidelines# Rr-mallocfree]