C++ Core Guidelines: Regeln für die Fehlerbehandlung

Modernes C++  –  1 Kommentare

Fehlerbehandlung ist ein wichtiger Bestandteil guter Software. Daher bieten die C++ Core Guidelines rund 30 Regeln für die Fehlerbehandlung an.

Welche Aspekte gehören laut der Guidelines zur Fehlerbehandlung?

  • Detecting an error
  • Transmitting information about an error to some handler code
  • Preserve the state of a program in a valid state
  • Avoid resource leaks

Du solltest Ausnahmen für die Fehlerbehandlung verwenden. David Abrahams, einer der Gründer der Boost-Library und ehemaliges Mitglied des ISO-C++-Standardisierungskomitees, formalisiert in seinem Dokument "Exception-Safety in Generic Components", was exception-safety bedeutet. Die Abrahams Guarantees beschreiben einen Vertrag, der grundlegend ist, wenn du über exception-safety nachdenkst. Daher werde ich ihn kurz schildern und mich in weiteren Artikel darauf beziehen. Hier sind die vier Abstufungen des Vertrags aus der gerade zitieren Wiki-Seite in absteigender Reihenfolge:

  1. No-throw guarantee, also known as failure transparency: Operations are guaranteed to succeed and satisfy all requirements even in exceptional situations. If an exception occurs, it will be handled internally and not observed by clients.
  2. Strong exception safety, also known as commit or rollback semantics: Operations can fail, but failed operations are guaranteed to have no side effects, so all data retains their original values.
  3. Basic exception safety, also known as a no-leak guarantee: Partial execution of failed operations can cause side effects, but all invariants are preserved and there are no resource leaks (including memory leaks). Any stored data will contain valid values, even if they differ from what they were before the exception.
  4. No exception safety: No guarantees are made.

Hier nochmals die vier Punkte kurz und kompakt: Die stärkste Garantie ist, dass keine Ausnahme auftritt (1). Falls eine auftritt, kann das System wieder auf den Zustand vor der Ausnahme gesetzt werden (2). Punkt (3) sichert zu, dass kein Ressourcenleck durch eine Ausnahme entsteht. Letztlich gibt (4) keine Garantie.

Oft ist es nicht möglich, dass ein Programm sich vollkommen von einer Ausnahme erholt. Nun hast du zwei Möglichkeiten: Zuerst einmal kannst du das Programm in einem einfacheren Zustand weiter ausführen. Das heißt, dass die Software nicht mehr ihre volle Funktionalität besitzt, aber zumindest ihre Grundfunktionen noch zur Verfügung stellt. Zum Beispiel kann dies bedeuten, dass ein Defibrillator nicht mehr zum Defibrillieren verwendet werden kann, aber zumindest noch Anweisungen an den Operator geben kann.

Natürlich kann das Programm einfach neu gestartet werden. Oft ist dies der schnellste und einfachste Weg, um in einen sicheren Zustand zu kommen und wieder die volle Funktionalität anzubieten.

Die Regeln der Guidelines sollen dir helfen, die folgenden Fehler zu vermeiden:

  • Type violations
  • Resource leaks
  • Bounds errors
  • Lifetime errors
  • Logical errors
  • Interface errors

Nach meinen doch recht theoretischen Bemerkungen geht es jetzt los mit den ersten drei Regeln:

Die ganze Regel besteht nur aus dieser Begründung: "A consistent and complete strategy for handling errors and resource leaks is hard to retrofit into a system." Um ehrlich zu sein, dass ist mir viel zu wenig für eine Begründung. Fehlerbehandlung ist ein sogenannter cross-cutting concern wie Logging oder Security. Das bedeutet, cross-cutting concerns sind schwierig umzusetzen, da sie sich nicht einfach modularisieren lassen. Sie haben darüber hinaus Einfluss auf die ganze Software.

