Modernes C++: Best Practices für Ausnahmen und Fehlerbehandlung

In modernem C++ ist in den meisten Szenarien die Verwendung von Ausnahmen die bevorzugte Methode zum Mitteilen und Behandeln von logischen Fehlern und Laufzeitfehler. Dies gilt besonders, wenn der Stapel möglicherweise einige Funktionsaufrufe zwischen der Funktion enthält, die den Fehler entdeckt, und der Funktion, die über den Kontext für die Fehlerbehandlung verfügt. Ausnahmen stellen eine formale, gut definierte Methode für den Code bereit, der Fehler erkennt, um die Informationen an die Aufrufliste (call stack) zu übergeben.

Verwenden von Ausnahmen für Code, der Ausnahmen zulässt

Programmfehler werden häufig in zwei Kategorien unterteilt:

  • Logikfehler, die durch Programmierfehler verursacht werden. Beispiel: Fehler „Der Index befindet sich außerhalb des gültigen Bereichs.“
  • Laufzeitfehler, die nicht der Kontrolle der programmierenden Person unterliegen. Beispiel: Fehler „Netzwerkdienst nicht verfügbar“

In der C-Programmierung sowie in COM erfolgt die Benachrichtigung über Fehler entweder durch Rückgabe eines Werts, der einen Fehlercode oder einen Statuscode für eine bestimmte Funktion darstellt, oder durch Festlegung einer globalen Variablen, die der Aufrufer nach jedem Funktionsaufruf abrufen kann, um festzustellen, ob Fehler gemeldet wurden. Beispielsweise wird bei der COM-Programmierung der HRESULT-Rückgabewert verwendet, um Fehler an den Aufrufer zu übermitteln. Die Win32-API verfügt über die GetLastError-Funktion, um den letzten Fehler abzurufen, der von der Aufrufliste gemeldet wurde. In beiden Fällen liegt es beim Aufrufer, den Code zu erkennen und darauf entsprechend zu reagieren. Wenn der Aufrufer den Fehlercode nicht explizit behandelt, stürzt das Programm möglicherweise ohne Warnung ab. Oder es wird u. U. weiterhin mit fehlerhaften Daten ausgeführt und erzeugt falsche Ergebnisse.

Ausnahmen werden in modernem C++ aus folgenden Gründen verwendet:

  • Eine Ausnahme erzwingt, dass der aufrufende Code eine Fehlerbedingung erkennt und behandelt. Nicht behandelte Ausnahmen beenden die Programmausführung.
  • Einer Ausnahme springt zu der Position in der Aufrufliste, an welcher der Fehler behandelt werden kann. Zwischenfunktionen können die Ausnahme weitergeben. Sie müssen nicht für die Koordination mit anderen Ebenen sorgen.
  • Nachdem eine Ausnahme ausgelöst wurde, zerstört der Entlademechanismus für den Ausnahmestapel alle Objekte im Gültigkeitsbereich nach genau definierten Regeln.
  • Eine Ausnahme ermöglicht die saubere Trennung zwischen dem Code, der den Fehler erkennt, und dem Code, der den Fehler behandelt.

Das folgende vereinfachende Beispiel zeigt die notwendige Syntax zum Auslösen und Abfangen von Ausnahmen in C++:

#include <stdexcept>
#include <limits>
#include <iostream>

using namespace std;

void MyFunc(int c)
{
    if (c > numeric_limits< char> ::max())
    {
        throw invalid_argument("MyFunc argument too large.");
    }
    //...
}

int main()
{
    try
    {
        MyFunc(256); //cause an exception to throw
    }

    catch (invalid_argument& e)
    {
        cerr << e.what() << endl;
        return -1;
    }
    //...
    return 0;
}

