Exception Handling

Die Ausnahmebehandlung (Exception Handling) in C++ ist ein Mechanismus, um Laufzeitfehler (Fehler, die während der Programmausführung auftreten) strukturiert zu erkennen und zu behandeln. Beispiele für Laufzeitfehler sind:

Warum man die Ausnahmebehandlung benötigt

Fehler oder abnormale Zustände können auch ohne Ausnahmebehandlung behandelt werden, z. B. wie in C mit Bedingungsanweisungen (if-else). Die Ausnahmebehandlung bietet jedoch folgende Vorteile gegenüber der traditionellen Fehlerbehandlung:

Trennung von Fehlerbehandlungscode und normalem Code

In der traditionellen Fehlerbehandlung gibt es immer if-else-Bedingungen, um Fehler zu behandeln. Diese Bedingungen und der Fehlerbehandlungscode vermischen sich mit dem normalen Programmfluss, was den Code weniger lesbar und wartbar macht. Mit try- und catch-Blöcken wird der Fehlerbehandlungscode vom normalen Ablauf getrennt.

Funktionen/Methoden können nur die Ausnahmen behandeln, die sie wählen

Eine Funktion kann viele Ausnahmen werfen, muss aber nicht alle selbst behandeln. Nicht abgefangene Ausnahmen können vom Aufrufer behandelt werden. Wenn der Aufrufer sie nicht abfängt, übernimmt der Aufrufer des Aufrufers die Behandlung. In C++ kann eine Funktion die von ihr geworfenen Ausnahmen mit dem throw-Schlüsselwort spezifizieren, und der Aufrufer muss die Ausnahme auf irgendeine Weise behandeln (entweder erneut deklarieren oder abfangen).

Gruppierung von Fehlertypen

In C++ können sowohl Basisdatentypen als auch Objekte als Ausnahmen geworfen werden. Es ist möglich, eine Hierarchie von Ausnahmen zu erstellen, Ausnahmen in Namespaces oder Klassen zu gruppieren und sie nach Typ zu kategorisieren.

Wie die Ausnahmebehandlung in C++ funktioniert

Anstatt ein Programm bei einem Fehler abrupt zu beenden, erlaubt C++ das Werfen (throw) und Abfangen (catch) von Ausnahmen, um Fehler elegant zu behandeln.

Exception werfen

Wenn ein Fehler oder eine unerwartete Situation auftritt, verwendet das Programm das Schlüsselwort throw, um eine Ausnahme auszulösen.

Exception abfangen

Das Programm sucht nach einem passenden catch-Block, um die geworfene Ausnahme abzufangen und zu behandeln.

Exception behandeln

Der catch-Block enthält die Logik, die auf den Fehler reagiert. Dadurch kann sich das Programm erholen oder kontrolliert beendet werden.

Wenn ein Fehler auftritt, wird das C++ Programm normalerweise gestoppt und wirf eine Fehlernachricht - eine sogenannte Exception.

Exception Handling mit try and catch

1. try-catch Block

C++ bietet eine integrierte Funktion zur Behandlung von Ausnahmen mithilfe von try- und catch-Blöcken. Dabei handelt es sich um einen Mechanismus zur Ausnahmebehandlung, bei dem der Code, der möglicherweise eine Ausnahme verursachen kann, im try-Block platziert wird und der Code zur Behandlung der Ausnahme im catch-Block steht.

try {         
    // Code that might throw an exception
} 
catch (ExceptionType e) {   
    // exception handling code
}

Wenn im try-Block eine Ausnahme auftritt, stoppt die Ausführung, und die Kontrolle wird an den passenden catch-Block zur Behandlung weitergegeben.

2. Throwing Exceptions

Eine Ausnahme zu werfen bedeutet, einen bestimmten Wert, der die Ausnahme repräsentiert, aus dem try-Block zurückzugeben. Der passende catch-Block wird anhand des Typs des geworfenen Werts gefunden. Das Schlüsselwort throw wird verwendet, um die Ausnahme zu werfen.

try {         
    throw val
} 
catch (ExceptionType e) {   
    // exception handling code
}

Es gibt drei Arten von Werten, die als Ausnahme geworfen werden können:

2.1 Built-in Types Exceptions

Das Werfen eingebauter Datentypen ist sehr einfach, liefert jedoch keine hilfreichen Informationen.