Exception-safety ist ein wichtiger Bestandteil des Interface-Entwurfs und muss somit von Anfang an im Fokus stehen. Nun ist natürlich die Frage: Was ist ein Interface? Meine Definition eines Interfaces ist relativ breit gehalten.

Ein Interface ist ein Protokoll zwischen zwei Komponenten. Eine Komponente kann eine Funktion, ein Objekt, ein Subsystem oder das ganze System sein. Eine Komponente kann aber auch eine externe Abhängigkeit wie Hardware oder ein Betriebssystem sein.

An der Grenze gibt es zwei Arten der Kommunikation: reguläre und irreguläre. Die reguläre Kommunikation ist der funktionale Aspekt des Interfaces. Oder anders ausgedrückt: Was das System tun soll. Die irreguläre Kommunikation steht für die nichtfunktionale Aspekte des Interfaces. Sie geben vor, wie sich das System verhalten soll. Ein großer Teil der nichtfunktionalen Aspekte ist die Fehlerbehandlung oder einfach das, was schiefgehen kann. Oft werden die nichtfunktionalen Aspekte schlicht Qualitätsattribute genannt.

Allgemeiner betrachtet, besteht das Interface aus zwei Komponenten. Jede Komponente muss dabei einen speziellen Vertrag einhalten.

  1. Eine Vorbedingung (Precondition), die immer gelten muss, bevor die Komponente aufgerufen wird.
  2. Eine Invariante (Invariant), die immer gelten muss, wenn die Komponente ausgeführt wird.
  3. Eine Nachbedingung (Postcondition), die immer gelten muss, nachdem die Komponente aufgerufen wurde.

Diese Begrifflichkeit geht auf Betrand Meyer zurück und ist unter dem Namen Design by Contract bekannt.

Von Fabuio - Own work (CC0) (Bild: https://commons.wikimedia.org/w/index.php?curid=38020523 )

Bevor ich weiterschreibe, möchte ich gerne erwähnen, dass C++20 wohl Contracts unterstützen wird.

Die Konsequenz dieser Regel ist es, dass sich der Aufrufer der Funktion mit einer try/catch-Anweisung um die Ausnahme kümmern muss. Die Frage ist natürlich: Wann sollst du eine Ausnahme werfen?

Hier sind die typischen Anwendungsfälle:

  • Eine Vorbedingung kann nicht eingehalten werden.
  • Ein Konstruktor kann das Objekt nicht erzeugen.
  • Eine out-of-range-Ausnahme tritt auf.
  • Eine Ressource kann nicht angefordert werden.

Dies ist meiner Meinung nach der schlimmste Missbrauch einer Ausnahme. Ausnahmen sind eine Art goto-Anweisung. Es kann sein, dass deine Richtlinien für Sourcecode die Verwendung von goto-Anweisungen verbieten. Daher kommst du auf eine sehr clevere Idee: Ausnahmen für den Kontrollfluss einzusetzen.

Das folgende Beispiel verwendet Ausnahmen im Erfolgsfall.

// don't: exception not used for error handling
int find_index(vector<string>& vec, const string& x)
{
try {
for (gsl::index i = 0; i < vec.size(); ++i)
if (vec[i] == x) throw i; // found x
} catch (int i) {
return i;
}
return -1; // not found
}

Das Codebeispiel verwendet gsl::index der Guidelines Support Library. Im Beispiel ist der reguläre Kontrollfluss nicht von dem Kontrollfluss bei einer Ausnahme getrennt. Im Erfolgsfall wirft der Code eine Ausnahme; im Fehlerfall verwendet der Code eine return-Anweisung. Wenn das nicht verwirrend ist?

Selbstverständlich werde ich in meinem nächsten Artikel mit den Regeln zur Fehlerbehandlung fortsetzen. Insbesondere werde ich über Vorbedingungen, Invarianten und Nachbedingungen schreiben.

Auf meinem deutschen Blog und meinem englischen Blog findet gerade die Wahl zum nächsten PDF-Päckchen statt. Hier sind die Links zur Wahl: