Garbage Collection - No thanks

C++ ist eine so altmodische Programmiersprache. Sie unterstützt kein Garbage Collection. Kein Garbage Collection? Stimmt! Altmodisch? Stimmt nicht!

 

Was spricht gegen Garbage Collection in C++? Zuallererst widerspricht Garbage Collection einem der wichtigsten Prinzipien in C++: "Don't pay for something you don't use". Das heißt, wenn das Programm kein Garbage Collection benötigt, soll die C++ Laufzeit auch nicht damit beschäftigt sein, den Speicher aufzuräumen. Meine zweite Erwiderung ist schon ein wenig anspruchsvoller.

Wir besitzen in C++ RAII und damit die vollkommen deterministische Ausführung der Destruktoren von Objekten. Doch was ist RAII? Genau darum dreht sich der Artikel.

Rescource Acquisition Is Initialization

RAII steht für Rescource Acquisition Is Initialization. Dieses, wohl wichtigste C++ Idiom besagt, dass eine Ressource in Konstruktor eines Objektes angefordert und im Destruktor des Objektes wieder freigegeben wird. Das Entscheidende dabei ist, dass der Destruktor genau dann automatisch aufgerufen wird, wenn das Objekt seine Gültigkeit verliert. Wenn das nicht vollkommen deterministisch ist? Die Garantie geben Destruktoren in Java oder Python (__del__) nicht. Daher kann es tödlich sein, in Python einen Destruktor zu verwenden, eine kritische Ressource wie ein Lock wieder freizugeben. Ein klassisches Anti-Pattern für einen Deadlock. Das gilt aber nicht für C++. 

Zuerst ein Beispiel, dass das deterministische Verhalten von RAII auf den Punkt bringt. 

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// raii.cpp

#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;

}

 

ResourceGuard repräsentiert einen Wächter, der auf die ihm anvertraute Ressource verwaltet. In dem konkreten Fall ist die Ressource nur als String angedeutet. ResourceGuard legt in seinem Konstruktor (Zeile 11 - 13) seine Ressource an und gibt sie in seinem Destruktor (Zeile 14 - 16). Seine Aufgabe macht er sehr zuverlässig.

So wird der Destruktor von resGuard1 (Zeile 23) genau nach dem Ende der main-Funktion (Zeile 46). Die Lebenszeit von resGuard2 (Zeile 27) endet schon in der Zeile 28. Dies führt automatische zum Aufruf seines Destruktors. Selbst eine Ausnahme ändert nichts an der Zuverlässigkeit von resGuard3 (Zeile 36). Seine Destruktor wird bei dem Verlassen des try-Blocks (Zeile 35 - 38) aufgerufen.

Der Screenshot des Programms zeigt schön die Lebenszyklen der Objekte.

raii

Ich will nochmals auf das Programm raii.cpp zurückkommen. Was ist die entscheidende Idee des RAII-Idioms? Der Lebenszyklus einer Ressource wird an den Lebensyzklus einer lokalen Variable gebunden. C++ verwaltet automatisch den Lebenszyklus seiner lokalen Ressource.

Die Idee ist einfach, besitzt aber weitreichende Konsequenzen für die Programmiersprache C++. Kritische Ressourcen werden in lokale Objekte verpackt. Den Rest der Arbeit übernimmt die C++-Laufzeit.

RAII überall

Dies trifft auf die Locks std::lock_guard, std::unique_lock und std::shared_lock, die eine Mutex verwalten, zu. Dies trifft aber auch auf die Smart Pointer std::unique_ptr, std::shared_ptr und std::weak_ptr, die in der Regel Speicher verwalten, zu.

So kommt durch die Hintertür RAII doch noch Garbage Collection in der Form von automatischen, explizitem Speichermanagement in den C++-Sprachumfang.

Es gibt aber zwei feine Unterschied zur allgemeinen Garbage Collection.

  1. Das Speichermangement mit Smart Pointer muss explizit angefordert werden.
  2. Das Speichermanagment mit std::unique_ptr besitzt keinen Overhead in Geschwindigkeit und Speicher gegenüber einem einfachen Zeiger. (siehe Speicher und Performanz Overhead von Smart Pointern)

Damit bleibt C++ seinem wichtigen Prinzip in doppelter Hinsicht treu. Don't pay for something you don't use.

Besondere Ressourcen

Dank RAII wird der Destruktor des lokalen Objektes und damit die Aufräumfunktion der Ressource absolut deterministisch aufgerufen. So weit, so gut. Kann die Aufräumfunktion aber eine Ausnahme werfen, wird RAII diese Ausnahme durch seinen Destruktoraufruf immer anstoßen. Dies ist zum Beispiel der Fall, wenn die Ressource eine Datei ist. In diesem Fall kann die close-Funktion eine Ausnahme auslösen. Da stellt sich natürlich die Frage, ob es tolerierbar ist, dass der Destruktor eine Ausnahme werfen kann oder ob in diesem Fall RAII nicht verwendet werden soll. Diese Entscheidung muss natürlich im konkreten Einzelfall getroffen werden.

Umgang mit werfenden Destruktoren (Udo Steinbach)

Das Problem der werfenden Destruktoren hat Udo Steinbach deutlich beschrieben. Daher möchte ich seine E-Mail hier zitieren. Kleine Anmerkungen habe ich in runden Klammern eingefügt.

