Ausnahmebehandlung

Bisherige Fehlerbehandlung

In diesem Kapitel werden wir uns ansehen, wie schwerwiegende Fehler zur Programmlaufzeit abgefangen werden können.

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 einem übergebenen Wert berechnet.

#include <print>
#include <cmath>
double CalcSqrt(double val)
{
    // Wenn Parameter negativ, dann Minuswert
    // zurueckliefern (Wurzel ist immer positiv)
    if (val < 0.0)
        return -1.0;
    return std::sqrt(val);
}
int main()
{
    // Wurzel aus positivem Wert
    double var = 9.0;
    auto res = CalcSqrt(var);
    if (res < 0.0)
        std::println("Wurzel aus {} kann nicht berechnet werden!",var);
    else
        std::println("Wurzel aus {} ist {}.",var,res);
    // Wurzel aus negativem Wert
    var = -5.0;
    res = CalcSqrt(var);
    if (res < 0.0)
        std::println("Wurzel aus {} kann nicht berechnet werden!",var);
    else
        std::println("Wurzel aus {} ist {}.",var,res);
}

Wurzel aus 9 ist 3.
Wurzel aus -5 kann nicht berechnet werden!

Ist der übergebene Wert negativ, liefert die Funktion den Wert -1.0 zurück. Ist der übergebene Wert dagegen positiv, wird die Bibliotheksfunktion sqrt() aufgerufen, um die Quadratwurzel zu berechnen und zurückzugeben. In main() wird der Returnwert der Funktion abgeprüft und eine Fehlerbehandlung durchgeführt.

Um solche schwerwiegende Fehler abzufangen, kann die Ausnahmebehandlung (Exception-Handling) eingesetzt werden. Dazu werden drei neue Schlüsselwörter benötigt: try, catch und throw.

Einleiten der Ausnahmebehandlung

Die Ausnahmebehandlung wird durch das Schlüsselwort try eingeleitet. Nach try folgt ein Block {...}, welcher die Anweisungen einschließt, für die eine Ausnahmebehandlung durchgeführt werden soll. Dabei ist es nicht relevant, ob der Fehler innerhalb des Blocks oder in einer im Block aufgerufenen Funktion auftritt.

try
{
   ...
}

In unserem Beispiel soll die Funktion CalcSqrt() gleich noch eine Ausnahme auslösen, wenn der übergebene Wert negativ ist. Damit die Ausnahme verarbeitet werden kann, ist der Aufruf von CalcSqrt() in einen try-Block einzuschließen.

#include <print>
#include <cmath>

double CalcSqrt(double val)
{
    // Wenn Parameter negativ, dann Ausnahme ausloesen
    if (val < 0.0)
    {
        // hier wird gleich noch eine Ausnahme ausgeloest
    }
    return std::sqrt(val);
}

int main()
{
    // Wurzel aus positivem Wert
    double var = 9.0;
    // Ausnahmebehandlung einleiten
    try
    {
        std::println("Wurzel aus {} ist {}.",var,CalcSqrt(var));
        var = -5.0;
        std::println("Wurzel aus {} ist {}.",var,CalcSqrt(var));
    }
    // Beispiel noch nicht compilierbar da hier
    // noch etwas fehlt
    std::println("Ende der Anwendung");
}

Da der try-Block auch die Gültigkeit der innerhalb des Blocks definierten Daten begrenzt, denken Sie daran: Wird innerhalb eines try-Blocks ein Datum oder Objekt definiert, ist dieses nur im try-Block gültig.

Abfangen (Behandeln) einer Ausnahme

Um eine innerhalb eines try-Blocks ausgelöste Ausnahme abzufangen, folgt unmittelbar nach dem try-Block ein weiterer Block, der durch eine catch-Anweisung eingeleitet wird.

catch (PARAM)
{
   ... // Ausnahme verarbeiten
}

Für PARAM setzen wir im ersten Schritt die Folge ... (3 Punkte) ein. Und innerhalb des catch-Blocks dann stehen die Anweisungen, die beim Auftreten einer Ausnahme, und nur dann, ausgeführt werden. Der catch-Block wird auch als Exception-Handler bezeichnet.

Damit kann das Beispiel wie folgt erweitert werden. Wie erwähnt, muss der catch-Block unmittelbar nach dem try-Block stehen. Andere Anweisungen (außer Kommentare) zwischen diesen Blöcken sind nicht erlaubt.

#include <print>
#include <cmath>

