Garbage Collection - No thanks

Inhaltsverzeichnis[Anzeigen]

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 erst 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. Dies wohl wichtigste C++ Idiom besagt, das 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 nicht 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, das der Destruktor ein Ausname 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 muß 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, daß 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 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 muß 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   

0 #1 Udo 2016-07-05 16:30
Leider wird bei RAII dessen Fehlerträchtigkeit vergessen. Freigeben, löschen, zerstören mag für Speicher garantiert fehlerfrei enden, nicht aber bei anderen Objekten, etwa Dateien. Wenn also der Destructor nicht werfen soll, ignoriert man Fehler oder RAII (Close() immer auch manuell aufrufen).
Zitieren
0 #2 Rainer Grimm 2016-07-05 19:41
zitiere Udo:
Leider wird bei RAII dessen Fehlerträchtigkeit vergessen. Freigeben, löschen, zerstören mag für Speicher garantiert fehlerfrei enden, nicht aber bei anderen Objekten, etwa Dateien. Wenn also der Destructor nicht werfen soll, ignoriert man Fehler oder RAII (Close() immer auch manuell aufrufen).
Vielne Dank für die Anmerkung. Für mich spricht dies aber nicht gegen RAII, sondern gegen die spezielle Objekte, die beim Aufräumen eine Ausnahme werrfen können. RAII macht seine Sache sehr gut. Seine Ressource vollkommen deterministisch aufräumen. Für mich ist nicht RAII fehleranfällig, sondern close(). Ich sehe das Problem nicht so kritiisch, da mindestens 99% aller Anwendungsfälle von RAII Smart Pointer und Locks sind. Unter den restlichen 1% gibt es kritische Kandidaten wie Dateien oder Sockets. Hier stellt sich natürlich die Frage, ob tolerierbar ist, dass diese Objekte beim Aufräumen werfen. Ich neige zu ja. Den Artikel habe ich angepasst.
Zitieren
+1 #3 Udo 2016-07-07 14:07
Fehlerträchtig = Genau aufpassen, wofür man das verwendet.

> ob tolerierbar ist, dass diese Objekte beim Aufräumen werfen. Ich neige zu ja

Ich nicht, ich finde es peinlich, wenn ich ein Programm nach einer Fehlermeldung neu starten oder Dateien löschen muß — Was ja noch eine harmlose Folge ist. Leider befaßt sich kaum jemand mit Thema Fehlerbehandlung über Exceptions und RAII hinaus. Es wird unterschätzt. Aber das nur nebenbei.
Zitieren
0 #4 Udo 2016-07-22 07:42
Erst jetzt fällt mir ein Schlußsatz ein: Ein Automatismus, hier Destruktor oder Garbage Collector, kann Fehler nur automatisch behandeln, also ignorieren oder minimalistisch. In Anwendungsprogrammen sollte das nicht akzeptabel sein, und muß es auch nicht.
Zitieren
0 #5 karts en Toledo 2017-03-06 04:17
Your way of explaining all in this paragraph is actually nice, every one can simply
be aware of it, Thanks a lot.
Zitieren
0 #6 Anya 2017-03-08 04:04
http://www.grimm-jaud.de is very informative, bookmarked
Zitieren

Kommentar schreiben


Abonniere den Newsletter (+ pdf Päckchen)

Beiträge-Archiv

Sourcecode

Neuste Kommentare