Ein Ausnahmetalent

Jedes Programm muss mit Fehlerbedingungen klarkommen. Die althergebrachte Weise zur Signalisierung eines Fehlers sind entsprechende Codes als Rückgabewerte von Funktionen bspw. Das ist nicht in jedem Fall sinnvoll/praktisch. Zum einen muss je nach Funktion ein Teil des Wertebereichs des Rückgabewertes für Fehlercodes abgezwackt werden und zum anderen kann eine Behandlung des Fehlers immer nur an der Stelle des Aufrufs erfolgen. C++ stellt mit Exceptions eine Möglichkeit bereit, die komplett anders funktioniert: Objekte werden als Ausnahmen “geworfen” und in entsprechenden Exception-Handlern “gefangen” um behandelt zu werden.

Video

Code

 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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
#include <string>
#include <iostream>

class NoValueException
{
};

template <typename T> class Optional
{
  T mValue;
  bool mHasValue;

public:

  Optional() : mHasValue(false) {  }

  Optional(T const &v) : mValue(v), mHasValue(true) { }

  void set(T const &v)
  {
    mValue = v;
    mHasValue = true;
  }

  void clear()
  {
    mHasValue = false;
  }

  T const &get() const
  {
    if (!mHasValue) {
      throw NoValueException();
    }
    return mValue;
  }

  bool hasValue() const
  {
    return mHasValue;
  }
};

int main()
{
  Optional<int> i;

  try
  {
    i.set(10);
    std::cout << "Wert: " << i.get() << "\n";
    i.clear();
    i.get();
    std::cout << "Hier komme ich nie vorbei.\n";
  }
  catch(NoValueException const &e)
  {
    std::cerr << "Versuch, auf eine Variable ohne Wert zuzugreifen!\n";
  }
}

Erklärung

Der Quellcode des Optional-Templates sieht im Prinzip aus wie beim letzten Mal. Nur die Funktion get() hat sich geändert: statt auf der Konsole eine Fehlermeldung auszugeben (was sowieso nicht sinnvoll wäre), wird nun bei Erkennen eines Fehlers ein Objekt des Typs NoValueException geworfen. Das Schlüsselwort throw unterbricht in Zeile 33 den normalen Kontrollfluss der Funktion und übergibt die Kontrolle an den nächsten passenden Handler. Exception-Handler werden in C++ definiert, indem erstmal der Block, in dem auf Exceptions geachtet werden soll, in ein try { ... } eingefasst wird, an das sich ein oder mehrere catch (...) Blöcke anschließen. Jeder dieser catch-Blöcke ist ein Exception-Handler, der auf den entsprechenden in Klammern angegebenen Typ von Exceptions reagiert. Die Zuordnung geschieht vergleichbar zum Aufruf von Funktionen mit Parametern. D.h. eine abgeleitete Exception wird von einem Block der Basisklasse gefangen, wenn nötig. Dazu aber mehr in einem anderen Video.

Der passende catch-Block ist hier direkt in der aufrufenden Funktion ab Zeile 56. Das ist nicht notwendigerweise so. Exceptions können auch über mehrere Funktionsebenen hinweg abgewickelt werden. Dabei verhalten sich die Funktionen so, als würde ihre Ausführung an der aktuellen Stelle abgebrochen (lokale Variablen etc. werden bspw. aufgeräumt, ganz so, als hätte man mittels return beendet). Gerade dieses Abwickeln über mehrere Ebenen ermöglicht eine ungeheure Flexibilität beim Behandeln von Fehler. Es ist bspw. nicht untypisch, spezifische Fehler in einer Funktion zu behandeln und alles Unerwartete einfach “nach außen” fliegen zu lassen, wo es dann ganz generisch mit “Irgendwas ist schiefgegangen” gefangen wird. Die inneren Funktionen müssen über diese Details nichts wissen. Die werfen lediglich Exceptions, wenn nötig.

Da Exceptions einen sehr nicht-lokalen Kontrollfluss erzeugen können (eine Exception wird geworfen und völlig woanders gefangen) können sie ggf. die Analyse des Programmflusses etwas erschweren. Sie sollten also tunlichst nur für das eingesetzt werden, wofür sie gedacht sind: Fehler/Ausnahmen, die im Programm auftreten können. Sie sind kein Mittel zur normalen Flusssteuerung (wer aus einer Funktion vorzeitig raus will, hat immernoch return zur Verfügung). Außerdem sollten Exceptions, die in einer Funktion auftreten können, dokumentiert werden. Eine unbehandelte Exception (sprich: eine, die auch aus der main()-Funktion raus fliegt, weil sich kein Handler dafür zuständig fühlt) führt nämlich sonst zum einigermaßen unkontrollierten Abbruch des Programms.

Unser Beispiel ist ein klassischer Anwendungsfall von Exceptions. Dadurch, dass der Rückgabewert des Templates ja noch bis zu Instanziierung unbekannt ist, kann der Code an der Stelle gar nicht wissen, wie er einen Fehler mittels Rückgabewert signalisieren sollte. Exceptions machen uns das Leben hier leicht.