double CalcSqrt(double val)
{
    // Wenn Parameter negativ, dann Ausnahme ausloesen
    if (val < 0.0)
    {
        // hier wird gleich noch eine Ausnahme ausgeloest
    }
    return std::sqrt(val);
}

int main()
{
    // Wurzel aus positivem Wert
    double var = 9.0;
    // Ausnahmebehandlung einleiten
    try
    {
        std::println("Wurzel aus {} ist {}.",var,CalcSqrt(var));
        var = -5.0;
        std::println("Wurzel aus {} ist {}.",var,CalcSqrt(var));
    }
    // Ausnahme abfangen
    catch(...)
    {
        std::println("Wurzel aus negativer Zahl!!!");
    }
    std::println("Ende der Anwendung");
}

Wurzel aus 9 ist 3.
Wurzel aus -5 ist -nan.
Ende der Anwendung

Auslösen einer Ausnahme

Das Auslösen einer Ausnahme erfolgt mit der throw-Anweisung:

throw PARAM;

Auf die genaue Bedeutung von PARAM kommen wir gleich zu sprechen. Für den Augenblick soll es genügen, für PARAM einen beliebigen int-Wert einzusetzen.

Ausnahmen in Funktionen/Methoden

Die Ausführung von throw veranlasst, dass der normale Programmablauf abgebrochen und an der nächsten Stelle fortgesetzt wird, an der die Ausnahme verarbeitet wird. D.h., wird die Ausnahme in einer Funktion oder Methode ausgelöst und z.B. erst in main() verarbeitet, so wird unmittelbar nach main() zurückgesprungen und dort der entsprechende catch-Block ausgeführt. Das korrekte Aufräumen des Stacks, der die Rücksprungadressen aus Funktionen, eventuelle Funktionsparameter und lokalen Daten einer Funktion enthalten kann, ist Sache des Laufzeitsystems. Dieses Aufräumen des Stacks funktioniert auch dann noch, wenn zwischen der throw-Anweisung und dem catch-Block mehrere Funktionsebenen liegen.

Damit kann das Beispiel nun vervollständigt werden.

#include <print>
#include <cmath>

double CalcSqrt(double val)
{
    // Wenn Parameter negativ, dann Ausnahme ausloesen
    if (val < 0.0)
    {
        throw 1;
    }
    return std::sqrt(val);
}

int main()
{
    // Wurzel aus positivem Wert
    double var = 9.0;
    // Ausnahmebehandlung einleiten
    try
    {
        std::println("Wurzel aus {} ist {}.",var,CalcSqrt(var));
        var = -5.0;
        std::println("Wurzel aus {} ist {}.",var,CalcSqrt(var));
    }
    // Ausnahme abfangen
    catch(...)
    {
        std::println("Wurzel aus negativer Zahl!!!");
    }
    std::println("Ende der Anwendung");
}

Wurzel aus 9 ist 3.
Wurzel aus negativer Zahl!!!
Ende der Anwendung

Ausnahmen im Konstruktor

Mithilfe der Ausnahmebehandlung ist es möglich, schwerwiegende Fehler beim Erstellen eines Objekts abzufangen, in dem im Konstruktor eine Ausnahme ausgelöst wird. Dabei sind jedoch einige Dinge zu beachten.

Das Auslösen der Ausnahme innerhalb des Konstruktors erfolgt wie gewohnt mittels throw.

Beim Löschen des Objekts ist jetzt Folgendes zu beachten:

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

D.h., wird im Konstruktor eine Ausnahme ausgelöst, sind eventuelle Aufräumarbeiten im Konstruktor vor dem Auslösen der Ausnahme durchzuführen.

Überdies können auf diese Weise nur Fehler beim Instanziieren von lokalen oder dynamischen Objekten abgefangen werden. Für die globalen Objekte gibt es keinen try-Block!

Das Auslösen einer Ausnahme im Destruktor ist so weit wie möglich zu vermeiden, da ansonsten das Objekt nicht korrekt gelöscht wird.

Weiterleiten von Ausnahmen

Unter Umständen kann es erforderlich sein, ein und dieselbe Ausnahme an mehreren Stellen im Programmablauf zu behandeln. Im nachfolgenden Beispiel wird in CalcSqrt() in Zeile 9 eine Ausnahme ausgelöst wenn der übergebene Wert negativ ist. Diese Ausnahme wird nun im catch-Block in CalcSqrt() (Zeile 11) abgefangen und verarbeitet.  Jetzt gilt es aber noch main() vom Auftreten der Ausnahme zu benachrichtigen. Und dies wird dadurch erreicht, dass die im catch-Block in Zeile 14 die abgefangene Ausnahme durch die Anweisung throw; erneut ausgelöst und damit an main() weitergeleitet wird.

