Suchen

C++ Error Handling Revisited

Autor / Redakteur: Raphael Meyer / Sebastian Gerstl

Eine gut funktionierende Fehlerbehandlung ist in der Softwareentwicklung essentiell und gerade für Embedded-Systeme unverzichtbar. Funktionale Programmiersprachen, wie etwa der C++17-Standard, haben hier einige vielversprechende Konzepte zu bieten.

Firmen zum Thema

Der Einsatz von C++17 ermöglicht eine bessere Fehlerbehandlung in Code, etwa in der Form der Kommunikation von auftretenden Fehlern, aber auch mit Hilfe einer eindeutigeren Klassifizierung.
Der Einsatz von C++17 ermöglicht eine bessere Fehlerbehandlung in Code, etwa in der Form der Kommunikation von auftretenden Fehlern, aber auch mit Hilfe einer eindeutigeren Klassifizierung.
(Bild: gemeinfrei / Pixabay )

Die Behandlung von Fehlern und Ausnahmesituationen ist nicht nur ein wichtiger Teil jeder Software, sondern oft auch sehr umfangreich. Gerade bei Embedded Software, welche von der Interaktion mit Hardware geprägt ist, ist eine sorgfältige Fehlerbehandlung für den einwandfreien Betrieb der entwickelten Geräte unabdingbar.

Funktionale Programmiersprachen bieten hier interessante Konzepte, welche nicht ganz neu sind, in letzter Zeit aber vermehrt wiederentdeckt werden. Die beiden in C++17 dazu gestoßenen Templateklassen std::optional und std::variant können helfen, die Signaturen von Funktionen und Methoden sprechender zu gestalten.

In der Softwareentwicklung gibt es unterschiedliche Typen von Fehlern. Wir konzentrieren uns hier auf Interaktionen mit der Außenwelt, die nicht zum gewünschten Ergebnis führen. Zum Beispiel die Bewegung eines Motors, die nicht zu Ende geführt werden kann. Mit anderen Worten sprechen wir von Funktionen mit Seiteneffekten, die nicht erfolgreich ausgeführt werden konnten.

Wie können Fehler kommuniziert werden?

Eine Funktion, die eine Interaktion mit Seiteneffekten abstrahiert, kann das Resultat auf unterschiedliche Art zurückmelden. In C++ ist es unter anderem üblich, einen Return Code zurückzugeben. Dies kann ein einfacher boolscher Wert, eine Enumeration oder ein Zahlencode sein. Die Verwendung von C++ Exceptions ist eine weitere Möglichkeit.

Verwendet eine Funktion Return Codes, so muss ein allfälliger Rückgabewert als Ausgabeparameter zurückgegeben werden. Ausgabeparameter können beispielsweise mit Referenzen oder (intelligenten) Zeigern implementiert werden. Der Nachteil ist, dass deren Absicht ohne zusätzliche Dokumentation oftmals nicht ersichtlich ist. Es tauchen Fragen zu den Besitzverhältnissen auf und es braucht weitere Erklärungen zum Zustand oder zur Gültigkeit eines Ausgabeparameters im Fehlerfall.

Die Auswertung von Return Codes kann auf unterschiedliche Art geschehen. Die zwei häufigsten Muster sind verschachtelte if-Anweisungen und Early Returns. Verschachtelte if-Anweisungen skalieren schlecht mit dem Ablauf, den man in einem Block ausführen möchte. Die Verschachtelungstiefe nimmt mit jedem zusätzlichen Schritt zu.

Betrachten wir zur Veranschaulichung ein einfaches Beispiel. Wir haben ein einfaches Gerät zum automatischen Bewässern einer Topfpflanze. Aufgrund der Umgebungstemperatur und der Feuchtigkeit im Topf soll jeweils entschieden werden, wie viel Wasser in den Topf gepumpt wird.

Bild 1: Return Codes und verschachtelte if-Anweisungen
Bild 1: Return Codes und verschachtelte if-Anweisungen
(Bild: bbv Software Services)

Beim Early Return unterbrechen die if-Anweisungen zwischen den einzelnen Schritten leider den ursprünglichen Ablauf, und wirken sich nachteilig auf die Lesbarkeit aus. Ein prominentes Beispiel für Early Return ist die Programmiersprache Go. In Go ist es das Standardvorgehen zur Fehlerauswertung.

Bild 2: Return Codes und Early Returns
Bild 2: Return Codes und Early Returns
(Bild: bbv Software Services)

Werden Exceptions verwendet, so bleibt der eigentliche Ablauf kompakt und übersichtlich. Die Ausgabeparameter entfallen ebenso. In der Embedded Softwareentwicklung werden Exceptions jedoch aus verschiedenen Gründen vielfach vermieden, manchmal auch ungerechtfertigt.

Bild 3: Beispiel unter Verwendung von Exceptions
Bild 3: Beispiel unter Verwendung von Exceptions
(Bild: bbv Software Services)

Ein Blick über den Zaun

Die Behandlung von Seiteneffekten ist in der Programmiersprache Haskell auf elegante Art und Weise gelöst. Das erwähnte Beispiel könnte in Haskell folgendermaßen aussehen:

Bild 4: Beispielhafte Implementierung in Haskell
Bild 4: Beispielhafte Implementierung in Haskell
(Bild: bbv Software Services)

Der Code wird durch die Fehlerbehandlung nicht gestört. Es ist aber trotzdem klar, dass hier Seiteneffekte behandelt werden. Das Maybe Volume in der Funktionssignatur signalisiert, dass die Funktion nur vielleicht einen Wert zurückgibt. Der Rückgabewert kann entweder Just <volume> oder Nothing sein.