Zum Beispiel:

#include <bits/stdc++.h>
using namespace std;
 
int main() {
    int x = 7;
    try {
        if (x % 2 != 0) {
 
            // Throwing int
            throw -1;
        }
    }
 
    // Catching int
    catch (int e) {
        cout << "Exception Caught: " << e;
    }
    return 0;
}

Hier müssen wir eine Entscheidung basierend auf dem geworfenen Wert treffen. Das unterscheidet sich nicht wesentlich davon, Fehler mit if-else zu behandeln. In C++ gibt es jedoch eine bessere Methode: Anstatt einfache Werte zu werfen, können wir Objekte von Klassen werfen, die selbst Informationen über die Art der Ausnahme enthalten.

2.2 Throwing Standard Exceptions

Standardausnahmen sind eine Menge von Klassen, die verschiedene Arten häufiger Ausnahmen darstellen. Alle diese Klassen sind im Header <stdexcept> definiert und leiten sich hauptsächlich von der std::exception-Klasse ab, die als Basisklasse für eingebaute Ausnahmen dient.

Das folgende Bild zeigt die Hierarchie der Standardausnahmen in C++:

Diese Ausnahmen werden von Komponenten der C++-Bibliothek ausgelöst, daher sollten wir wissen, wie man sie behandelt. Die Methode what() ist in jeder Standardausnahme vorhanden und liefert Informationen über die Ausnahme selbst.

Zum Beispiel: Die Methode .at() wirft eine out_of_range-Ausnahme, wenn ein Element mit dem angegebenen Index nicht existiert.

#include <bits/stdc++.h>
using namespace std;
 
int main() {
    vector<int> v = {1, 2, 3};
 
    try {
        // Accessing out of bound element
        v.at(10);
    }
    catch (out_of_range e) {
        cout << "Caught: " << e.what();
    }
    return 0;
}
Beispiel 2
// ConsoleApplication1.cpp : Diese Datei enthält die Funktion "main". Hier beginnt und endet die Ausführung des Programms.
//
 
#include <iostream>
#include <string>
 
using namespace std;
 
int main()
{
 
	string eingabe = "";
	bool beenden = false;
 
	do {
 
		try {
			cout << "Wollen Sie das Programm nochmals ausführen (ja/nein)?";
			cin >> eingabe;
 
                        if (cin.fail())
                        {
                                throw invalid_argument("Falsche Eingabe!");
                        }
			else if (eingabe.compare("ja") == 0)
			{
				beenden = false;
			}
			else if (eingabe.compare("nein") == 0)
			{
				beenden = true;
			}
			else
			{
				beenden = false;
				throw invalid_argument("Falsche Eingabe!");
			}
 
		}
		catch (invalid_argument e)
		{
			cout << e.what() << endl;
		}
	} while (beenden == false);
 
}
Beispiel 3
#include <iostream>
using namespace std;
 
void calculate();
 
int main()
{
	int Zahl1 = 0;
	int Zahl2 = 0;
	string Operator;
	string eingabe = "";
	bool beenden = false;
 
	do {
		bool userinput = "";
 
		cout << "Geben Sie die Erste Zahl ein: " << endl;
		try {
			cin >> Zahl1;
			//cout << Zahl1;
 
			if (cin.fail())
			{
				throw invalid_argument("Falsche Eingabe!");
			}
			cout << "Geben Sie die Zweite Zahl ein: " << endl;
 
			cin >> Zahl2;
			if (cin.fail())
			{
				throw invalid_argument("Falsche Eingabe!");
			}
 
			cout << "Geben Sie einen Operator ein: " << endl;
 
			cin >> Operator;
			if (cin.fail())
			{
				throw invalid_argument("Falsche Eingabe!");
			}
 
			calculate();
 
			cout << "Wollen Sie das Programm nochmals ausfuehren (ja/nein)?";
			cin >> eingabe;
			if (cin.fail())
                        {
                                throw invalid_argument("Falsche Eingabe!");
                        }
			else if (eingabe.compare("ja") == 0)
			{
				beenden = false;
			}
			else if (eingabe.compare("nein") == 0)
			{
				beenden = true;
			}
			else
			{
				beenden = false;
				throw invalid_argument("Falsche Eingabe!");
			}	
		}
		catch (invalid_argument e)
		{
			cout << e.what() << endl;
			cin.clear();
			cin.ignore(1000, '\n');
		}
	} while (beenden == false);
}

