C++ Tutorial

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.

1: double CalcSqrt(double val)
2: {
3:    if (val < 0.0)
4:       return -1.0;
5:    return sqrt(val);
6: }
7: int main()
8: {
9:    ...
10:   auto res = CalcSqrt(var);
11:   if (res < 0.0)
12:      ... // Fehler behandeln
13:   ...
14: }

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.

Einleiten und Auslösen einer Ausnahme

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

Eine 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 spielt es dann keine Rolle, ob der Fehler innerhalb des Blocks oder in einer im Block aufgerufenen Funktion auftritt.

try
{
  ...
}

In unserem Beispiel soll die Funktion CalcSqrt() später 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.

1: double CalcSqrt(double val)
2: {
3:    if (val < 0.0)
4:       ...
5:    return sqrt(val);
6: }
7: int main()
8: {
9:    ...
10:   try
11:   {
12:      auto res = CalcSqrt(var);
13:      ...
14:   }
15: }

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

throw PARAM;

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

Damit kann die Funktion CalcSqrt() wie folgt erweitert werden.

1: double CalcSqrt(double val)
2: {
3:    if (val < 0.0)
4:       throw 1;
5:    return sqrt(val);
6: }
7: int main()
8: {
9:    ...
10:   try
11:   {
12:      auto res = CalcSqrt(var);
13:      ...
14:   }
15: }

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.

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

Auch hier ignorieren wir vorerst die Bedeutung von PARAM und setzen dafür die Folge ... (3 Punkte) ein. Innerhalb des catch-Blocks 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 angegeben vervollständigt werden. Wie erwähnt, muss der catch-Block unmittelbar nach dem try-Block stehen. Andere Anweisungen zwischen diesen Blöcken sind nicht erlaubt.

#include <print>
#include <cmath>
double CalcSqrt(double val)
{
   if (val < 0.0)
      throw 1;
   return sqrt(val);
}
int main()
{
   double var1=1.44, var2=-1.;
   try
   {
      std::println("sqrt({}): {}",var1,CalcSqrt(var1));
      std::println("sqrt({}): {}",var2,CalcSqrt(var2));
   }
   catch (...)
   {
      std::println("Fehler in CalcSqrt()!");
   }
   std::println("Programmende");
}

sqrt(1.44): 1.2
Fehler in CalcSqrt()!
Programmende

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

Wird an CalcSqrt() ein positiver Wert übergeben, berechnet die Funktion die Quadratwurzel und gibt diese an main() zurück. In main() werden dann die weiteren Anweisungen innerhalb des try-Blocks ausgeführt. Der auf den try-Block folgende catch-Block wird anschließend übersprungen, da dessen Anweisungen nur im Falle einer Ausnahme ausgeführt werden.

Wird jedoch an CalcSqrt() ein negativer Wert übergeben, führt dies zum Auslösen einer Ausnahme mittels throw 1. Die Ausführung der throw-Anweisung bewirkt, dass sofort zu dem auf den try-Block folgenden catch-Block gesprungen wird. In unserem Beispiel wird nach der throw-Anweisung als nächstes die println-Anweisung im catch-Block ausgeführt. Ist der catch-Block abgearbeitet wird normal mit der Programmausführung fortgefahren.

Ausnahmen in Funktionen/Methoden

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 dem try-Block und der throw-Anweisung mehrere Funktionsebenen liegen.

1: void Func2()
2: {
3:    if (...)
4:       throw 1;
5:    ...
6: }
7: void Func1()
8: {
9:    Func2();
10:   ...
11: }
12: int main()
13: {
14:    try
15:    {
16:       Func1();
17:       ...
18:    }
19:    catch(...)
20:    {
21:       ... // Fehlerbehandlung
22:    }
23: }

Auch hier kehrt das Programm nach dem Auslösen einer Ausnahme in Func2() sofort in den catch-Block in main() zurück. D.h., es werden nach dem Auslösen der Ausnahme keine Anweisungen mehr in Func2() und Func1() sowie innerhalb des try-Blocks ausgeführt.