Ausnahmen in C++ ähneln denen in Sprachen wie C# und Java. Eine ausgelöste Ausnahme wird im try-Block vom ersten zugehörigen catch-Block abgefangen, dessen Typ dem der Ausnahme entspricht. Das heißt, die Ausführung springt von der throw-Anweisung zur catch-Anweisung. Ist kein verwendbarer catch-Block vorhanden, wird std::terminate aufgerufen und das Programm beendet. In C++ kann jeder Typ ausgelöst werden. Es wird jedoch empfohlen, einen Typ auslösen, der direkt oder indirekt von std::exception abgeleitet ist. Im vorherigen Beispiel wird der Ausnahmetyp invalid_argument in der Standardbibliothek in der <stdexcept>-Headerdatei definiert. C++ bietet oder benötigt keinen finally-Block, um sicherzustellen, dass alle Ressourcen freigegeben werden, wenn eine Ausnahme ausgelöst wird. Die RAII-Technik (Resource Acquisition Is Initialization, Ressourcenbelegung ist Initialisierung), die intelligente Zeiger verwendet, bietet die erforderliche Funktionalität zur Ressourcenbereinigung. Weitere Informationen finden Sie unter Entwurfsrichtlinien für sichere Ausnahmebehandlung. Weitere Informationen zum C++-Stapelentlademechanismus finden Sie unter Ausnahmen und Stapelentladung.

Grundlegende Richtlinien

Stabile Fehlerbehandlung ist in jeder Programmiersprache schwierig. Obwohl Ausnahmen etliche Funktionen bereitstellen, die gute Fehlerbehandlung unterstützen, können sie Ihnen nicht die gesamte Arbeit abnehmen. Um die Vorteile des Ausnahmemechanismus auszuschöpfen, sollten Sie Ausnahmen bei der Entwicklung Ihres Codes einplanen.

  • Verwenden Sie Assertionen, um nach Bedingungen zu suchen, die immer „true“ oder immer „false“ sein sollen. Verwenden Sie Ausnahmen, um Fehler abzufangen, die beispielsweise bei der Eingabevalidierung oder in Parametern von öffentlichen Funktionen auftreten können. Weitere Informationen dazu finden Sie im Abschnitt Ausnahmen und Assertionen.
  • Verwenden Sie Ausnahmen, wenn der Code, der den Fehler behandelt, durch einen oder mehrere eingeschobene Funktionsaufrufe von dem Code getrennt ist, der den Fehler erkennt. Erwägen Sie, Fehlercodes statt leistungskritischer Schleifen zu verwenden, wenn der Code, der den Fehler behandelt, eng mit dem Code gekoppelt ist, der den Fehler erkennt.
  • Für jede Funktion, die eine Ausnahme auslösen oder verteilen kann, sollten Sie eine der drei Ausnahmegarantien bereitstellen: die starke Garantie, die grundlegende Garantie oder die Nothrow-Garantie (noexcept). Weitere Informationen finden Sie unter Entwurfsrichtlinien für sichere Ausnahmebehandlung.
  • Lösen Sie Ausnahmen per Wert aus, fangen Sie Ausnahmen per Referenz ab. Fangen Sie nicht ab, was Sie nicht behandeln können.
  • Verwenden Sie keine Ausnahmespezifikationen; sie sind seit C++11 veraltet. Weitere Informationen dazu finden Sie im Abschnitt Ausnahmespezifikationen und noexcept.
  • Verwenden Sie wenn möglich Ausnahmetypen der Standardbibliothek. Leiten Sie benutzerdefinierte Ausnahmetypen aus der Hierarchie der exception-Klasse ab.
  • Lassen Sie Ausnahmen nicht von Destruktoren oder Funktionen zur Speicherfreigabe auslösen.

Ausnahmen und Leistungsfähigkeit

Der Ausnahmemechanismus verursacht nur geringe Leistungseinbußen, wenn keine Ausnahme ausgelöst wird. Wenn eine Ausnahme ausgelöst wird, sind die Kosten für Stapeldurchlauf und -Entladung ungefähr mit den Kosten eines Funktionsaufrufs vergleichbar. Zwar sind zusätzliche Datenstrukturen erforderlich, um die Aufrufliste zu durchlaufen, nachdem ein try-Block erreicht wurde, und es sind weitere Anweisungen nötig, um den Stapel zu entladen, wenn eine Ausnahme ausgelöst wird. Trotzdem sind in den meisten Szenarien die entsprechenden Leistungseinbußen und der Speicherbedarf nicht signifikant. Die nachteilige Auswirkung von Ausnahmen auf die Leistung ist wahrscheinlich nur auf Systemen mit eingeschränktem Arbeitsspeicher von Bedeutung oder in leistungskritischen Schleifen, in denen ein Fehler wahrscheinlich wiederkehrend auftritt und in denen der den Fehler behandelnde Code eng mit dem Code gekoppelt ist, der den Fehler meldet. In jedem Fall ist es unmöglich, die Effektivkosten von Ausnahmen ohne Codeprofilierung und Messungen zu ermitteln. Auch in den seltenen Fällen, in denen die Kosten signifikant sind, sollten Sie abwägen, ob diese Kosten nicht die Vorteile rechtfertigen (wie korrekter Code, leichtere Verwaltbarkeit und andere), die von einer gut entworfenen Ausnahmerichtlinie sichergestellt werden.