#include <print>
#include <cmath>

double CalcSqrt(double val)
{
    try
    {
        if (val < 0.0)
            throw 1;
    }
    catch (...)     // Ausnahme abfangen
    {
        std::println("Negativer Wert in CalcSqrt()!");
        throw;      // Ausnahme weiterleiten
    }
    return std::sqrt(val);
}

int main()
{
    // Wurzel aus positivem Wert
    double var = 9.0;
    // Ausnahmebehandlung einleiten
    try
    {
        std::println("Wurzel aus {} ist {}.",var,CalcSqrt(var));
        var = -5.0;
        std::println("Wurzel aus {} ist {}.",var,CalcSqrt(var));
    }
    // Ausnahme abfangen
    catch(...)
    {
        std::println("Wurzel aus negativer Zahl!!!");
    }
    std::println("Ende der Anwendung");
}

Wurzel aus 9 ist 3.
Negativer Wert in CalcSqrt()!
Wurzel aus negativer Zahl!!!
Ende der Anwendung

throw-Parameter

Die bisherigen Beispiele gingen davon aus, dass für alle Ausnahmen die innerhalb eines try-Blocks auftreten, die gleiche Behandlung durchgeführt wird. Um verschiedene Ausnahmen unterschiedlich behandeln zu können gibt es mehrere Lösungswege.

Die einfachste Art Ausnahmen in einem catch-Block unterscheiden zu können besteht darin, der throw-Anweisung unterschiedliche Werte des gleichen Datentyps mitzugeben. Bleibt dann das kleine Problem, wie der Exception-Handler an diesen mitgegebenen Wert gelangt. Erinnern Sie sich an die Syntax der catch-Anweisung: catch (PARAM)? Für PARAM wurden bisher immer drei Punkte eingesetzt. Diese drei Punkte bedeuten nichts anderes als: Fange alle Ausnahmen ab, für die nicht ein anderer Exception-Handler definiert ist. Um den Wert der throw-Anweisung auszuwerten, wird die catch-Anweisung wie folgt umgeschrieben:

catch (DTYP param)

DTYP ist der Datentyp des Wertes, der in der throw-Anweisung angegeben ist, und param ein beliebiger Parametername. Durch Auswerten dieses Parameters innerhalb des Exception-Handlers kann geprüft werden, welche Ausnahme ausgelöst wurde.

#include <print>
#include <cmath>

// Enums fuer Fehlertyp
enum Error {NEGATIV, ZERO};

double CalcSqrt(double val)
{
    if (val < 0.0)      // Ausnahme NEGATIV ausloesen
        throw NEGATIV;
    if (val == 0.0)     // Ausnahme ZERO ausloesen
        throw ZERO;
    return std::sqrt(val);
}

int main()
{
    // Wurzel aus positivem Wert
    double var = 9.0;
    // Ausnahmebehandlung einleiten
    try
    {
        std::println("Wurzel aus {} ist {}.",var,CalcSqrt(var));
        var = 0;
        std::println("Wurzel aus {} ist {}.",var,CalcSqrt(var));
    }
    // Ausnahme abfangen
    catch(const Error& err)
    {
        // 'Ausnahmewert' auswerten
        switch (err)
        {
        case NEGATIV:
            std::println("Wurzel aus negativer Zahl!!!");
            break;
        case ZERO:
            std::println("Das war wohl Spass?!?");
        }
    }
    std::println("Ende der Anwendung");
}

Wurzel aus 9 ist 3.
Das war wohl Spass?!?
Ende der Anwendung

Verwenden Sie für den Datentyp innerhalb der catch-Anweisung eine const-Referenz. Sie sparen damit unter Umständen unnötige Kopieroperationen.

Die zweite Art Ausnahmen zu unterscheiden ist über den Datentyp der Argumente bei der throw- und catch-Anweisung.  Im nachfolgenden Beispiel löst CalcSqrt() eine Ausnahme mit einem int-Wert als Argument aus und eine zweite Ausnahme mit einem string. Damit wird ein Exception-Handler zum Auffangen der int-Ausnahme benötigt und einer zum Auffangen der string-Ausnahme. Dabei ist zu beachten, dass beide Exception-Handler unmittelbar hintereinanderstehen. Die Reihenfolge der Exception-Handler spielt zunächst keine Rolle.

