C++ Kurs

Ausnahme-Behandlung

Die Themen:

Bisherige Fehlerbehandlung

In diesem Kapitel sehen wir uns an, welche Möglichkeiten C++ zur Verfügung stellt, um schwerwiegende Fehler zur Programmlaufzeit abzufangen.

Beginnen wir mit einem einfachen Beispiel, das die Behandlung eines Laufzeitfehlers mit den bisherigen Mitteln demonstriert. Im nachfolgenden Beispiel wird eine Funktion CalcSqrt(...) aufgerufen, die die Quadratwurzel aus übergebenen Wert berechnet. Ist der übergebene Wert negativ, so kann daraus, laut Mathematik-Unterricht 6. Klasse, keine Wurzel berechnet werden. In diesem Fall gibt die Funktion den Wert -1.0 zurück. Ist der Wert positiv, so wird die Bibliotheksfunktion sqrt(...) aufgerufen um die Quadratwurzel zu berechnen und deren Ergebnis wird zurückgegeben. In main() wird anschließend der Returnwert der Funktion abgeprüft und dann eine eventuell notwendige Fehlerbehandlung durchgeführt.

PgmHeaderdouble CalcSqrt(double val)
{
    if (val < 0.0)
        return -1.0;
    return sqrt(val);
}
int main()
{
    ....
    auto res = CalcSqrt(var);
    if (res < 0.0)
        ....  // Fehler behandeln
    ....
}

Einleiten und Auslösen einer Ausnahme

Aber das Ganze geht unter C++ natürlich wesentlich eleganter. Um solche Fehlerfälle abzufangen, kann die Ausnahme-Behandlung (Exception-Handling) eingesetzt werden. Hierzu werden nun nacheinander drei neue Schlüsselwörter eingeführt: try, catch und throw.

Die Ausnahme-Behandlung wird durch das Schlüsselwort try eingeleitet. Nach try folgt ein Block {...}, der die Anweisungen einschließt, für die eine Ausnahme-Behandlung durchgeführt werden soll. Dabei spielt es keine Rolle, ob der Fehler innerhalb des Blocks oder gar in einer im Block aufgerufenen Funktion auftritt.

try
{
   ....
}

Im Beispiel soll die Funktion CalcSqrt(...) später eine Ausnahme auslösen, wenn der ihr übergebene Wert negativ ist. Damit diese Ausnahme richtig verarbeitet werden kann, muss der Aufruf von CalcSqrt(...) zunächst innerhalb eines try-Blocks erfolgen.

PgmHeaderdouble CalcSqrt(double val)
{
    if (val < 0.0)
        ....
    return sqrt(val);
}
int main()
{
    ....
    try
    {
        auto res = CalcSqrt(var);
        ....
    }
}

Sehen wir uns nun an, wie die Funktion CalcSqrt(...) eine Ausnahme auslösen kann. Zum Auslösen einer Ausnahme dient die throw-Anweisung die folgende Syntax besitzt:

throw PARAM;

Welche Bedeutung PARAM hat, wird gleich noch näher erläutert. Im Augenblick reicht es aus, wenn für PARAM ein beliebiger int-Wert eingesetzt wird.

Damit können wir die Funktion CalcSqrt(...) jetzt wie nachfolgend angegeben erweitern. Wenn der an die Funktion übergebene Wert negativ ist, löst die Funktion mittels throw 1; eine Ausnahme aus.

PgmHeaderdouble CalcSqrt(double val)
{
    if (val < 0.0)
        throw 1;
    return sqrt(val);
}
int main()
{
    ....
    try
    {
        auto res = CalcSqrt(var);
        ....
    }
}

Damit kennen wir nun die Anweisung, die die Ausnahme-Behandlung einleitet (try) und die, die die Ausnahme auslöst (throw). Was jetzt nur noch fehlt, ist die Anweisung zum Abfangen der Ausnahme.

Abfangen (Behandeln) einer Ausnahme

Um eine innerhalb eines try-Blocks ausgelöste Ausnahme abzufangen, folgt unmittelbar nach dem try-Block ein neuer Block, der durch eine catch-Anweisung eingeleitet wird. Er besitzt folgende Syntax:

catch (PARAM)
{
    .... // Ausnahmebehandlung
}

Auch hier ignorieren wir vorerst einmal die genaue Bedeutung von PARAM und setzen dafür die Zeichenfolge ... (das sind drei Punkte) ein. Und innerhalb des catch-Blocks stehen dann die Anweisungen, die beim Auftreten einer Ausnahme, und nur dann, ausgeführt werden sollen. Der catch-Block wird auch als Exception-Handler bezeichnet. Damit können wir unser Beispiel jetzt wie angegeben vervollständigen. Und wie erwähnt, muss der catch-Block unmittelbar nach dem try-Block stehen. Andere Anweisungen zwischen diesen Blöcken sind nicht erlaubt.

PgmHeaderdouble CalcSqrt(double val)
{
    if (val < 0.0)
        throw 1;
    return sqrt(val);
}
int main()
{
    ....
    try
    {
        auto res = CalcSqrt(var);
        ....
    }
    catch (...)
    {
        cout << "Fehler in CalcSqrt()!\n";
    }
    ....
}

Sehen wir uns an, was die eingefügten Anweisungen letztendlich bewirken.

Gehen wir als erstes vom Gut-Fall aus, d.h. es wird kein negativer Wert an die Funktion CalcSqrt(...) übergeben. Damit wird in der Funktion auch keine Ausnahme ausgelöst und die Funktion berechnet die Quadratwurzel und gibt diese an main() zurück. In main() werden dann alle weiteren Anweisungen innerhalb des try-Blocks ganz normal ausgeführt. Der auf den try-Block folgende catch-Block wird anschließend übersprungen, da dessen Anweisungen ja nur im Falle einer Ausnahme ausgeführt werden.

Wir jedoch an an CalcSqrt(...) ein negativer Wert übergeben, so führt dies zum Auslösen einer Ausnahme innerhalb der Funktion mittels throw 1. Wird die throw-Anweisung ausgeführt, so wird sofort zu dem auf den try-Block folgenden catch-Block gesprungen, d.h. die Funktion CalcSqrt(...) wird vorzeitig verlassen. Ebenso werden die restlichen Anweisungen innerhalb des try-Blocks von main() übersprungen. In unserem Beispiel wird also nach der throw-Anweisung sofort die cout-Anweisung im catch-Block ausgeführt. Ist der catch-Block abgearbeitet wird ganz normal mit der Programmbearbeitung fortgefahren.

Ausnahmen in Funktionen/Memberfunktionen

Das korrekte Aufräumen des Stacks, der ja die Rücksprungadresse aus Funktionen, eventuelle Funktionsparameter und die lokale Daten einer Funktion enthalten kann, ist Sache des Laufzeitsystems. Darum brauchen wir uns nicht kümmern. Dieses Aufräumen des Stacks funktioniert sogar dann noch, wenn zwischen dem try-Block und der throw-Anweisung mehrere Funktionsebenen liegen.

PgmHeadervoid Func2()
{
    if (...)
        throw 1;
    ....
}
void Func1()
{
    ....
    Func2();
    ....
}
int main()
{
    ....
    try
    {
        Func1();
        ....
    }
    catch(...)
    {
        cout << "Fehler aufgetreten\n";
        exit(1);
    }
    ....
}

Im Beispiel ruft main() innerhalb eines try-Blocks zunächst die Funktion Func1(...) auf und diese ruft wiederum irgendwann Func2(...) auf. In der Funktion Func2(...) wird bei einer bestimmten Bedingung nun eine Ausnahme ausgelöst. Und auch hier kehrt das Programm nach Auslösen der Ausnahme sofort in den catch-Block in main() zurück, um eine entsprechende Meldung auszugeben und dann das Programm zu beenden. D.h. es werden nach dem Auslösen der Ausnahme keinerlei Anweisungen mehr in von Func2(...) und Func1(...) sowie innerhalb des try-Blocks ausgeführt.

Ausnahmen im Konstruktor

Ebenfalls prinzipiell möglich ist es, Ausnahmen in einem Konstruktor auszulösen. Wie bekannt, besitzt ein Konstruktor keinen Returnwert über den z.B. abgeprüft werden kann, ob die Erstellung eines Objekts erfolgreich war oder nicht. Beim Einsatz von Ausnahmen im Konstruktor sind jedoch einige Dinge zu beachten.

Fangen wir mit der Erstellung und dem Löschen des Objekts an. Beim Auslösen der Ausnahme innerhalb des Konstruktors gibt es zunächst keine Besonderheiten zu beachten.

PgmHeaderWindow::Window(int width,...)
{
    ....
    if (width > 1600)
       throw 1;
    ....
}

Aber beim Löschen des Objekts ist nun besondere Vorsicht geboten. Merken Sie sich immer folgenden wichtigen Satz:

AchtungDer Destruktor eines Objekts wird nur dann aufgerufen, wenn der Konstruktor vollständig, und damit fehlerfrei, ausgeführt wurde!

Wird also im Konstruktor eine Ausnahme ausgelöst, so wird und darf der Destruktor nicht mehr aufgerufen werden. In diesem Fall müssen eventuelle Aufräumarbeiten im Konstruktor, vor dem Auslösen der Ausnahme, selbst durchführt werden.

Der nächste Punkt ist zwar fast schon trivial, jedoch wird er leicht übersehen. Wird innerhalb eines try-Blocks ein Objekt definiert, so ist dieses auch nur im try-Block gültig. Das nachfolgende Beispiel erzeugt also beim Übersetzen eine Fehlermeldung, da außerhalb des Blocks versucht wird auf das im try-Block definierte Objekt zuzugreifen. Soll ein Objekt innerhalb eines try-Blocks erstellt und danach auch außerhalb des Blocks zur Verfügung stehen, so muss ein entsprechender Objektzeiger, der natürlich außerhalb des try-Blocks definiert wird, eingesetzt werden. Vergessen Sie dabei aber nicht, dass im Fehlerfall das Objekt eventuell nicht gültig ist!

PgmHeadertry
{
    Window myWin(...)
    ....
}
catch(...)
{
    ....
}
myWin.MoveWin(...);  // Fehler!!
AchtungDas Auslösen einer Ausnahme im Destruktor sollte soweit wie möglich vermieden werden, da ansonsten das entsprechende Objekt nicht korrekt gelöscht wird.

Weiterleiten von Ausnahmen

Machen wir den nächsten Schritt. Unter Umständen kann es auch erforderlich sein, dass die im nachfolgenden Beispiel in Func2(...) ausgelöste Ausnahme sowohl in Func1(...) als auch in main() verarbeitet werden muss, d.h. die Ausnahme muss nach ihrer ersten Verarbeitung in Func1(...) irgendwie weitergegeben werden. Für diesen Fall wird zunächst der Aufruf der Funktion Func2(...) innerhalb von Func1(...) in einen try-Block gelegt, auf den dann natürlich der dazugehörige catch-Block folgen muss. Zusätzlich muss nun aber auch noch der Aufruf von Func1(...) aus main(...) heraus in einen try-Block gelegt werden. Löst nun die Funktion Func2(...) eine Ausnahme aus, so wird diese zunächst im catch-Block von Func1(...) abgefangen und kann dort entsprechende bearbeitet werden. Jetzt gilt es lediglich noch, main() vom Auftreten der Ausnahme zu benachrichtigen. Und dies kann dadurch erreicht werden, dass die im catch-Block in Func1(...) aufgefangene Ausnahme durch die Anweisung throw, jetzt ohne zusätzlichen Parameter, erneut ausgelöst wird und damit an main() weitergeleitet wird.

PgmHeadervoid Func2()
{
    if (...)
        throw 1;  // Ausnahme auslösen
    ....
}
void Func1()
{
    try
    {
        Func2();
    }
    catch(...)  // Ausnahme abfangen
    {
        cout << "Func2-Fehler!\n";
        throw;  // Ausnahme weiterleiten
    }
    ....
}
int main()
{
    ....
    try
    {
        Func1();
        ....
    }
    catch(...)   // Ausnahme abfangen
    {
        cout << "Fehler aufgetreten\n";
        exit(1);
    }
    ....
}

Wird im Beispiel also in der Funktion Func2(...) eine Ausnahme ausgelöst, so werden folgende Ausgaben ausgegeben:

ProgrammausgabeFunc2-Fehler!
Fehler aufgetreten

throw Parameter

Erweitern wir erneut die Ausnahme-Behandlung. Die bisherigen Beispiele gingen immer davon aus, dass für alle Ausnahmen, die innerhalb eines try-Blocks auftreten können, auch die gleiche Behandlung durchgeführt wird. Jedoch können innerhalb eines try-Blocks auch verschiedene Ausnahmen auftreten, die auch unterschiedliche Behandlungen erfordern. Und hierzu gibt es verschiedene Lösungswege.

Die einfachste Art, Ausnahmen unterscheiden zu können besteht darin, der throw-Anweisung verschiedene Werte mitzugeben, die aber alle den gleichen Datentyp besitzen. Bleibt dann noch das kleine Problem, wie der Exception-Handler nun an diesen mitgegebenen Wert gelangt. Erinnern Sie sich noch an den allgemeinen Aufbau der catch-Anweisung? Sie hat ja folgende Syntax: catch (PARAM). Für PARAM hatten wir bisher immer drei Punkte eingesetzt. Diese drei Punkte bedeuten nichts anderes als: fange alle Ausnahmen ab für die nicht explizit ein anderer Exception-Handler definiert wurde. Um nun den Wert der throw-Anweisung auszuwerten, wird die catch-Anweisung wie folgt umgeschrieben:

catch (DTYP PARAM)

DTYP ist der Datentyp des Werts in der throw-Anweisung und PARAM ein beliebiger Parametername. Durch Auswerten des Parameters innerhalb des Exception-Handlers kann dann unterschieden werden, welche Ausnahme ausgelöst wurde.

PgmHeaderenum Error {ZDIV, SQRT};
void Func()
{
    if (....)
        throw ZDIV;  // 1. Ausnahme auslösen
    ....
    if (....)
        throw SQRT;  // 2. Ausnahme auslösen
}
int main()
{
    ....
    try
    {
        Func();
    }
    catch(const Error& e)
    {
        // Ausnahme auswerten
        switch(e)
        {
        case ZDIV:
            cout << "0 Division\n";
            break;
        case SQRT:
            cout << "Wurzelberechnung\n";
            break;
        }
    }
    ....
}

So wird im Beispiel beim Auftreten der Ausnahme ZDIV bzw. SQRT jeweils eine andere Fehlermeldung ausgegeben.

HinweisVerwenden Sie für den Datentyp innerhalb der catch-Anweisung in der Regel eine const-Referenz. Sie ersparen sich damit unter Umständen unnötige Kopieroperationen.

Sehen wir uns jetzt die allgemeine Form an, wie Ausnahmen unterschieden werden. Wie vorher bereits erwähnt, hängt der Datentyp des Wertes der throw-Anweisung eng mit dem Datentyp des Parameters der dazugehörigen catch-Anweisung zusammen. Wenn nun verschiedene throw-Anweisungen unterschiedliche Datentypen als Argument erhalten, so können auch unterschiedliche Exception-Handler ausgeführt werden. Im nachfolgenden Beispiel löst die Funktion Func(...) einmal eine Ausnahme mit einem int-Wert als Argument aus und eine zweite Ausnahme mit einem char-Zeiger. Unterschiedliche Datentypen in der throw-Anweisung bedingen nun auch mehrere Exception-Handler. D.h. für das Beispiel benötigen wir jetzt einen Exception-Handler zum Auffangen der 'int-Ausnahme' und einen zum Auffangen der 'char-Zeiger-Ausnahme'. Dabei ist unbedingt zu beachten, dass beide Exception-Handler unmittelbar hintereinander stehen. Zwischen dem try-Block und den Exception-Handlern dürfen keine anderen Anweisungen stehen Die Reihenfolge der Exception-Handler spielt hier zunächst keine Rolle.

PgmHeadervoid Func()
{
    if (....)
        throw 1;    // 1. Ausnahme auslösen
    ....
    if (....)
        throw "A";  // 2. Ausnahme auslösen
}
int main()
{
    ....
    try
    {
        Func();
    }
    catch(int val)
    {
        cout << "Fehler " << val << endl;
    }
    catch(const char *const pT)
    {
        cout << "Fehler " << pT << endl;
    }
    ....
}

Die Anzahl der auf einen try-Block folgenden Exception-Handler ist nicht begrenzt, d.h. es können zum Auslösen von Ausnahmen alle bekannten Datentypen verwendet werden.

HinweisDies entspricht aber nicht unbedingt der Praxis. Dort werden in der Regel zur Verarbeitung von Ausnahmen entsprechende Fehlerklassen verwendet, was wir im nächsten Abschnitt auch gleich tun werden.

Zusätzlich zu diesen typisierten Exception-Handlern können Sie immer auch den Default-Exception-Handler definieren, d.h. den mit den drei Punkten in der Parameterklammer. Alle nicht durch typisierte Exception-Handler abgefangenen Ausnahmen werden dann dort aufgesammelt. Hierbei ist aber unbedingt darauf achten, dass dieser als letzter in der 'Reihe' der Exception-Handler definiert wird, da für die Bearbeitung einer Ausnahme der erst nach dem try-Block stehende Exception-Handler ausgeführt wird, der den in der throw-Anweisung stehenden Wert verarbeiten kann.

