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.
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.
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: 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: 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: 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: 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 ?.
Stand: 08.12.2025
Es ist für uns eine Selbstverständlichkeit, dass wir verantwortungsvoll mit Ihren personenbezogenen Daten umgehen. Sofern wir personenbezogene Daten von Ihnen erheben, verarbeiten wir diese unter Beachtung der geltenden Datenschutzvorschriften. Detaillierte Informationen finden Sie in unserer Datenschutzerklärung.
Einwilligung in die Verwendung von Daten zu Werbezwecken
Ich bin damit einverstanden, dass die Vogel Communications Group GmbH & Co. KG, Max-Planckstr. 7-9, 97082 Würzburg einschließlich aller mit ihr im Sinne der §§ 15 ff. AktG verbundenen Unternehmen (im weiteren: Vogel Communications Group) meine E-Mail-Adresse für die Zusendung von redaktionellen Newslettern nutzt. Auflistungen der jeweils zugehörigen Unternehmen können hier abgerufen werden.
Der Newsletterinhalt erstreckt sich dabei auf Produkte und Dienstleistungen aller zuvor genannten Unternehmen, darunter beispielsweise Fachzeitschriften und Fachbücher, Veranstaltungen und Messen sowie veranstaltungsbezogene Produkte und Dienstleistungen, Print- und Digital-Mediaangebote und Services wie weitere (redaktionelle) Newsletter, Gewinnspiele, Lead-Kampagnen, Marktforschung im Online- und Offline-Bereich, fachspezifische Webportale und E-Learning-Angebote. Wenn auch meine persönliche Telefonnummer erhoben wurde, darf diese für die Unterbreitung von Angeboten der vorgenannten Produkte und Dienstleistungen der vorgenannten Unternehmen und Marktforschung genutzt werden.
Meine Einwilligung umfasst zudem die Verarbeitung meiner E-Mail-Adresse und Telefonnummer für den Datenabgleich zu Marketingzwecken mit ausgewählten Werbepartnern wie z.B. LinkedIN, Google und Meta. Hierfür darf die Vogel Communications Group die genannten Daten gehasht an Werbepartner übermitteln, die diese Daten dann nutzen, um feststellen zu können, ob ich ebenfalls Mitglied auf den besagten Werbepartnerportalen bin. Die Vogel Communications Group nutzt diese Funktion zu Zwecken des Retargeting (Upselling, Crossselling und Kundenbindung), der Generierung von sog. Lookalike Audiences zur Neukundengewinnung und als Ausschlussgrundlage für laufende Werbekampagnen. Weitere Informationen kann ich dem Abschnitt „Datenabgleich zu Marketingzwecken“ in der Datenschutzerklärung entnehmen.
Falls ich im Internet auf Portalen der Vogel Communications Group einschließlich deren mit ihr im Sinne der §§ 15 ff. AktG verbundenen Unternehmen geschützte Inhalte abrufe, muss ich mich mit weiteren Daten für den Zugang zu diesen Inhalten registrieren. Im Gegenzug für diesen gebührenlosen Zugang zu redaktionellen Inhalten dürfen meine Daten im Sinne dieser Einwilligung für die hier genannten Zwecke verwendet werden. Dies gilt nicht für den Datenabgleich zu Marketingzwecken.
Recht auf Widerruf
Mir ist bewusst, dass ich diese Einwilligung jederzeit für die Zukunft widerrufen kann. Durch meinen Widerruf wird die Rechtmäßigkeit der aufgrund meiner Einwilligung bis zum Widerruf erfolgten Verarbeitung nicht berührt. Um meinen Widerruf zu erklären, kann ich als eine Möglichkeit das unter https://contact.vogel.de abrufbare Kontaktformular nutzen. Sofern ich einzelne von mir abonnierte Newsletter nicht mehr erhalten möchte, kann ich darüber hinaus auch den am Ende eines Newsletters eingebundenen Abmeldelink anklicken. Weitere Informationen zu meinem Widerrufsrecht und dessen Ausübung sowie zu den Folgen meines Widerrufs finde ich in der Datenschutzerklärung, Abschnitt Redaktionelle Newsletter.
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: 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: 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: bbv Software Services)
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.
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.