2.3 Throwing Custom Exceptions

Wenn die Standardausnahmen unsere Anforderungen nicht erfüllen, können wir eine benutzerdefinierte Ausnahmeklasse erstellen. Es wird empfohlen, in dieser Klasse von std::exception zu erben, um eine nahtlose Integration mit Bibliothekskomponenten zu gewährleisten, auch wenn es nicht zwingend erforderlich ist.

Hier ist ein vollständiges Beispiel für eine benutzerdefinierte Ausnahme in C++ mit Code:

#include <iostream>
#include <exception>
using namespace std;
 
// Custom exception class
class NegativeValueException : public exception {
private:
    int value;
public:
    // Constructor
    NegativeValueException(int val) : value(val) {}
 
    // Override what() method
    const char* what() const noexcept override {
        return "Negative value error occurred!";
    }
 
    // Optional: method to get the invalid value
    int getValue() const {
        return value;
    }
};
 
// Function that throws the custom exception
void checkValue(int x) {
    if (x < 0) {
        throw NegativeValueException(x);
    }
    else {
        cout << "Value is: " << x << endl;
    }
}
 
int main() {
    int numbers[] = {10, -5, 20};
 
    for (int n : numbers) {
        try {
            checkValue(n);
        }
        catch (NegativeValueException &e) {
            cout << "Exception caught: " << e.what() 
                 << " Value = " << e.getValue() << endl;
        }
    }
 
    return 0;
}

3. Catching Exceptions

Wie bereits erwähnt, wird der catch-Block verwendet, um Ausnahmen abzufangen, die im try-Block geworfen werden. Der catch-Block nimmt ein Argument entgegen, das vom gleichen Typ wie die Ausnahme sein sollte.

catch (exceptionType e) {
    ...
}

Hier ist e der Name, der der Ausnahme zugewiesen wird. Die Anweisungen innerhalb des catch-Blocks werden ausgeführt, wenn im try-Block eine Ausnahme vom Typ exceptionType geworfen wird.

3.1 Catching Multiple Exceptions

Ein try-Block kann mit mehreren catch-Blöcken verknüpft sein, um verschiedene Arten von Ausnahmen zu behandeln. Zum Beispiel:

try {         
    // Code that might throw an exception
} 
catch (type1 e) {   
    // executed when exception is of type1
}
catch (type2 e) {   
    // executed when exception is of type2
}
catch (...) {
    // executed when no matching catch is found
}

m obigen Code erstellt die letzte Anweisung catch(…) einen Catch-All-Block, der ausgeführt wird, wenn keiner der vorherigen catch-Blöcke passt. Zum Beispiel:

#include <bits/stdc++.h>
using namespace std;
 
int main() {
 
    // Code that might throw an exception
    try {
        int choice;
        cout << "Enter 1 for invalid argument, "
            << "2 for out of range: ";
        cin >> choice;
 
        if (choice == 1) {
            throw invalid_argument("Invalid argument");
        }
        else if (choice == 2) {
            throw out_of_range("Out of range");
        }
        else {
            throw "Unknown error";
        }
 
    }
 
    // executed when exception is of type invalid_argument
    catch (invalid_argument e) {
        cout << "Caught exception: " << e.what() << endl;
    }
 
    // executed when exception is of type out_of_range
    catch (out_of_range e) {
        cout << "Caught exception: " << e.what() << endl;
    }
 
    // executed when no matching catch is found
    catch (...) {
        cout << "Caught an unknown exception." << endl;
    }
    return 0;
}

3.2 Catch by Value or Reference

Genau wie bei Funktionsargumenten kann ein catch-Block Ausnahmen entweder per Wert oder per Referenz abfangen. Beide Methoden haben ihre eigenen Vorteile.

Catch by Value

Das Abfangen von Ausnahmen per Wert erstellt im catch-Block eine neue Kopie des geworfenen Objekts. Da Ausnahmen in der Regel nicht sehr groß sind, entsteht dadurch normalerweise kein großer Overhead.

#include <bits/stdc++.h>
using namespace std;
 