PgmHeaderint main()
{
    ....
    try
    {
        ....
    }
    catch(int val)
    {
        ....
    }
    catch(const char *const pT)
    {
        ....
    }
    catch(...)  // Sammelt alle Exceptions auf, welche
    {           // nicht vom Typ int oder char* sind
        ....
    }
    ....
}

Ausnahme-Klassen

Erweitern wir die Ausnahme-Behandlung nochmals. Bisher wurden beim Auslösen von Ausnahmen nur die Standard-Datentypen wie int oder char-Zeiger verwendet. Da die Ausnahme-Behandlung aber beliebigen Datentypen zulässt, können auch Klassen hierfür eingesetzt werden.

Definition der Ausnahme-Klasse

Im nachfolgenden Beispiel wird eine Klasse Ex für die Behandlung von mathematischen Fehlern definiert. Diese Klasse enthält zum einen eine Nummer für den aufgetretenen Fehler (errNum) und zum anderen entsprechende Fehlermeldungen (pErrText). Das Feld mit den Fehlertexten ist als statische Eigenschaft definiert, da die Fehlertexte für alle Objekte dieser Klasse nur einmal vorhanden sein müssen. Die Fehlernummern selbst sind wieder als enum-Werte innerhalb der Klasse definiert. Dadurch, dass hier sowohl die Fehlernummern als auch die Fehlertexte in einer Klasse zusammengefasst sind, ist das Anwenderprogramm unabhängig von der internen Durchnummerierung der Fehler und dem zum Fehler gehörigen Fehlertext. Für die Ausgabe des Fehlertextes wurde der Klasse noch der überladene Operator << hinzugefügt.

PgmHeaderclass Ex
{
    short errNum;                   // Fehlernummer
    static const char *pErrText[];  // Fehlertexte
  public:
    enum eMathErr{ZDIV, SQRT};      // enums mit den Fehlern
    // ctor
    Ex(eMathErr err)
    {
        errNum = err;
    }
    // copy-ctor
    Ex(const Ex& src) = default;
    // Ausgabe des Fehlertextes
    friend ostream& operator << (ostream& os, const Ex& e);
};
// statisches Feld mit Fehlermeldungen
const char *Ex::pErrText[]
{
   "Division durch 0",
   "Wurzel aus negativer Zahl"
};
ostream& operator <<(ostream& os, const Ex& e)
{
    os << "Fehler " << e.pErrText[e.errNum] << " aufgetreten!\n";
    return os;
}
HinweisSie sollten der Ausnahme-Klasse auch immer den Kopierkonstruktor mitgeben. Beim Auslösen einer Ausnahme wird u.U. eine Kopie des Ausnahme-Objekts erstellt und an den Exception-Handler zurückgegeben. Dies gilt insbesondere dann, wenn im entsprechenden Exception-Handler (catch-Block) keine const-Referenz auf das Ausnahme-Objekt angegeben wurde.

Auslösen und Auffangen von Ausnahme einer Ausnahme-Klassen

Nach dem die Ausnahme-Klasse definiert wurde, kann eine Ausnahme vom Typ der Ausnahme-Klasse ausgelöst werden. Hierzu ist in der throw-Anweisung ein Objekt der Ausnahme-Klasse zu erzeugen, wobei der Konstruktor der Ausnahme-Klasse als Parameter die Kennung des aufgetretenen Fehlers (als enum-Konstante) erhält.

PgmHeaderclass Ex
{
    ....
};

void Func()
{
    if (....)
        throw Ex(Ex::ZDIV);
    ....
    if (....)
        throw Ex(Ex::SQRT);
}

Zum Abfangen der Ausnahme ist dann ein Exception-Handler zu definieren, der als Parameter eine const-Referenz auf ein Objekt der Ausnahme-Klasse enthält. Innerhalb des Exception-Handlers wird dann die entsprechende Fehlermeldung durch Ausgabe des Objekts ausgegeben.

PgmHeaderint main()
{
    try
    {
        Func();
    }
    catch(const Ex& e)
    {
       cout << e;
    }
    ....
}

Nachfolgend nun das komplette Beispiel zur Ausnahme-Klasse:

PgmHeader// Beispiel zur Ausnahmebehandlung

// Zuerst Dateien einbinden

#include <iostream>

using std::cout;
using std::endl;

// Definition der Ausnahmeklasse
class Exception
{
    short errNum;                         // Fehlernummer
    static const char* const pErrText[];  // Fehlertexte
public:
    enum eMathErr {ZDIV, SQRT};           // Fehlernummern
    Exception(eMathErr err);
    Exception(const Exception& CO);
    ~Exception();
    friend std::ostream& operator << (std::ostream& os, const Exception& ex);
};
// Fehlertexte definieren
const char* const Exception::pErrText[]
{
    "Division durch 0",
    "Wurzel aus negativer Zahl"
};
// Definition der Memberfunktionen
// Konstruktore

inline Exception::Exception(eMathErr err)
{
    errNum = err;
    cout << this << " ctor\n";
}
// Hinweis: ohne Ausgabe koennte der copy-ctor
// auch als default copy-ctor definiert werden

inline Exception::Exception(const Exception& CO)
{
    errNum = CO.errNum;
    cout << this << " copy-ctor\n";
}
// Destruktor
inline Exception::~Exception()
{
    cout << this << " dtor\n";
}
// Überladener Ausgabeoperator
std::ostream& operator << (std::ostream& os, const Exception& ex)
{
    os << "Fehler " << ex.pErrText[ex.errNum] << " aufgetreten!\n";
    return os;
}

// Testfunktion für Division
void Div(int val)
{
    // Falls Divisior 0, dann Ausnahme auslösen
    if (val == 0)
        throw Exception(Exception::ZDIV);
    cout << "Division erlaubt!\n";
}

// Testfunktion für Wurzelberechnung
void Sqrt(double val)
{
    // Falls Wurzelwert negativ, dann Ausnahme auslösen
    if (val < 0.0)
        throw Exception(Exception::SQRT);
    cout << "Wurzel kann berechnet werden!\n";
}

// main() Funktion
int main()
{
    // Ausnahmebehandlung einleiten
    try
    {
        Div(1);         // Das geht fehlerfrei
        Div(0);         // Führt zum Auslösen einer Ausnahme
    }
    // Ausnahme abfangen
    catch(const Exception& ex)
    {
        cout << ex;     // Fehlermeldung ausgeben
    }
    // Default-Behandlung für Exceptions
    catch(...)
    {
        cout << "unbekannte Exception!\n";
    }
    // Neue Ausnahmebehandlung einleiten
    try
    {
        Sqrt(1.2);      // Das geht fehlerfrei
        Sqrt(-1.2);     // Führt zum Auslösen einer Ausnahme
    }
    // Ausnahme abfangen
    catch(const Exception& ex)
    {
        cout << ex;
    }
    // Default-Behandlung für Exceptions
    catch(...)
    {
        cout << "unbekannte Exception!\n";
    }

    // Fertig
    cout << "Fertig!" << endl;
}

In den Konstruktor und im Destruktor werden noch die Adressen der erstellten bzw. zu löschenden Objekte ausgegeben (this-Zeiger). Modifizieren Sie den Exception-Handler auch einmal so, dass anstelle einer Objektreferenz direkt das Objekt verarbeitet wird.

Ableiten von Ausnahme-Klassen

Experimentieren wir noch ein klein wenig mit den Ausnahme-Klassen. Wie im Kapitel über Ableiten von Klassen erwähnt, lassen sich von einer Basisklasse weitere Klassen ableiten. Und dies gilt natürlich auch für Ausnahme-Klassen. Nachfolgend wird von der Basisklasse Ex eine spezielle Klassen ExDiv abgeleitet, um für eine Division durch Null eine gesonderte Fehlerbehandlung durchführen zu können.

PgmHeader// Basis Ausnahme-Klasse
class Ex
{...};
// Ausnahme-Klasse für 0-Division
class ExDiv: public Ex
{...};

Das Auslösen der Ausnahme erfolgt wie bisher. Beachtet werden muss dabei, dass beim Erstellen eines Ausnahme-Objekts, dessen Konstruktor keine Parameter besitzt, eine leere Klammer anzugeben ist.

PgmHeadervoid Div(int cal)
{
    if (val == 0)
        throw ExDiv();
    cout << "Division ok!\n";
}

Interessant wird das Ganze beim Abfangen der Ausnahme, denn Exception-Handler vom Typ einer Basisklasse können auch Ausnahme vom Typ einer abgeleiteten Klasse verarbeiten.