#include <print>
#include <cmath>
#include <string>

using namespace std::string_literals;

double CalcSqrt(double val)
{
    if (val < 0.0)      // int-Ausnahme ausloesen
        throw 5;
    if (val == 0.0)     // string-Ausnahme ausloesen
        throw "Das war wohl Spass"s;
    return std::sqrt(val);
}

int main()
{
    // Wurzel aus positivem Wert
    double var = 9.0;
    // Ausnahmebehandlung einleiten
    try
    {
        std::println("Wurzel aus {} ist {}.",var,CalcSqrt(var));
        var = 0;
        std::println("Wurzel aus {} ist {}.",var,CalcSqrt(var));
    }
    // int-Ausnahme abfangen
    catch (int errCode)
    {
        std::println("Fehler {} aufgetreten",errCode);
    }
    // string-Ausnahme abfangen
    catch (const std::string& errText)
    {
        std::println("{}",errText);
    }
    std::println("Ende der Anwendung");
}

Wurzel aus 9 ist 3.
Das war wohl Spass
Ende der Anwendung

Zusätzlich zu diesen 'typisierten' Exception-Handlern steht immer der Default-Exception-Handler catch(...) zur Verfügung. Alle nicht durch die typisierten Exception-Handler abgefangenen Ausnahmen werden dann dort behandelt. Dabei ist darauf achten, dass dieser als letzter in der 'Reihe' der Exception-Handler definiert ist. Für die Bearbeitung einer Ausnahme wird der erste nach dem try-Block stehende Exception-Handler ausgeführt, der das in der throw-Anweisung angegebene Argument verarbeiten kann.

Ausnahmeklassen

Bisher wurden zum Auslösen von Ausnahmen nur vordefinierte Datentypen verwendet, z.B. int oder std::string. Da die Ausnahmebehandlung aber beliebige Datentypen zulässt, können ebenfalls eigene Klassen hierfür eingesetzt werden.

Definition der Ausnahmeklasse

Im nachfolgenden Beispiel wird eine Klasse Ex für die Behandlung von Fehlern definiert. Die Klasse enthält eine Eigenschaft für die Nummer des aufgetretenen Fehlers (error) und ein Feld mit den korrespondierenden Fehlermeldungen (errText). Die Fehlernummern sind als enum-Werte innerhalb der Klasse definiert. Die Fehlermeldungen sind als statische Eigenschaft definiert, da sie für alle Objekte dieser Klasse nur einmal definiert sein müssen. Dadurch, dass hier sowohl die Fehlernummern wie auch die Fehlermeldungen in einer Klasse zusammengefasst sind, ist das Anwenderprogramm unabhängig von der internen Nummerierung der Fehler und der zum Fehler gehörigen Fehlermeldung. Für die Ausgabe des Fehlermeldung wurde der Klasse der überladene Operator << hinzugefügt.

// Ausnahmne-Klasse
class Ex
{
public:
    enum Error {NEGATIV, ZERO}; // enums fuer Fehlerfaelle
private:
    Error error;                // aktueller Fehler
    // Fehlertext zum enum-Wert
    inline static std::string errText[] =
        {"Negativer Wert"s, "Das war wohl Spass"s};
public:
    // ctor
    Ex(Error _error): error(_error)
    {}
    // copy-ctor
    Ex(const Ex& src) = default;
    // Ausgabe des Fehlers
    friend std::ostream& operator << (std::ostream& out, const Ex& obj);
};

Wenn für die Ausnahmebehandlung eine Klasse eingesetzt wird, sollte diese immer den Kopierkonstruktor definieren, da beim Auslösen einer Ausnahme u.U. eine Kopie des Ausnahme-Objekts erstellt und an den Exception-Handler übergeben wird. Dies gilt insbesondere dann, wenn der Exception-Handler keine const-Referenz auf das Ausnahme-Objekt als Parameter erhält.

Auslösen und Auffangen von Ausnahmen einer Ausnahmeklasse

Zum Auslösen der Ausnahme ist in der throw-Anweisung ein Objekt der Ausnahmeklasse zu instanziieren, wobei im Beispiel der Konstruktor der Ausnahmeklasse als Parameter die Kennung des aufgetretenen Fehlers (Enumerator) erhält.