int main() {
    try {
        throw runtime_error("This is runtime exception");
    }
 
    // Catching by value
    catch (runtime_error e) {
        cout << "Caught: " << e.what();
    }
 
    return 0;
}
Catch by Reference

Die Methode catch by reference übergibt lediglich die Referenz auf die geworfene Ausnahme, anstatt eine Kopie zu erstellen. Obwohl dadurch der Kopieraufwand reduziert wird, ist dies nicht der Hauptvorteil dieser Methode. Der wesentliche Vorteil liegt darin, dass polymorphe Ausnahmetypen korrekt abgefangen werden können. Zum Beispiel:

#include <bits/stdc++.h>
using namespace std;
 
int main() {
    try {
        throw runtime_error("This is runtime exception");
    }
 
    // Catching by reference
    catch (exception& e) {
        cout << "Caught: " << e.what();
    }
 
    return 0;
}

Im obigen Beispiel konnten wir die runtime_error-Ausnahme abfangen, indem wir eine Referenz auf die exception-Klasse verwendet haben. Dies ist besonders nützlich, um Ausnahmen zu behandeln, die von anderen Ausnahmen abgeleitet sind, wie es in der Standardausnahme-Hierarchie der Fall ist.

3.3 Handle Uncaught Exceptions

Wenn die Ausnahme von keinem catch-Block abgefangen wird (falls kein Catch-All vorhanden ist), beendet das Ausnahmebehandlungssystem standardmäßig die Programmausführung. Dieses Verhalten kann jedoch geändert werden, indem man einen benutzerdefinierten Terminate-Handler erstellt und ihn mit der Funktion set_terminate() als Standard setzt.

Exception Propagation (Weiterwerfen von Ausnahmen)

Die Ausnahmeweitergabe beschreibt, wie eine Ausnahme im Aufrufstack weitergegeben wird.

Wenn eine Ausnahme geworfen wird, wird die Ausführung des aktuellen Blocks sofort beendet, und alle zugewiesenen Ressourcen werden automatisch freigegeben (außer dynamische Ressourcen, die mit new erstellt wurden).

Beim Stack Unwinding wird der Aufrufstack „abgebaut“, während die geworfene Ausnahme nach einem passenden catch-Block sucht.

Wenn der entsprechende catch-Block gefunden wird, wird die Ausnahme abgefangen und behandelt.

Wird sie nicht abgefangen, terminiert das Programm.

Schauen wir uns den Ablauf anhand des folgenden Beispiels an:

#include <bits/stdc++.h>
using namespace std;
 
// A dummy class
class GfG {
    public:
    GfG() {
        cout << "Object Created" << endl;
    }
    ~GfG() {
        cout << "Object Destroyed" << endl;
    }
};
 
int main() {
    try {
        cout << "Inside try block" << endl;
        GfG gfg;
        throw 10;
		cout << "After throw" << endl;
    }
    catch (int e) {
        cout << "Exception Caught" << endl;
    }
    cout << "After catch";
    return 0;
}

Output:

Inside try block
Object Created
Object Destroyed
Exception Caught
After catch

Im obigen Beispiel:

5. Nested (=Verschachtelte) Try Catch Blocks

In C++ können try-catch-Blöcke auch innerhalb anderer try- oder catch-Blöcke definiert werden. Zum Beispiel:

try {
    // Code...... throw e2
    try {
        // code..... throw e1
    }
    catch (eType1 e1) {
        // handling exception
    }
}
catch (eType e2) {    
    // handling exception
}

Wenn die Ausnahme im inneren try-Block vom Typ eType2 statt eType1 ist, wird sie vom äußeren catch-Block behandelt. Weitere Informationen dazu finden Sie im Artikel Nested Try Blocks in C++.

Eine Ausnahme erneut werfen (Rethrowing an Exception)

Das erneute Werfen einer Ausnahme in C++ bedeutet, dass eine Ausnahme innerhalb eines try-Blocks abgefangen wird und nicht lokal behandelt, sondern erneut geworfen wird, damit sie von einem äußeren catch-Block abgefangen werden kann. Auf diese Weise bleiben Typ und Details der Ausnahme erhalten, sodass sie auf der passenden Ebene im Programm behandelt werden kann.

Dieser Ansatz wird typischerweise verwendet, wenn Ausnahmen auf mehreren Ebenen behandelt werden sollen.