C++ in der Embedded-Entwicklung: Exceptions und Assertions



  • Der vierte Artikel dieser Serie zu C++ in der Embedded-Entwicklung wendet sich dem Thema des Exception handlings und den Möglichkeiten von Assertions zu. Selbst in der umgänglichen C++-Programmierung gibt es dazu einiges zu sagen, die Technik von Exceptions stellt sich aber im Embedded-Umfeld nochmal ganz anders dar.

    Klicken sie hier um den Artikel zu lesen



  • @admin leider wieder ein schlechter Artikel zum Thema C++

    Neben dem grundsätzlich fragwürdigen Ansatz C++ (vor allem für Embedded Entwickler mit Java Code und Analogien vorzustellen (finally Block beim Exception Beispiel), sind einige fachliche Fehler drin. Anbei die offensichtlichsten:

    1. Es wird behaupted errno sei nicht thread-safe. Bei POSIX Systemen mindestens seit 1996 und sonst seit C11 ist errno keine globale Variable, sondern ein macro, dass den Zugriff auf eine thread-local Variable ermöglicht. Damit ist es garantiert, dass errno Thread-safe ist, sofern Threads möglich sind.

    2. Java Code Beispiel "Prinzip des Exception Handlings" Es wird zwar gesagt das es Java code ist, aber warum? Vor allem wird man in C++ sicher nie eine Exception mit "throw new RuntimeException" werfen. Da wären ja Speicherlecks vorprogrammiert. In C++ gilt "Throw by value, catch by const-reference"

    3. Im Abschnitt "Exception handling bei Embedded Geräten" scheint der Autor davon auszugehen, dass Testen manuell erfolgt. Das wäre absolut unzeitgemäss und amateurhaft. Tests werden soweit wie möglich automaisiert und neben Systemtests vor allem feingranulare Unit Tests für die Code-Qualität realisiert.
      Dort wird auch der Mythos hoch gehalten, dass Exception Handling Rechenzeit kostet, was normalerweise gegenüber den Alternativen völliger Quatsch ist. Es gibt ggf. noch ABIs mit schlecht definiertem Exception Handling mit unnötigem Overhead, aber in einem Embedded System spielt ein ABI in der Regel keine Rolle, da ja sowieso meistens alles für das Zielsystem neu kompiliert wird und selten dynamisch gelinkte Libraries vorkommen, die über Betriebssystemversionen kompatible sein müssen.

    4. Es wird angedroht in einem zukünftigen Artikeln zu erklären longjmp() statt throw zu verwenden. Um Gottes Willen, blos nicht in C++. Damit verliert man alle Vorteile von C++ (deterministische Objektdestruktion beim Verlassen eines Scopes) und hat nur fragilen Code. Ich sage nicht, dass setjmp/longjmp in Ausnahmefällen auch in C++ genutzt werde können, aber das sollte auf keinen Fall der Default sein. Bitte halten Sie einen solchen Schwacchsinnsartikel zurück.

    5. Exceptions bei Memory Verletzungen,Nullpointer: Hier wird auf einen Windows-spezifischen Mechanismus eingegangen und gesagt auf Embedded Systemen geht das nicht. Das ist zwar richtig, aber es wird der Eindruck erweckt, dass man ja nicht verhindern kann, dass man in C++ Nullpointer bekommt (am Beispiel Matlab). Mittels Referenzen und smart Pointern (unique_ptr) gibt es meiner Meinung nach keinen Grund in C++ solche Pointerfehler zu machen. Wenn man wirklich einen Pointer hat, kann man diesen prüfen, bevor man ihn dereferenziert. Auf Tools, wie man auch in C++ (und auch in C) typische Fehler mit Pointern zur Laufzeit überwacht, wie valgrind, oder clang address sanitizer geht der Autor leider nicht ein, sondern hofft, dass solche Fehler "vorher gefunden" werden. Wie das ohne Unit Tests gehen soll, frage ich mich.

    6. Assertions
      Auch hier wird angeblich Java code gezeigt, der aber nach #include <cassert> auch perfektes C++ ist. mit -DNDEBUG beim kompilieren, kann man wie in Java auch in C und C++ das assert() Makro deaktivieren. Ich habe das Gefühl der Autor hat weder von C noch von C++ wirklich eine grosse Ahnung. Vor allem fehlt das in C++ vorhandene static_assert mit dem sich zur Compile-Zeit Bedingungen prüfen lassen, von denen der Code ausgeht. Das ist auch bei nicht-generischem Code sinnvoll, wenn dieser auf verschiedenen Plattformen laufen soll (Entwicklungsrechnter und Embedded Core).
      Auf die Alternative von Assertions, explizite Guard-Klauseln, die Vorbedinungen im normalen Code prüfen und bei Verletzung dieser eine Fehlerbehandlung auslösen (Exception oder Error-Return), wird nicht eingegangen.

    7. Fehlersuche beim Integrationstest
      Hier werden Annahmen über charakterliche und soziale Eigenschaften gemacht, die unzeitgemäss, bzw. aus Software Engineering Sicht unprofessionell sind. Wenn die Integration ein Problem ist, hat man vorher amateurhaft gearbeitet.

    8. Assertions in C und C++
      Vom Titel hätte ich erwartet dass es hier nur um C++ geht, deshalb "Warum Macros?" In modernem C++ kann man solche checks auch mittels if constexpr(NDEBUG+0) ausschalten, wenn man NDEBUG nutzen möchte.
      Aber auf die Preconditio-Prüfung (Design by Contract) wird gar nicht eingegangen, sondern eine nebulöse macro-definition vorgestellt, ohne die Infrastruktur dahinter vorzustellen.

    Ein echter Schluss fehlt leider.

    ->

    Ich erkläre die Möglichkeiten der Fehlerbehandlung immer mit 5 Varianten:

    1. Nichts prüfen und nichts melden (Aufrufer ist verantwortlich alle Vorbedingngen einzuhalten) - am effizientesten und unsichersten (typisch für C)
    2. Fehlercode oder speziellen Wert(z.B. POSIX -1) zurück geben, der als korrektes Ergebnis nicht vorkommen kann. (was macht man dann mit dem normalen Ergebnis?), Aufrufer muss diesen prüfen. Falls alle Werte des Wertebereichs vorkommen können, kann man in C++ z.B. std::optional verwenden und im Fehlerfalle ein nullopt zurück liefern (besser als pointer mit nullptr)
    3. Fehler über Seiteneffekt melden (Referenzparamter errorcode, errno, iostream-state (good(),bad(), fail()). Prüfung durch den Aufrufer. Schlecht, wenn man die Variable für den Fehlerstatus immer mitgeben muss.
    4. Default-Ergebnis generieren, d.h. Fehlersituation überspielen mit einem sinnvollen Wert, um weiterlaufen zu können (z.B. in einem catch Block eines Exception Handlers).
    5. Exception werfen und adäquat behandeln, wo man es kann.

    Im Falle von Punkt 2, wenn es keinen Wert im Ergebnistyp gibt, der den den Fehler markieren kann, bietet sich heute std::optional<T> an, um mit einem nullopt ein Fehlerergebnis zu melden, oder das leider (noch) nicht standardisierte expected<T,E> bzw. eine std::variant<Ergebnistyp,Errorcode>. Alternativ kann man auch eine struct mit 2 Feldern retournieren, die sich mit structured Bindings an der Aufrufstelle direkt entpacken lässt.

    Mit einer solchen konzeptuellen Struktur der Fehlerbehandlung kann man viel besser auf mögliche Mechanismen und deren Vor- und Nachteile einer Fehlerbehandlung eingehen und auch, wenn bestimmte Dinge einmal technisch nicht möglich sind (Exceptions und RTTI abgeschaltet für embedded), die Alternativen klarer darstellen.

    Peter Sommerlad.



  • Endlich einmal ein Feedback zu einem Artikel, mit konkreten Hinweisen aus anderer Sicht. Insbesondere die 5 Varianten der Fehlerbehandlung, die der Kritiker nennt, finden meine Zustimmung.

    Aber: Insbesondere Punkt 1, 4 und 5 dürfen keine dauerhafte Programmiererentscheidung sein, sondern sollten ohne Quellenänderung je nach Aufrufumgebung einstellbar sein. Beim Test (von embedded Software am PC, insbesondere Unit-Test) Verletzungen mit Exception melden, damit die Fehler überhaupt erst deutlich erkannt werden. Danach die gleiche Software im Zielsystem unter kritischen Echtzeitbedingungen nach 1. behandeln, oder nach 4. wenn ein Fehler nicht ausgeschlossen werden kann. Genau dafür habe ich meine Makros für TRY-CATCH entworfen.

    Punkt 3 der Kritik, "Mythos Exception Handling kostet Rechenzeit" ist leider kein Mythos sondern gemessener Fakt und auch an sich ganz logisch und folgerichtig, wie ich im Artikel nicht ausführlich genug dargestellt habe. Modernes Exceptionhandling in C++ hat den Zusatzaufwand im Nichtfehlerfall auf fast 0 reduziert. Durchläuft ein Algorithmus ein try-catch ohne Exception, dauert er kaum länger als ohne diesen try-catch-Rahmen. Damit ist Performance gesichert und der obige Mythos tatsächlich ein Mythos, nicht wahr, diesbezüglich. Aber: Dafür dauert das throw um so länger. Und dies geht nicht, wenn eine hochzyklische kritische Echtzeitverarbeitung erfolgt. Damit ist Exceptionhandling mit C++ throw nicht anwendbar in zyklischen Interrupts oder Threads von Regelungen. Aber Exceptionhandling ist wünschenswert. Was macht eine weggelaufene Regelung wegen eines defekten Sensors? Am besten throw, und eine Ersatzhandlung im catch sichert die Funktion. Sind alle Fehlermöglichkeiten einer komplexen Software vorher per Test vorauszusagen? Nein. Man möge nicht wieder unterstellen, ich würde Unit-Test ablehnen. Auch Boeing hat bei 737 MAX sicherlich Unit-Tests durchgeführt.

    Da man im embedded Bereich die Destructoren eigentlich nicht wirklich braucht bzw. bewusst darauf verzichten kann, ist der longjmp die probate Lösung. Denn er hält die Zeitbedingungen ein und funktioniert genauso. Ich möchte dies nochmals deutlich unterstreichen, in Kenntnis der Argumente von Kritikern.

    Als letztes eine Bemerkung zu C++ und Java. In C++20 mit "gcc -c -x c++ -std=c++20", aufgerufen unter Cygwin, gcc (GCC) 10.2.0 kann man immer noch fehlerfrei programmieren:

    ExmplClass* myClass = new ExmplClass();
    (myClass+1)->set(456);

    Die unmittelbar hintereinanderstehenden Statements offenbaren mit dem Hinsehen oder einem Checktool selbstverständlich den Blödsinn dieser Anweisungsfolge. Stehen die Anweisungen jedoch nicht hintereinander und ist der Fehler Folge von Änderungen nicht bis ins letzte durchdacht, und Fehler beim Unittest nicht aufgefallen, dann erzeugen solche Fehler über Seiteneffekte irgendwo anders die Nullpointer oder zerstörte vtbl-Zeiger. Die Ursache sind die in den 1970-ger Jahren gefundenen einfachen Grundlagen in C, die in C++ immer noch möglich sind und verwendet werden. Man hat unter Kenntnis dieser Dinge mit Java in den 1990-gern einen Schnitt gemacht und so etwas gar nicht zugelassen (Pointerarithmetik). Daher ist Java und weitere beliebte Programmiersprachen im PC Bereich sicher und dringend zu empfehlen gegenüber C++. C und besser C++ ist notwendig (wenn man so will ein notwendiges Übel) im embedded Bereich, um direkt und optimal auf die Maschinencodeebene zu kommen. Andere Programmiersprachen haben sich dafür leider nicht etabliert. Es gibt auch für Java für Embedded passende Virtual-Machine-Lösungen, leider nur nicht so bekannt. Diese wären dann für den Teil der Datenverarbeitung (ohne Hardwarezugriffe) die bessere Lösung.

    Hartmut Schorrig


Log in to reply