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!