Ausnahmen im Konstruktor

Mithilfe der Ausnahme-Behandlung 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.

1: Window::Window(int width,...)
2: {
3:    ...
4:    if (width < 50)
5:       throw 1;
6:    ...
7: }

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

Wird im Konstruktor eine Ausnahme ausgelöst, wird der Destruktor nicht mehr aufgerufen. In diesem Fall 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!

Weiterleiten von Ausnahmen

Unter Umständen kann es erforderlich sein, ein und dieselbe Ausnahme an mehreren Stellen im Programmablauf zu behandeln. Im nachfolgenden Beispiel soll auf die in Func2() ausgelöste Ausnahme sowohl in Func1() wie auch in main() reagiert werden, d.h., die Ausnahme muss nach ihrer ersten Verarbeitung in Func1() weitergeleitet werden. Dazu wird zunächst der Aufruf der Funktion Func2() innerhalb von Func1() in einen try-Block gelegt, auf den dann der dazugehörige catch-Block folgt. Zusätzlich ist der Aufruf von Func1() in main() in einen try-Block zu legen. Löst die Funktion Func2() nun eine Ausnahme aus, wird diese zunächst im catch-Block von Func1() abgefangen und kann dort bearbeitet werden. Jetzt gilt es noch main() vom Auftreten der Ausnahme zu benachrichtigen. Und dies wird dadurch erreicht, dass die im catch-Block in Func1() aufgefangene Ausnahme durch die Anweisung throw; erneut ausgelöst und damit an main() weitergeleitet wird.

1: void Func2()
2: {
3:    if (...)
4:       throw 1; // Ausnahme auslösen
5:    ...
6: }
7: void Func1()
8: {
9:    try
10:   {
11:      Func2();
12:   }
13:   catch(...) // Ausnahme abfangen
14:   {
15:      std::cout << "Func2-Fehler!\n";
16:      throw; // Ausnahme weiterleiten
17:   }
18: ...
19: }
20: int main()
21: {
22:    try
23:    {
24:       Func1();
25:    }
26:    catch(...) // Ausnahme abfangen
27:    {
28:       std::cout << "Fehler aufgetreten\n";
29:      exit(1);
30:    }
31:    ...
32: }

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 unterscheiden zu können besteht darin, der throw-Anweisung unterschiedliche Werte mitzugeben, die alle den gleichen Datentyp besitzen. 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.

1: enum Error {ZDIV, SQRT};
2: void Func()
3: {
4:    if (...)
5:       throw ZDIV; // 1. Ausnahme auslösen
6:    ...
7:    if (...)
8:       throw SQRT; // 2. Ausnahme auslösen
9: }
10: int main()
11: {
12:    try
13:    {
14:       Func();
15:    }
16:    catch(const Error& err)
17:    {
18:       // Ausnahme auswerten
19:       switch(err)
20:       {
21:       case ZDIV:
22:          std::cout << "0 Division\n";
23:          break;
24:       case SQRT:
25:          std::cout << "Wurzelberechnung\n";
26:          break;
27:       }
28:    }
29:    ...
30: }

Sehen wir uns die zweite Form an, wie Ausnahmen unterschieden werden. Wie erwähnt, hängt der Datentyp des Wertes der throw-Anweisung mit dem Datentyp des Parameters der catch-Anweisung zusammen. Wenn verschiedene throw-Anweisungen nun unterschiedliche Datentypen als Argument besitzen, können unterschiedliche Exception-Handler ausgeführt werden. Im nachfolgenden Beispiel löst die Funktion Func() eine Ausnahme mit einem int-Wert als Argument aus und eine zweite Ausnahme mit einem char-Zeiger. D.h., es wird ein Exception-Handler zum Auffangen der 'int-Ausnahme' benötigt und einer zum Auffangen der 'char-Zeiger-Ausnahme'. Dabei ist zu beachten, dass beide Exception-Handler unmittelbar hintereinanderstehen. Zwischen dem try-Block und den Exception-Handlern dürfen keine anderen Anweisungen stehen. Die Reihenfolge der Exception-Handler spielt zunächst keine Rolle.