RAII ist eine nützliche Sache - solange keine Fehler auftreten können. Letzteres wird beim Frohlocken über RAII leider oft vergessen. Warum ein Destruktor nicht werfen sollte, kann man an vielen Stellen https://www.qwant.com/?q=should%20destructors%20throw nachlesen. Die Folge ist, das RAII in vielen Fällen manuell ergänzt werden muß und damit doppelt gemoppelt scheint. 

class MyFileHandle
{  public:
      MyFileHandle(...)
         :handle(::OpenFile(...))
      {  if (handle == nullptr)
            throw ...;
      }
      ~MyFileHandle() noexcept
      {  ::CloseFile(handle);
      }
   private:
      MySystemHandle handle;
};


{
   MyFileHandle file(...);
   ...
}

Verweigert CloseFile() das Schließen, wird (eine) korrekte Funktion vorgetäuscht, das Handle ist verloren, der Benutzer muss das Programm neu starten und mitunter die Datei selbst suchen und löschen, oder andere peinliche Symptome, wie sie aus Anwendungsprogrammen allzu bekannt sind.
Also muß die Klasse um ein werfendes

void Close();

ergänzt werden und der Destruktor überprüfen:

{ 
MyFileHandle file(...)
...
file.Close();
}

 

Das sieht schon weniger nach RAII aus. Um eine Symmetrie herzustellen, scheint ein manuelles Open() sinnvoll:

{
MyFileHandle file;
file.Open(...);
...
file.Close();
}

 

... RAII perdu. Für den Liebhaber bleibt immerhin der Trost, dass das Objekt nun wiederverwendbar ist und das sowohl für den Fehlerfall Vorkehrungen getroffen wurden und es ansonsten korrekt läuft.

Unter der Maßgabe einer ordentlichen Fehlerbehandlung aus Sicht des Programm-Benutzers verzichte ich bei vielen meiner Klassen auf RAII. Modultests nach einer Idee von http://www.boost.org/community/exception_safety.html zeigen

  • mindestens grundlegende Ausnahmesicherheit http://en.wikipedia.org/wiki/Exception_safety ,
  • bei ordentlcher Fehlerbehandlung, auf die bei RAII ja verzichtet werden muß, z.B. automatisches Löschen unvollständiger Dateien und weiter werfen der Ausnahme,
  • und dem Benutzer präsentierbare Fehlermeldungen,

ein stets bestmögliches Verhalten des Programms: Mache Benutzer und Support glücklich durch das Ersetzen von Absturz und Datenmüll durch aussagekräftige Meldungen.

Ein Automatismus, hier Destruktor oder Garbage Collector, kann Fehler nur automatisch behandeln, also ignorieren oder minimalistisch. In Anwendungsprogrammen sollte das nicht akzeptabel sein, und muss es auch nicht.

Die berühmten letzten Worte von Bjarne Stroustrup

BjarneStroustrup

Bjarne Stroustrup schrieb einen kurzen Kommentar zu meinen News auf C++Enthusiasts:

"Things are still improving": http://www.stroustrup.com/resource-model.pdf

 

Um was geht es in dem zitieren Artikel, der von Bjarne Stroustrup, Herb Sutter und Gabriel Dos Reis verfasst wurde. Hier ist ein Screenshot. Du musst den Artikel aber schon selbst lesen.

paper

 

Wie geht's weiter?

Mit den nächsten Artikel betreten wie ein Bereich, der den C++-Experten vorbehalten sein sollte. Dem expliziten Speichermanagement mit C++.

 

 

 

 

 

 

title page smalltitle page small Go to Leanpub/cpplibrary "What every professional C++ programmer should know about the C++ standard library".   Hole dir dein E-Book. Unterstütze meinen Blog.

 

Tags: Speicher, RAII

Kommentare   

-5 #1 Rik 2020-04-15 10:30
Unglaublich kompliziert! Kein Wunder, dass heute noch C für Linux und alles, was unmittelbar auf der Maschine aufsitzt, verwendet wird! Wie viele Jahre an kostbarer Lebenszeit haben Sie wohl mit C++ verschwendet?! Die einzige Motivation für mich, C++ zu verwenden, wäre das Entwickeln von Anwendungen mit nativer Benutzeroberfläche in Qt, wobei Qt mittlerweile für Python offizielle Bindungen bietet, sodass auch hier C++ langfristig zur bloßen Backend-Sprache verkommt (wie es schon bei Game-Engines der Fall ist). Ich denke auch nicht, dass Bibliotheken in C++ so langlebig sein werden wie Fortran-Programme, einfach, weil C++ unwartbar geworden ist und eine Syntax vorweist, welche Jenseits von Gut und Böse liegt.
Langfristig wird C++ an seiner monströsen Komplexität ersticken;
Zitieren

Mentoring

Stay Informed about my Mentoring

 

Rezensionen

Tutorial

Besucher

Heute 2823

Gestern 2405

Woche 11029

Monat 38346

Insgesamt 3891060

Aktuell sind 53 Gäste und keine Mitglieder online

Kubik-Rubik Joomla! Extensions

Abonniere den Newsletter (+ pdf Päckchen)

Beiträge-Archiv

Sourcecode

Neuste Kommentare