Das Abfangen der Ausnahme erfolgt durch einen Exception-Handler, der als Parameter eine const-Referenz auf ein Objekt der Ausnahmeklasse enthält. Innerhalb des Exception-Handlers wird im Beispiel die Fehlermeldung durch Ausgabe des Objekts ausgegeben.

Damit sieht das vollständige Beispiel wie folgt aus:

#include <print>
#include <cmath>
#include <string>
#include <iostream>

using namespace std::string_literals;

// Ausnahmne-Klasse
class Ex
{
public:
    enum Error {NEGATIV, ZERO}; // enums fuer Fehlerfaelle
private:
    Error error;                // aktueller Fehler
    // Fehlertext zum enum-Wert
    inline static std::string errText[] =
        {"Negativer Wert"s, "Das war wohl Spass"s};
public:
    // ctor
    Ex(Error _error): error(_error)
    {}
    // copy-ctor
    Ex(const Ex& src) = default;
    // Ausgabe des Fehlers
    friend std::ostream& operator << (std::ostream& out, const Ex& obj);
};
// Ausgabe des Fehlertextes
std::ostream& operator << (std::ostream& out, const Ex& obj)
{
    out << std::format("Fehler '{}' aufgetreten!\n", obj.errText[obj.error]);
    return out;
}

double CalcSqrt(double val)
{
    if (val < 0.0)      // Ausnahme NEGATIV ausloesen
        throw Ex{Ex::NEGATIV};
    if (val == 0.0)     // Ausnahme ZERO ausloesen
        throw Ex(Ex::ZERO);
    return std::sqrt(val);
}

int main()
{
    // Wurzel aus positivem Wert
    double var = 9.0;
    // Ausnahmebehandlung einleiten
    try
    {
        std::println("Wurzel aus {} ist {}.",var,CalcSqrt(var));
        var = 0;
        std::println("Wurzel aus {} ist {}.",var,CalcSqrt(var));
    }
    // Ausnahme abfangen
    catch(const Ex& err)
    {
        std::cout << err;
    }
    std::println("Ende der Anwendung");
}

Wurzel aus 9 ist 3.
Fehler 'Das war wohl Spass' aufgetreten!
Ende der Anwendung

Ableiten von Ausnahmeklassen

Wie im Kapitel über Ableiten von Klassen erwähnt, lassen sich von einer Basisklasse weitere Klassen ableiten. Und dies gilt auch für Ausnahmeklassen. Nachfolgend wird von der Basisklasse Ex eine Klasse ExDiv abgeleitet, um für eine Division durch null eine gesonderte Fehlerbehandlung durchführen zu können.

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

Das Auslösen der Ausnahme erfolgt wie bisher. Dabei ist zu beachten, dass beim Erstellen eines Ausnahme-Objekts, dessen Konstruktor keine Parameter besitzt, eine leere Klammer anzugeben ist.

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

Wie erwähnt, wird eine Ausnahme stets vom ersten zutreffenden Exception-Handler nach dem try-Block bearbeitet. Und da Exception-Handler vom Typ einer Basisklasse auch Ausnahmen vom Typ der abgeleiteten Klasse verarbeiten können, ist die Reihenfolge der Exception-Handler in diesem Fall relevant. Im Beispiel wird zuerst der Exception-Handler für die abgeleitete Klasse ExDiv definiert und danach der für die Basisklasse Ex. Würde Reihenfolge umgedreht, würde der Exception-Handler ExDiv niemals zur Ausführung gelangen.

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

Standard-Ausnahmen

Auch die C++-Standardbibliothek definiert eine Reihe von Ausnahmen, die beim Auftreten eines Fehlers ausgelöst werden. Die dazugehörigen Ausnahmeklassen sind, bis auf zwei Ausnahmen, in der Header-Datei stdexcept definiert und liegen wieder im Namensraum std.

Die Basisklasse für alle Ausnahmeklassen ist die Klasse exception, von der u.a. folgende Klassen abgeleitet sind:

Klasse Verwendung
logic_error Abfangen von Logikfehlern. Von dieser Klasse sind u.a. abgeleitet: invalid_argument, length_error, out_of_range
runtime_error Abfangen von Fehlern zur Programmlaufzeit. Abgeleitet davon sind u.a. range_error, overflow_error, underflow_error, ios_base::failure und format_error
bad_typeid Abfangen von Fehlern beim Aufruf des Operators typeid. Ausnahme definiert in Header-Datei typeinfo.
bad_cast Abfangen von Fehlern beim Aufruf von dynamic_cast<>. Ausnahme definiert in Header-Datei typeinfo.