1: void Func()
2: {
3:    if (...)
4:       throw 1; // 1. Ausnahme auslösen
5:    ...
6:    if (...)
7:       throw "A"; // 2. Ausnahme auslösen
8: }
9: int main()
10: {
11:    try
12:    {
13:       Func();
14:    }
15:    catch(int val)
16:    {
17:       std::cout << std::format("Fehler {}\n", val);
18:    }
19:    catch(const char *const pT)
20:    {
21:       std::cout << std::format("Fehler {}\n", pT);
22:    }
23:    ...
24: }

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.

1: int main()
2: {
3:    try
4:    {
5:       ...
6:    }
7:    catch(int val)
8:    {
9:       ...
10:   }
11:   catch(const char *const pT)
12:   {
13:      ...
14:   }
15:   catch(...) // Sammelt alle Exceptions auf, welche
16:   {          // nicht vom Typ int oder char* sind
17:      ...
18:   }
19:   ...
20: }

Ausnahme-Klassen

Bisher wurden zum Auslösen von Ausnahmen nur intrinsische Datentypen, wie int oder char-Zeiger, verwendet. Da die Ausnahme-Behandlung aber beliebige Datentypen zulässt, können ebenfalls Klassen hierfür eingesetzt werden.

Definition der Ausnahme-Klasse

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 (errNum) und ein Feld mit den korrespondierenden Fehlermeldungen (pErrText). Das Feld mit den Fehlertexten ist als statische Eigenschaft definiert, da die Fehlertexte für alle Objekte dieser Klasse nur einmal definiert werden müssen. Die Fehlernummern sind als enum-Werte innerhalb der Klasse definiert. Dadurch, dass hier sowohl die Fehlernummern wie auch die Fehlertexte in einer Klasse zusammengefasst sind, ist das Anwenderprogramm unabhängig von der internen Nummerierung der Fehler und dem zum Fehler gehörigen Fehlertext. Für die Ausgabe des Fehlertextes wurde der Klasse der überladene Operator << hinzugefügt.

1: class Ex
2: {
3:    short errNum; // Fehlernummer
4:    // Fehlertexte
5:    inline static const char *pErrText[] =
6:    {
7:       "Division durch 0",
8:       "Wurzel aus negativer Zahl"
9:    };
10: public:
11:   enum eMathErr{ZDIV, SQRT}; // enum mit den Fehlern
12:   // ctor
13:   Ex(eMathErr err)
14:   {
15:      errNum = err;
16:   }
17:   // copy-ctor
18:   Ex(const Ex& src) = default;
19:   // Ausgabe des Fehlertextes
20:   friend ostream& operator << (ostream& os, const Ex& e);
21: };
22: ostream& operator <<(ostream& os, const Ex& e)
23: {
24:   os << std::format("Fehler {} getreten!\n",
25:   e.pErrText[e.errNum]);
26:   return os;
27: }

Wenn für die Ausnahmebehandlung eine Klasse eingesetzt wird, sollte diese immer den Kopierkonstruktor definieren, denn beim Auslösen einer Ausnahme wird u.U. eine Kopie des Ausnahme-Objekts erstellt und an den Exception-Handler übergeben. 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 Ausnahme-Klasse

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

1: class Ex
2: {
3:    ...
4: };
5: void Func()
6: {
7:    if (...)
8:       throw Ex(Ex::ZDIV);
9:    ...
10:   if (...)
11:      throw Ex(Ex::SQRT);
12: }

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

1: int main()
2: {
3:    try
4:    {
5:       Func(val);
6:    }
7:    catch (const Ex& ex)
8:    {
9:       std::cout << ex;
10:   }
11:   ...
12: }

Ableiten von Ausnahme-Klassen

Wie im Kapitel über Ableiten von Klassen erwähnt, lassen sich von einer Basisklasse weitere Klassen ableiten. Und dies gilt auch für Ausnahme-Klassen. 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.