Wenn sowohl ein Exception-Handler für Ausnahmen vom Typ der Basisklasse als auch für davon abgeleiteten Klassen definiert sind, spielt die Reihenfolge der Definitionen der Exception-Handler eine wichtige Rolle. Eine Ausnahme wird immer vom ersten zutreffenden Exception-Handler nach dem try-Block bearbeitet. Im Beispiel unten wird deshalb zuerst der Exception-Handler für die abgeleitete Klasse ExDiv definiert und danach der für die Basisklasse Ex. Wird nun in der Funktion Div(...) eine Ausnahme vom Typ ExDiv ausgelöst, so wird diese auch im Exception-Handler catch(const ExDiv&) bearbeitet.

PgmHeadertry
{
    Div(2);
    Div(0);
}
catch(const ExDiv& e) // abgeleitete Klasse
{
    ....
}
catch(const Ex& e)    // Basisklasse
{
    ....
}

Anders würde der Fall liegen, wenn die beiden Exception-Handler in umgekehrter Reihenfolge definiert wären. Da der Exception-Handler catch(const Ex&) dann vor dem Exception-Handler catch(const ExDiv&) definiert ist, würden Ausnahmen immer im Exception-Handler catch(const Ex&) bearbeitet werden.

Damit haben wir den prinzipiellen Ablauf der Ausnahme-Behandlung kennengelernt. Sehen wir uns nun noch kurz die restlichen Funktionen an, die mit der Behandlung von Ausnahmen zu tun haben.

new und Ausnahmen

Wie schon bei der Beschreibung des new Operators erwähnt, löst new bei nicht erfolgreicher Speicherreservierung eine Ausnahme von Typ bad_alloc aus. Um nun auf einen solchen Fehler reagieren zu können, ist die Anweisung für die Speicherreservierung in einen try-Block einzuschließen. Auf den try-Block folgt dann der Exception-Handler für die bad_alloc Ausnahme. Was im Falle einer nicht erfolgreichen Speicherreservierung dann durchzuführen ist, hängt letztendlich nur von der Anwendung ab.

PgmHeadertry
{
    short *pData = nullptr;         // Immer gut
    pData = new short[1000];
    ....  // Arbeiten mit dem Feld
}
catch (const bad_alloc& e)
{
    cout << "Out of memory!" << endl;
    .... // weitere Fehlerbehandlung
}

Sonstige Ausnahme-Funktionen

Beginnen wir mit der Beantwortung der Frage: was passiert, wenn eine Ausnahme ausgelöst wird und kein entsprechender Exception-Handler definiert ist? In diesem Fall wird standardmäßig die Funktion terminate(...) aufgerufen, die innerhalb der Standard-Bibliothek definiert ist. Defaultmäßig beendet diese Funktion das Programm. Jedoch kann das Verhalten von terminate(...) verändert werden, indem mittels der Funktion

terminate_handler set_terminate(terminate_handler ph) throw();

eine eigene Funktion eingehängt wird. Der Parameter ph ist ein Verweis auf die einzuhängende Funktion, welche vom Typ void () sein muss. Als Ergebnis liefert set_terminate() einen Zeiger auf die bisherige Funktion. Innerhalb der eingehängten Funktion muss das Programm dann selbst beendet werden, entweder durch Aufruf der Funktion exit(...), abort(...) oder der ursprünglichen terminate(...) Funktion.

Des Weiteren kann eine Funktion auch die durch sie auszulösenden Ausnahmen einschränken. Dazu ist die Funktionsdefinition wie folgt zu erweitern:

RTYP FNAME (P1, P2,...) throw(Ex1, Ex2,..)
{....}

Innerhalb der Klammer der throw-Anweisung stehen die Datentypen der Ausnahmen, die die Funktion auslösen darf. Im nachfolgenden Beispiel darf die Funktion Div(...) nur Ausnahmen vom Typ ExZDiv und char-Zeiger auslösen.

PgmHeadervoid Div(int val) throw(ExZDiv, char*)
{
    if (val == 0)
        throw "Divisionsfehler";
    cout << "Division ok!\n";
}

Und sogleich stellt sich die nächste Frage: was aber passiert nun, wenn eine Exception ausgelöst wird, deren Typ nicht innerhalb dieser throw-Anweisung angegeben ist? In diesem Fall wird die Funktion unexpected(...) aufgerufen, die wiederum terminate(...) aufruft. Aber auch diese Standardfunktion kann durch Aufruf von

unexpected_handler set_unexpected(unexpected_handler ph) throw();

überschrieben werden. Für den Aufruf von set_unexpected(...) gilt das gleiche wie für den Aufruf der Funktion set_terminate(...).

noexcept Anweisung