Die Anweisung do bedeutet, dass die darin aufgerufenen Funktionen vielleicht auch nichts zurückgeben. Sobald die erste innere Funktion nichts zurückliefert, werden die nachfolgenden Aufrufe nicht mehr getätigt, und die Funktion gibt Nothing zurück.

Wenn zusätzlich zum Auftreten eines Fehlers auch Informationen zum Fehler selbst mitgeteilt werden sollten, kann der abstrakte Datentyp Either verwendet werden. Ein Either hat entweder den Wert Right <value> oder Left <error>. Ist der Wert ein Left, handelt es sich um einen Fehler.

In der noch jungen Programmiersprache Rust werden die beiden Typen Option und Result verwendet, um Fehler zu melden. Sie können mit Maybe und Either aus Haskell verglichen werden. Außerdem enthält Rust Spracheigenschaften, die hilfreich sind bei der Verwendung der beiden Typen. Zum Beispiel das Statement match oder der Operator ?.

Bild 5: Exemplarische Implementation in Rust
Bild 5: Exemplarische Implementation in Rust
(Bild: bbv Software Services)

Das match ist eine vereinfachte Form von dem, was man in Haskell als Pattern Matching kennt. In Rust ist es darauf abgestimmt, mit Typen wie Result umzugehen.

Neues aus C++17

Mit C++17 wurde die Klasse std::optional in die Standard Library aufgenommen. Sie kann als C++ Variante von Maybe angesehen werden.

Mit std::optional muss ein Rückgabewert nicht mehr als Ausgabeparameter definiert werden. Das Verhalten einer Funktion lässt sich so einfacher aus der Signatur ableiten, als bei einer Implementation mit Return Code und Ausgabeparameter.

Bild 6: Verwendung von std::optional
Bild 6: Verwendung von std::optional
(Bild: bbv Software Services)

std::variant ist eine weitere Templateklasse, welche mit C++17 neu dazugekommen ist. Damit lassen sich Typen wie ein Either aus Haskell oder ein Result aus Rust nachbilden.

Bild 7: Direkte Verwendung von std::variant
Bild 7: Direkte Verwendung von std::variant
(Bild: bbv Software Services)

Verwendet man std::variant direkt, um ein Result zu imitieren, so ist dessen Verwendung zuweilen etwas umständlich. Dies rührt unter anderem daher, dass Templateklassen aus der Standard Library möglichst allgemein gehalten werden, damit Erweiterungen in alle Richtungen möglich sind.

Mit einem einfachen Wrapper um std::variant, kann man die Lesbarkeit des Code aber stark erhöhen.

Bild 8: Eine einfache Implementation von Result
Bild 8: Eine einfache Implementation von Result
(Bild: bbv Software Services)

Bild 9: Anwendungsbeispiel mit der eigenen Klasse Result
Bild 9: Anwendungsbeispiel mit der eigenen Klasse Result
(Bild: bbv Software Services)

Das Weiterleiten von Fehlern erfolgt zwar immer noch manuell mit if-Anweisungen, aber die Signaturen drücken die Absicht der Funktionen und Methoden besser aus. Der Code wird durch das Interface der Klasse Result klarer.

Natürlich ist es denkbar, dass man die Funktionsweise eines Haskell do-Blocks oder dem ?-Operator aus Rust nachzubilden versucht. Man wird dabei aber kaum darum herumkommen, dass Funktionsaufrufe in Form von Funktionsobjekten benötigt werden. Das heißt wiederum, dass diese zusätzlich eingepackt werden müssen, beispielsweise mit Lambda-Ausdrücken oder std::bind.

Zusammenfassung

Wie am Beispiel von Haskell gezeigt, bieten funktionale Programmiersprachen interessante Möglichkeiten zur Fehlerbehandlung an. Entstanden ist dies vor allem aus einer Notwendigkeit heraus, denn auch funktionale Sprachen müssen mit Seiteneffekten umgehen können. Die Eleganz, mit der dieses Problem gelöst wurde, hat dazu geführt, dass Merkmale von funktionalen Sprachen vermehrt auch in konventionelle und etablierte Sprachen einfließen.

Die beiden Templateklassen std::optional und std::variant aus C++17 sind ein gutes Hilfsmittel, um Funktionen sprechender zu gestalten. Durch das Vermeiden von Ausgabeparametern kommen Fragen nach deren Gültigkeit oder Zustand im Fehlerfall erst gar nicht auf.

(Dieser Beitrag wurde mit freundlicher Genehmigung des Autors dem Tagungsband Embedded Software Engineering Kongress 2019 entnommen.)

Literatur- und Quellenverzeichnis

[1] Learn You a Haskell for Great Good!, Miran Lipovača
[2] https://golang.org/
[3] https://www.rust-lang.org/

Autor

Raphael Meyer
Raphael Meyer
(Bild: bbv Software Services)

Raphael Meyer ist Software-Ingenieur mit über zehn Jahren Erfahrung in der Geräteentwicklung. Er interessiert sich stark für Themen in der Software-Entwicklung, die dazu beitragen qualitativ hochstehende Produkte zu entwickeln. Deshalb ist er auch in der Software Crafters Community aktiv. Außerdem ist Raphael Meyer ein Organisator der C++ Usergroup Zentralschweiz.

Artikelfiles und Artikellinks

(ID:46836730)