Alle Ausnahmeklassen enthalten die public-Methode what(), die Informationen über die aufgetretene Ausnahme in einem C-String zurückliefert. Der Inhalt des C-Strings ist implementierungsabhängig.

ios_base::failure Ausnahme

Auch Streams können beim Fehlschlagen von Operationen Ausnahmen auslösen. Diese zusätzliche Eigenschaft der Streams wurde erst zu einem späteren Zeitpunkt in den C++-Standard aufgenommen, als Streams bereits recht häufig eingesetzt wurden. Um mit bestehenden Anwendungen kompatibel zu bleiben, lösen Streams defaultmäßig keine Ausnahmen aus. Das Auslösen von Ausnahmen durch Streams ist explizit durch Aufruf der Stream-Methode exceptions(flag) freizugeben. Als Parameter erhält exceptions() ein oder mehrere der nachfolgenden Flags:

Flag Bedeutung
std::ios::failbit Beim Einlesen konnten das erwartete Datum nicht eingelesen werden oder bei der Ausgabe konnte das Datum nicht geschrieben werden.
std::ios::eofbit Beim Einlesen wurde das Dateiende erreicht.
std::ios::badbit Der Inhalt des Ein- bzw. Ausgabestreams ist nicht mehr konsistent.

Die ios_base::failure Ausnahmeklasse ist in der Header-Datei ios definiert, die nicht gesondert eingebunden werden muss, da dies automatisch durch den entsprechenden Stream-Header (z.B. fstream) erfolgt.

#include <print>
#include <fstream>

int main()
{
    std::ifstream inFile;
    // Bisherige Flags retten und failbit setzen
    auto savedFlags = inFile.exceptions();
    inFile.exceptions(std::ios::failbit);

    try     // Ausnahmebehandlung aktivieren
    {
        // Datei oeffnen. Wenn u.a. die Datei nicht
        // existiert wird eine Ausnahme ausgeloest
        inFile.open("x:/temp/xxx.txt");
        std::println("Datei erfolgreich geoeffnet");
        inFile.close();
    }
    // ios_base::failure Ausnahmen abfangen
    catch(std::ios_base::failure& ex)
    {
        std::println("FEHLER: {}",ex.what());
        // Eventl. Datei schliessen
        if (inFile.is_open())
            inFile.close();
    }
    // Urspruengliche Flags wieder herstellen
    inFile.exceptions(savedFlags);
}

FEHLER: basic_ios::clear: iostream error

Innerhalb des Exception-Handlers könnte durch den Aufruf der Stream-Methoden eof(), fail() und bad() feststellt werden, welche Art von Fehler aufgetreten ist. Nicht vergessen werden sollte dabei, eine eventuell geöffnete Datei wieder zu schließen. Die Methode is_open() liefert den diesbezüglichen Status der Datei.

In der Praxis sollte das Erreichen des Dateiendes nicht durch Auslösen einer Ausnahme festgestellt werden, sondern durch Aufruf der Methode eof().

Auslösen von Standardausnahmen

Um in einer Anwendung eine der Standardausnahme auszulösen, ist dem Konstruktor der Ausnahme ein string-Objekt oder ein char* zu übergeben. Im Beispiel werden die beiden Ausnahmen underflow_error und overflow_error in Abhängigkeit von einer Bedingung ausgelöst. Innerhalb des Exception-Handlers kann dann über die Methode what() auf diesen String zugegriffen werden.

#include <print>
#include <exception>

void CheckRange(int param)
{
    if (param < 0)  // Wert < 10: underflow-Ausnahme
    {
        auto errText = std::format("underflow in {}, Zeile {}\n",
                             __FILE__, __LINE__);
        throw std::underflow_error(errText);
    }
    if (param > 10) // Wert > 10: overflow-Ausnahme
    {
        auto errText = std::format("overflow in {}, Zeile {}\n",
                             __FILE__, __LINE__);
        throw std::overflow_error{errText};
    }
    // Hier ist alles in Ordnung
    std::println("Parameter innerhalb des Bereich!");
}

int main()
{
    int var = 10;
    try
    {
        CheckRange(var);    // var im gueltigen Bereich
        var++;
        CheckRange(var);    // var ausserhalb des Bereichs
    }
    // Alle Standard-Ausnahmen abfangen
    catch (const std::exception& ex)
    {
        std::println("FEHLER: {}",ex.what());
    }
}

Parameter innerhalb des Bereich!
FEHLER: overflow in C:\temp\cpp_testprojekte\est1\main.cpp, Zeile 15

Nicht abgefangene Ausnahmen

Ist für eine Ausnahme kein Exception-Handler definiert, wird die Bibliotheksfunktion terminate() aufgerufen. Defaultmäßig beendet die Funktion das Programm. Jedoch kann das Verhalten von terminate() mittels der Funktion

term_handler set_terminate(term_handler ph) noexcept;

angepasst werden. Der Parameter ph ist ein Verweis auf eine einzuhängende Funktion, welche vom Typ void Fkt() oder eine Lambda-Funktion (wird im Kapitel Lambdas erklärt) sein muss. Als Ergebnis liefert set_terminate() einen Zeiger auf die bisherige Funktion zurück. Innerhalb der eingehängten Funktion ist das Programm dann zu beenden, entweder durch Aufruf der Funktion exit(), abort() oder der ursprüngliche terminate()-Funktion.

noexcept Spezifizierer

Löst eine Funktion (und die von ihr aufgerufenen Funktionen) keine Ausnahmen aus, kann bei der Funktionsdefinition und -deklaration nach der Parameterklammer das Schlüsselwort noexcept angegeben werden. Dies erleichtert dem Compiler die Arbeit, da er keine Ausnahmen berücksichtigen muss (kein Stack-Unwinding), was wiederum der Laufzeit der Anwendung zugutekommt.

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

noexcept kann optional als Parameter einen boolschen Ausdruck erhalten. Liefert die Auswertung des Ausdrucks true zurück, darf die Funktion keine Ausnahmen auslösen. Sehen wir uns dies anhand eines fiktiven Beispiels an.

void 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öst. Wird trotzdem in f1() eine Ausnahme ausgelöst, führt dies zum Aufruf der terminate()-Funktion. Ein guter Compiler gibt in einem solchen Fall eine Warnung aus.

Interessant ist die Funktion f3(). Sie enthält eine noexcept Anweisung mit dem Argument f2(). Die Auswertung dieses Arguments liefert den noexcept-Status der Funktion f2() zurück. Da f2() Ausnahmen auslösen kann, liefert noexcept(f2()) false zurück und die Funktion f3() kann somit Ausnahmen auslösen. D.h., das Argument "vererbt" den noexcept Status der aufgerufenen Funktion an die aufrufende Funktion.

Beachten Sie, dass einige Operatoren, wie z.B. der new Operator, und Bibliotheksfunktionen Ausnahmen auslösen können.

Wenn Sie Funktionszeiger einsetzen, ist darauf zu achten, dass der noexcept Spezifizierer mit zum Typ der Funktion gehört. D.h., ein Funktionszeiger auf eine noexcept Funktion kann nur Adressen von Funktionen von diesem Typ aufnehmen. Der umgekehrte Fall ist aber möglich. Ein 'normaler' Funktionszeiger kann auch Adressen von noexcept Funktionen aufnehmen.

Compile-Zeit "Ausnahme"

Zur Prüfung von Bedingungen beim Übersetzen des Programms kann die static_assert Prüfung eingesetzt werden. Sie hat folgende Syntax:

static_assert (AUSDRUCK [,TEXT]);

Beim Übersetzen wertet der Compiler AUSDRUCK aus. Ergibt die Auswertung false, wird der Übersetzungsvorgang abgebrochen und entweder eine Standardmeldung oder der optionale TEXT ausgegeben. Sehen Sie sich dazu das folgende Beispiel an:

// Beliebige Struktur
struct CAny
{
    int v1;
    char v2;
    int v3;
    long v4;
};

// Belegter Speicher durch die Strukturelemente
constexpr auto SIZEOFELEMENTS =
        sizeof CAny::v1 + sizeof CAny::v2 +
        sizeof CAny::v3 + sizeof CAny::v4;
// Pruefen, ob Strukturelemente ohne Fuellbytes
// im Speicher liegen
static_assert(sizeof(CAny) == SIZEOFELEMENTS,
    "Struktur enthaelt Fuellbytes!");

int main()
{
}

Die Struktur CAny enthält Elemente mit unterschiedlichen Datentypen. Angenommen, aus irgendwelchen Gründen müssen diese Elemente kontinuierlich im Speicher liegen, d.h. ohne Füllbytes zwischen den Elementen. Um dies sicherzustellen, kann die static_assert Anweisung eingesetzt werden. Zunächst wird die Anzahl der Bytes berechnet, die die einzelnen Strukturelemente belegen (SIZEOFELEMENTS). Diese Byteanzahl wird dann mit den von der Struktur belegten Bytes verglichen und bei Ungleichheit (Auswertung der Bedingung ergibt false) eine Meldung ausgegeben und der Übersetzungsvorgang abgebrochen.