Ausnahmen und Assertionen

Ausnahmen und Assertionen sind zwei verschiedene Mechanismen zum Erkennen von Laufzeitfehlern in einem Programm. Verwenden Sie assert-Anweisungen, um während der Entwicklung Bedingungen zu ermitteln, die immer „true“ oder immer „false“ sein sollen, wenn der gesamte Code fehlerfrei ist. Es gibt keine Möglichkeit, einen solchen Fehler mithilfe einer Ausnahme zu behandeln, weil der Fehler darauf hinweist, dass etwas im Code behoben werden muss. Der Fehler stellt keine Bedingung dar, die das Programm zur Laufzeit beheben muss. assert beendet die Ausführung in der Anweisung, damit Sie den Programmzustand im Debugger überprüfen können. Eine Ausnahme setzt die Ausführung bis zum ersten entsprechenden catch-Handler fort. Verwenden Sie Ausnahmen, um Fehlerbedingungen zu überprüfen, die zur Laufzeit auftreten können (beispielsweise "Datei nicht gefunden" oder "Nicht genügend Arbeitsspeicher"), auch wenn der Code korrekt ist. Ausnahmen können solche Bedingungen behandeln, auch wenn bei der Wiederherstellung nur eine entsprechende Meldung in eine Protokolldatei geschrieben und das Programm beendet wird. Überprüfen Sie immer Argumente für öffentliche Funktionen mithilfe von Ausnahmen. Auch wenn die Funktion fehlerfrei ist, haben Sie möglicherweise keine vollständige Kontrolle über die Argumente, die ein Benutzer übergibt.

C++-Ausnahmen und Windows SEH-Ausnahmen

Sowohl C-Programme als auch C++-Programme können den Mechanismus der strukturierten Ausnahmebehandlung (Structured Exception Handling, SEH) im Windows-Betriebssystem verwenden. Die Konzepte in SEH entsprechen denen für C++-Ausnahmen, außer dass SEH die Konstrukte __try, __except und __finally anstelle von try und catch verwendet. Im Microsoft C++-Compiler (MSVC) werden C++-Ausnahmen für SEH implementiert. Wenn Sie jedoch C++-Code schreiben, verwenden Sie die Syntax für C++-Ausnahmen.

Weitere Informationen zu SEH finden Sie unter Structured Exception Handling (C/C++).

Ausnahmespezifikationen und noexcept

Ausnahmespezifikationen wurden in C++ als Möglichkeit eingeführt, um die Ausnahmen festzulegen, die eine Funktion auslösen kann. Ausnahmespezifikationen haben sich in der Praxis jedoch als problematisch herausgestellt und wurden im Normenentwurf C++11 als veraltet gekennzeichnet. Es wird empfohlen, throw-Ausnahmespezifikationen nicht zu verwenden, mit Ausnahme von throw(), wodurch angegeben wird, dass die Funktion das Auslösen von Ausnahmen nicht zulässt. Wenn Sie Ausnahmespezifikationen im veralteten Format throw( type-name ) verwenden müssen, ist die MSVC-Unterstützung eingeschränkt. Weitere Informationen finden Sie unter Ausnahmespezifikationen (throw). Der Bezeichner noexcept wird in C++11 als die bevorzugte Alternative zu throw() eingeführt.

Weitere Informationen

Verbinden von Code, der Ausnahmen zulässt, mit Code ohne Ausnahmen
C#-Programmiersprachenreferenz
C++-Standardbibliothek