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