1: // Basis Ausnahme-Klasse
2: class Ex
3: {...};
4: // Ausnahme-Klasse für 0-Division
5: class ExDiv: public Ex
6: {...};

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.

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

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

1: try
2: {
3:    Div(2);
4:    Div(0);
5: }
6: catch(const ExDiv& e) // abgeleitete Klasse
7: {
8:    ...
9: }
10: catch(const Ex& e)   // Basisklasse
11: {
12:    ...
13: }

Standard-Ausnahmen

Die C++-Standardbibliothek definiert eine Reihe von Ausnahme-Klassen, die zum Auslösen von Ausnahmen in der Standardbibliothek verwendet werden. Die Ausnahme-Klassen sind, bis auf zwei Ausnahmen, in der Header-Datei <stdexcept> definiert und liegen ebenfalls im Namensraum std.

Die Basisklasse für alle Ausnahme-Klassen 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 Ausnahme-Klassen 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

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 muss explizit durch Aufruf der Stream-Methode exceptions(flag) freigegeben werden. Als Parameter erhält exceptions() ein oder mehrere der nachfolgenden Flags:

Flag
Bedeutung
std::ios::failbit
Beim Einlesen konnten die erwarteten Zeichen nicht eingelesen werden oder bei der Ausgabe konnte das Zeichen 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 Ausnahme 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. Das nachfolgende Beispiel demonstriert den Einsatz von Ausnahmen beim Bearbeiten einer Datei.

1: std::ifstream inFile;
2: // Alten Exception-Status retten
3: auto eOld = inFile.exceptions();
4: // Exceptions aktvieren
5: inFile.exceptions(std::ios::failbit | std::ios::eofbit);
6: try
7: {
8:     // Datei bearbeiten
9:     ...
10: }
11: // Datei-Exceptions abfangen
12: catch(std::ios_base::failure& e)
13: {
14:    ...
15: }

Nach der Freigabe der Ausnahmen durch den Aufruf der Methode exceptions() wird innerhalb des try-Blocks die Datei bearbeitet. Tritt beim Öffnen der Datei ein Fehler auf oder wird das Dateiende erreicht, wird eine ios_base::failure Ausnahme ausgelöst. Innerhalb des Exception-Handlers kann dann 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.

Auslösen von Standard-Ausnahmen

Um in einer Anwendung eine der Standard-Ausnahme 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.

1: void Check(int param)
2: {
3:    if (param>10)
4:    {
5:       auto err = std::format("overflow in {}, Zeile {}\n",
6:                              __FILE__, __LINE__);
7:       throw std::overflow_error(err);
8:    }
9:    if (param<0)
10:   {
11:      auto err = std::format("underflow in {}, Zeile {}\n",
12:                             __FILE__, __LINE__);
13:      throw std::underflow_error(err);
14:   }
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 später 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 Beispiels an.

1: void f1() noexcept
2: {
3:    throw "Ausnahme in f1() ausgeloest";
4: }
5: void f2()
6: {
7:    throw "Ausnahme in f2() ausgeloest";
8: }
9: void f3() noexcept(noexcept(f2()))
10: {
11:    f2();
12: }

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.

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:

1: // Beliebige Struktur
2: struct CAny
3: {
4:    char v1;
5:    int v2;
6:    char v3;
7:    long v4;
8: };
9: // Belegter Speicher durch die Strukturelemente
10: constexpr auto SIZEOFELEMENTS =
11:      sizeof CAny::v1 + sizeof CAny::v2 +
12:      sizeof CAny::v3 + sizeof CAny::v4;
13: // Pruefen, ob Strukturelemente ohne Füllbytes
14: // im Speicher liegen
15: static_assert(sizeof CAny == SIZEOFELEMENTS,
16:              "Struktur enthaelt Fuellbytes!");

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, wird die static_assert Anweisung eingesetzt. 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.

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


Copyright 2024 © Wolfgang Schröder
E-Mail mit Fragen oder Kommentaren zu dieser Website an: info@cpp-tutor.de
Impressum & Datenschutz