Zum Testen können Sie in Zeile 5 den Datentyp char durch ein int ersetzen. Das Programm sollte dann ohne Fehlermeldung übersetzt werden.

Da static_assert eine Deklaration ist, darf sie auch außerhalb von Code-Blöcken stehen.

Übungen

ausnah_01:

Erstellen Sie eine Klasse zur Realisierung eines Stacks für die Ablage von short-Werten. Die Größe des Stacks ist bei der Stackdefinition mit anzugeben. Wird versucht einen Stack für mehr als 100 Elemente anzulegen (Stackgröße > 100) ist eine Ausnahme auszulösen.

Zum Ablegen von Werten wird die Methode Push() verwendet. Wird diese Methode aufgerufen, obwohl der Stack belegt ist, ist eine Ausnahme auszulösen.

Die Methode Pop() dient zum Auslesen von Werten. Ist kein Wert mehr auf dem Stack abgelegt und die Methode wird aufgerufen, soll ebenfalls eine Ausnahme ausgelöst werden.

Legen Sie einen Stack für 5 Werte an und füllen diesen komplett, d.h. bis eine Ausnahme ausgelöst wird.

Anschließend ist der Stack vollständig zu leeren.

Erstellen Sie einen zweiten Stack für 1000 Einträge. Dies sollte zum Auslösen einer Ausnahme führen.

Fuelle Stack:
Stack belegt!
Leere Stack:
14, 13, 12, 11, 10, Stack leer!
Lege grossen Stack an:
Maximale Stackgroesse von 100 ueberschritten!

ausnah_02:

Fangen Sie im nachfolgenden Programm alle auftretenden Ausnahmen ab und geben Sie die entsprechende Meldungen aus. Einige der Ausnahmen werden von der Sprache C++ ausgelöst und andere innerhalb der Klassen Any bzw Another.

Verwenden Sie in der catch-Anweisung keine Referenz auf die Basisklasse exception für  Ausnahmen.

Wenn das Programm korrekt beendet wird, steht am Ende die Meldung 'Programm erfolgreich durchlaufen!' auf dem Bildschirm

#include <print>
#include <stdexcept>

// Globale Konstante für max. Parameterwert des Any ctors
constexpr auto ULIMIT=5;

// Klasse Any
class Any
{
    int data;
public:
    // ctor, loest Ausnahme aus wenn param
    // kleiner 0 oder groesser 4
   explicit Any(int param)
   {
      if ((param<0) || (param>=ULIMIT))
          throw std::out_of_range(
                     "ctor-Parameter <0 oder >=5 !");
     data = param;
   }
   virtual ~Any() = default;
};

// Klasse löst in Add() eine underflow_error oder
// overflow_error Exception aus wenn Ergebnis der
// Addition ausserhalb des short Wertebereichs liegt
class Another
{
 public:
   Another() = default;
   virtual ~Another() = default;
   short Add(short op1, short op2) const
   {
      short res = op1 + op2;
      // Pruefen auf Ueberlauf bei der Addition
      // short-Bereich: [-32768, 32767]
      if ((op1<0) && (op2<0) && (res>0))
         throw std::underflow_error(
                    "Underflow in Another::Add()");
      if ((op1>0) && (op2>0) && (res<0))
          throw std::overflow_error(
                     "Overflow in Another::Add()");
      return res;
  }
};

// Alle Ausnahmen in main() abfangen!!
int main()
{
    // 2 Any Objekte anlegen
    Any obj1{1};
    Any obj2{5};

    Any obj3{3};
    // Neues Any Objekt in Another Objekt konvertieren
    Another& ref = dynamic_cast<Another&>(obj3);
    ref.Add(10,10);

    // Another Objekt definieren und Additionen durchführen
    Another obj;
    std::println("Addition: {}",obj.Add(20000,10000));
    std::println("Addition: {}",obj.Add(-20000, 30000));
    std::println("Addition: {}",obj.Add(20000, 30000));

    // Programm erfolgreich beendet
    std::println("Programm erfolgreich durchlaufen!");
}

(hier steht dann die Fehlermeldungen)
Programm erfolgreich durchlaufen!