Wie gerade beschrieben, können die durch eine Funktion ausgelösten Ausnahmen mittels throw(...) eingeschränkt werden. Soll eine Funktion überhaupt keine Ausnahme auslösen dürfen, so ist bei der Funktionsdefinition (und Funktionsdeklaration) nach der Parameterklammer das Schlüsselwort noexcept anzugeben.

RTYP FNAME (P1, P2,...) noexcept
{....}

noexcept kann optional als Parameter auch einen boolschen Ausdruck besitzen. Liefert die Auswertung des Ausdrucks true zurück, so darf die Funktion, und auch die von ihr aufgerufenen Funktionen, keine Ausnahme auslösen. Sehen wir uns dies einmal anhand eines Beispiels an.

PgmHeadervoid f1() noexcept
{
    throw "Ausnahme in f1() ausgeloest";
}
void f2()
{
    throw "Ausnahme in f2() ausgeloest";
}
void f3() noexcept(noexcept(f2()))
{
    f2();
}

Die Funktion f1() ist mittels noexcept als Funktion definiert, die keine Ausnahmen auslösen darf. Damit führt das Auslösen eine Ausnahme in dieser Funktion zum Aufruf der terminate() Funktion. In der Funktion f2() hingegen ist das Auslösen von Ausnahmen erlaubt, welche dann entsprechend abgefangen werden können. Interessant ist die Funktion f3(). Sie enthält eine noexcept Anweisung mit dem Argument noexcept(f2()). Die Auswertung dieses Arguments liefert den noexcept Status der Funktion f2() zurück. Da f2() Ausnahmen auslösen darf, liefert noexcept(f2()) false zurück und die Funktion f3(), und die von ihr aufgerufenen Funktionen, dürfen damit auch Ausnahmen auslösen. Würde dagegen das Argument in noexcept(f1()) abgeändert werden, so würde die Auswertung des Arguments true ergeben und f3() dürfte keine Ausnahme auslösen. D.h. das Argument "vererbt" damit den noexcept Status der aufgerufenen Funktion an die aufrufende Funktion.

HinweisSie sollten Funktionen, die selber keine Ausnahmen auslösen und nur Funktionen aufrufen, die auch keine Ausnahmen auslösen, als noexcept Funktionen definieren. Dies erleichtert dem Compiler die Arbeit, da er keine Rücksicht auf Ausnahmen berücksichtigen muss (kein Stack-Unwinding), was wiederum der Laufzeit der Anwendung zugutekommt.

Compile-Zeit "Ausnahme"

Um bereits beim Übersetzen des Programms bestimmte Bedingungen abprüfen zu können, kann die static_assert Prüfung eingesetzt werden. Sie hat folgende Syntax:

static_assert (AUSDRUCK, TEXT);

Der Compiler wertet beim Übersetzen des Programms AUSDRUCK aus. Ergibt die Auswertung false, so wird TEXT ausgegeben und der Übersetzungsvorgang abgebrochen. Sehen Sie sich dazu das folgende Beispiel an:

PgmHeader// Beliebige Struktur
struct Any
{
    char v1;
    int  v2;
    char v3;
    long v4;
};
// Belegter Speicher durch die Strukturelemente
constexpr int SIZEOFELEMENTS = sizeof(Any::v1)+sizeof(Any::v2)+
                               sizeof(Any::v3)+sizeof(Any::v4);

// Pruefen, ob Srukturelement ohne Fuellbyte im Speicher liegen
static_assert(sizeof(Any) != SIZEOFELEMENTS,"Struktur enthaelt Fuellbytes!");

// main() Funktion
int main ()
{
    ...
}

Die Struktur Any enthält zunächst einmal beliebige Elemente mit verschiedenen Datentypen. Nehmen wir jetzt an, dass für den fehlerfreien Programmablauf sichergestellt sein muss, dass diese Elemente kontinuierlich im Speicher liegen, d.h. ohne Füllbytes zwischen den Elementen. Und dies wird mit der static_assert Anweisung abgeprüft. Zunächst wird die Anzahl der Bytes, die die einzelnen Strukturelemente belegen, berechnet. Diese Byteanzahl wird dann mit den von der Struktur tatsächlich belegten Bytes verglichen und bei Ungleichheit eine Meldung ausgegeben und der Übersetzungsvorgang abgebrochen.

Beachten Sie, dass static_assert auch außerhalb von Code-Blöcken stehen darf, da static_assert keine Anweisung im üblichen Sinne ist.

Beispiel und ÜbungUnd hier geht's zum Beispiel und zur Übung.