Exceptions in C++ hui oder pfui?



  • Wir sind uns einig, dass Ausnahmen für Ausnahmefälle da sind, aber was sind denn in der Praxis solche Fälle?

    Ist es wirklich so unerwartet, dass eine Datei nicht geöffnet werden kann?
    Ist es eine Ausnahme, wenn eine TCP-Verbindung geschlossen wird oder verloren geht?
    Sollte man auf einen Mangel an Speicher reagieren oder ist es dann nicht schon zu spät?
    Ist es nicht eher ein Hack, wenn man Fehler im Konstruktor per Ausnahme meldet, obwohl man bei Funktionen mit Rückgabewert diesen benutzt?
    Sollte man System.Exception oder std::exception fangen und damit möglicherweise Programmierfehler verschleiern?

    Ich weiß noch keine sinnvollen Antworten auf diese Fragen.
    Dass man mit assert nicht sparen sollte, ist aber klar. Und wenn man Referenzen verwendet, wo Referenzen angebracht sind, muss man sogar nicht einmal auf Nullzeiger testen.





  • Also, ich sehe das so:

    Die Aussage, dass Exceptions in C++ unschön seien, ist seit Jahren (über den genauen Zeitpunkt kann man sicher streiten) überholt. Es gab eine Zeit, in der Exceptions vergleichsweise langsam waren und insbesondere allein die Möglichkeit, dass eine Exception geschmissen worden sein könnte, einen Overhead bedeutete; das ist inzwischen hinfällig. Es gab auch eine Zeit, in der gängige C++-Programmierpraxis sich nicht allzu sehr von C unterschied, so dass eine Exception im Zweifel dauernd gefangen und wieder neu geworfen wurde, damit zwischendurch aufgeräumt werden konnte. Inzwischen kann das nicht mehr als Argument herhalten, weil (wie dir womöglich aufgefallen ist) einem der Begriff RAII praktisch andauernd um die Ohren gehauen wird. Was ja auch ganz gut ist.

    Gleichwohl sind Exceptions nicht in allen Fällen das Mittel der Wahl. Grundsätzlich bedeutet eine Exception: "ich komme hier so nicht weiter, und ich nehme an, dass du es auch nicht kannst." Es wäre zum Beispiel ziemlich albern, wenn

    int x;
    std::cin >> x;
    

    eine Exception würfe, wenn der Benutzer "abc" eingibt1. Es könnte aber programmabhängig sinnvoll sein,

    program_options parse_config_file(std::istream &cfg_file) {
      ...
      if(!(cfg_file >> node_id)) throw malformed_config_file("node_id muss angegeben und numerisch sein");
      ...
    }
    

    zu schreiben, denn man kann durchaus den Standpunkt vertreten, dass eine kaputte Konfigurationsdatei den Rest des Programms in der Regel am Weitermachen hindern wird.

    Das bedeutet durchaus nicht, dass eine Exception unbedingt zum Programmende führen muss, aber wenn ich eine Exception werfe, habe ich die Vermutung, dass ein größerer Programmteil durch die Ausnahmesituation, auf die ich gestoßen bin, abgebrochen werden müssen wird.

    1 Ich bin mir bewusst, dass man std::istream dazu zwingen kann, in diesem Fall eine Exception zu werfen. Ich stehe auf dem Standpunkt, dass man das nur tun sollte, wenn ein Lesefehler hinreichend fatal wäre.



  • Ist für euch eine Exception == Programmende? Oder warum soll man die nur in main fangen?



  • seldon schrieb:

    eine Exception würfe, wenn der Benutzer "abc" eingibt1. Es könnte aber programmabhängig sinnvoll sein,

    program_options parse_config_file(std::istream &cfg_file) {
      ...
      if(!(cfg_file >> node_id)) throw malformed_config_file("node_id muss angegeben und numerisch sein");
      ...
    }
    

    zu schreiben, denn man kann durchaus den Standpunkt vertreten, dass eine kaputte Konfigurationsdatei den Rest des Programms in der Regel am Weitermachen hindern wird.

    Das bedeutet durchaus nicht, dass eine Exception unbedingt zum Programmende führen muss, aber wenn ich eine Exception werfe, habe ich die Vermutung, dass ein größerer Programmteil durch die Ausnahmesituation, auf die ich gestoßen bin, abgebrochen werden müssen wird.

    Wenn man eine fehlerhafte Konfigdatei oder Beispiel aus der Praxis, ein Savestand nicht geladen werden kann, dann kann man das aber auch ganz gut als Teil des Programms behandeln.

    Also nen ganz normalen

    if() ...else
    

    Block nehmen und den Fehler dann im else Block als Teil des normalen Programms behandeln.

    Wozu brauche ich da also ne Exception?
    IMO überflüssig.



  • Nun, womöglich bist du gerade zehn Funktionen tief in einem Recdesc-Parser. Wenn man mal Funktionen hat, die andere Funktionen aufrufen (ich habe mir sagen lassen, dass das gelegentlich vorkommt), ist die if-else-Fehlerbehandlungsmethode für Fälle, in denen ein ganzer Funktionskomplex abgebrochen werden soll, doch ausgesprochen unhandlich.



  • x8000 schrieb:

    Ist für euch eine Exception == Programmende? Oder warum soll man die nur in main fangen?

    Niemand hat gesagt dass man grundsätzlich Exceptions nur in der main fangen soll. volkard möchte seine AssertionException nur in der main fangen. Vermutlich weil die nur dann ausgelöst wird, wenn ein Programmierfehler/Logikfehler enthalten ist (Assertion eben), und dann das Programm an sich eh zum Teufel ist und nicht verlässlich weiter verwendet werden kann.
    Grundsätzlich sollten Exceptions imo auf Subsystem-Ebene gefangen werden, d.h. wenn ein Subsystem einen so schwerwiegenden Fehler hat, dass es nicht sinnvoll weiterarbeiten kann, wirft es die Exception und fertig. Der Aufrufer sollte dann diese Exception fangen und entweder ordentlich behandeln oder aber eine zu seinem Subsystem passende Exception weiterwerfen.
    Beispiel Parse-Fehler in der save-Datei:
    Wenn der Parser komplett aussteigt, weil die Datei unkenntlich ist, schmeißt er eine SaveFileParseException. Das Modul, das den Parser aufgerufen hat, kann jetzt entweder eine Warnmeldung ausgeben á la "Save-Datei kaputt, starte leeres Dokument", wenn das angemessen ist, oder es wirft eine eigene Exception, dass es eine nötige Datei nicht laden konnte. Die Info, was beim Parsen schiefgelaufen ist, die vermutlich in der SaveFileParseException zu finden ist, interessiert an höherer Stelle vielleicht garnicht mehr.
    Auf jeden Fall bedeutet das "aus dem Modul rauswerfen", dass mehrere Funktionen durchflogen werden, bevor die exception gefangen wird. Try/Catch brauchts es bei so einer Behandlung nur an Modulgrenzen. Ein grundsätzlich exceptionsicherer Code muss weder komplexer noch imperformanter als "normaler" Code sein, das bedeutet unter anderem, dass vielleicht zwei oder drei von ~100 Funktionen im Modul eine Exception werfen können, die anderen Funktionen aber nicht mit Fehlerbehandlungscode zugemüllt werden müssen. Das ist ein Gegensatz zum alten C-Errorcode Gedöns, wo erstens die Errorcodes in so ziemlich jede Schnittstelle wandern müssen, um rausgereicht zu werden, und zweitens in jeder Funktion die Errorcodes der aufgerufenen Funktionen aufgesammelt, ausgewertet und weitergereicht werden müssen.
    Und selbst wenn aus irgendwelchen Gründen mein Modul einen Errorcode zurückliefern soll, dann tun das die 5-10 Funktionen, die zur äußeren Schnittstelle des Moduls gehören. Intern fliegen weiter lustig Exceptions, die an den Außengrenzen gefangen und als Errorcodes zurückgegeben werden.
    Ein weiterer Vorteil von Exceptions gegenüber Errorcodes ist, dass Klienten meines Moduls sie fangen/behandeln müssen und nicht mit einem korrumpierten Programm sorglos weiterarbeiten. Ich habe schon viel zu oft C-Style ErrorCodes gesehen, die einfach ignoriert wurden. Häufig landen dann Anfragen bei mir oder Kollegen, warum "das denn nicht tut", und wir müssen dann mühsam debuggen.
    @ volkard: __asm int 3 sagt mir jetzt nichts. Du schreibst, dass der Debugger an der Stelle hält. Um welchen Debugger gehts? MSVC hat eine Funktionalität, dass man beim Wurf von vorher auszuwählenden Exceptions ein break im Debugging bekommt.



  • vorschlag schrieb:

    Verlinkt doch mal ein Open Source C++ Projekt, dass eurer Meinung nach Exceptions effektiv verwendet.

    http://paludis.pioto.org/



  • TyRoXx schrieb:

    Und wenn man Referenzen verwendet, wo Referenzen angebracht sind, muss man sogar nicht einmal auf Nullzeiger testen.

    Fail. Referenzen sollte man anders benutzen, als Du postulierst und auch dann muß man nicht auf Nullzeiger testen. Aber das ist ein anderes Thema.



  • pumuckl schrieb:

    @ volkard: __asm int 3 sagt mir jetzt nichts. Du schreibst, dass der Debugger an der Stelle hält. Um welchen Debugger gehts?

    Ich denke, alle tuns auf Intel.
    http://en.wikipedia.org/wiki/INT_(x86_instruction)#INT_3
    Breakpoints anders zu implementieren, wäre irgendwie, gegen die Hardware zu arbeiten.

    Das da war für den MSVC: http://www.c-plusplus.net/forum/23425-13

    Und heute unter Linux getestet mit gcc und gdb:

    #include <iostream>
    using namespace std;
    
    int main() {
        for(int i=0;i<10000;++i){
            cout<<i<<endl;
            if(i==9876) asm("int $0x03");
        }
    }
    

    Klappt auch wie gewünscht.

    pumuckl schrieb:

    MSVC hat eine Funktionalität, dass man beim Wurf von vorher auszuwählenden Exceptions ein break im Debugging bekommt.

    Ich finde es im Code hübscher.



  • Ich sehe jetzt den Zusammenhang von deinem beispiel mit Exceptions nicht ganz, hab da wohl ein brett vorm Kopf.

    volkard schrieb:

    pumuckl schrieb:

    MSVC hat eine Funktionalität, dass man beim Wurf von vorher auszuwählenden Exceptions ein break im Debugging bekommt.

    Ich finde es im Code hübscher.

    Ich bin kein Freund von Dingen im Code, die nichts mit der Programmfunktionalität zu tun haben. Exceptions, um unvorhergesehene Probleme anzuzeigen, ja. Für Programmierfehler oder Logikfehler definitiv nicht. Debughilfen haben im normalen Code auch nichts verloren. asserts mag ich auch nicht unbedingt. Schließlich gibts unit-Tests, die ein Interface breit genug testen sollten.



  • pumuckl schrieb:

    Ich bin kein Freund von Dingen im Code, die nichts mit der Programmfunktionalität zu tun haben.

    Nichtmal #pragma(lib, ?
    Ich freue mich, wenn man einfach nur den Quellcode in ein Projekt kopieren muß und schon funktioniert es. Und umgekehrt würde ich manchmal gerne Leute erschlagen, die (selbstverständlich ohne Doku, denn nur der Code wird dokumentiert) verlangen, daß man zwei bis drei Stunden lang die Einstellungen sucht, die zu machen sind, um den vorliegenden Quellcode lauffähig zu kriegen.

    pumuckl schrieb:

    Exceptions, um unvorhergesehene Probleme anzuzeigen, ja. Für Programmierfehler oder Logikfehler definitiv nicht.

    Meine AssertExceptions sind keine Debughilfe, sondern Aufräumhilfe im assert-Fall. Also ich werde von assert in den Debugger geworfen. Dort untersuche ich die lokalen Variablen und stelle eine These auf, welchen Fehler ich gemacht hatte. Und dann stoppe ich das Programm, aber nicht mit Strg+F5 (Debugger abbrechen), sondern mit F5 (Programm weiterlaufen lassen), erst jetzt wird die Exception geworfen und eventueller Aufräumcode wird noch ausgeführt.

    pumuckl schrieb:

    Debughilfen haben im normalen Code auch nichts verloren.

    Doch, klar!

    pumuckl schrieb:

    asserts mag ich auch nicht unbedingt. Schließlich gibts unit-Tests, die ein Interface breit genug testen sollten.

    Hä??



  • volkard schrieb:

    TyRoXx schrieb:

    Und wenn man Referenzen verwendet, wo Referenzen angebracht sind, muss man sogar nicht einmal auf Nullzeiger testen.

    Fail. Referenzen sollte man anders benutzen, als Du postulierst und auch dann muß man nicht auf Nullzeiger testen. Aber das ist ein anderes Thema.

    Wie denn?



  • volkard schrieb:

    pumuckl schrieb:

    Ich bin kein Freund von Dingen im Code, die nichts mit der Programmfunktionalität zu tun haben.

    Nichtmal #pragma(lib, ?

    Also ich find #pragma comment(lib, "bla") ist eine wirkliche Unart. Sowas hat im Source-Code nun wirklich nichts zu suchen, das ist Aufgabe des Buildsystems.



  • +1 für dot.

    @Topic: Eindeutig pfui. Ich bin die ganze try-catch-Scheiße leid.



  • volkard schrieb:

    Was aber auch gilt: In C++ werfen wir tunlichst keine Exceptions bei Programmierfehlern (Stack Underflow, Arraygrenzenüberschreitung und so) und auch nicht bei ganz normalen Sachen wie einem Dateiende.

    Da ich mich gerade mit beschäftige - wie wird es richtig gemacht?
    Eventuell gibt es noch bessere Möglichkeiten

    template <typename T>
    class Threadsave_stack
    {
     private:
      std::stack<T> data;
      mutable std::mutex m;
     public:
      std::unique_ptr<T> pop() 
    };
    

    Exception-Variante:

    template <typename T>
    std::unique_ptr<T> Threadsave_stack<T>::pop()
    {
     std::lock_guard<std::mutex> lock(m);
     if(data.empty()) throw Empty_stack("Stack is empty");
     std::unique_ptr<T> res(new T(std::move(data.top())));
     data.pop();
     return res;
    }
    

    nullptr-Variante.

    template <typename T>
    std::unique_ptr<T> Threadsave_stack<T>::pop()
    { 
     std::lock_guard<std::mutex> lock(m); 
     if(!data.empty())  
     { 
      std::unique_ptr<T> res(new T(std::move(data.top()))); 
      data.pop(); 
      return res; 
     } 
     return std::unique_ptr<T>(nullptr); 
    }
    


  • volkard schrieb:

    pumuckl schrieb:

    Ich bin kein Freund von Dingen im Code, die nichts mit der Programmfunktionalität zu tun haben.

    Nichtmal #pragma(lib, ?

    Siehe dot.

    Ich freue mich, wenn man einfach nur den Quellcode in ein Projekt kopieren muß und schon funktioniert es. Und umgekehrt würde ich manchmal gerne Leute erschlagen, die (selbstverständlich ohne Doku, denn nur der Code wird dokumentiert) verlangen, daß man zwei bis drei Stunden lang die Einstellungen sucht, die zu machen sind, um den vorliegenden Quellcode lauffähig zu kriegen.

    Projekte mit so vielen Enstellungen sind mir bisher noch nicht untergekommen. Makefiles bzw. MSVC-Solutions haben mir bisher immer gereicht.

    pumuckl schrieb:

    Debughilfen haben im normalen Code auch nichts verloren.

    Doch, klar!

    Wenn ich sie zum Debuggen grade brauche, baue ich das entsprechende Modul mit ein paar Zeilen Debughilfe. Danach kommen sie wieder raus. Debugrelikte von anno dunnemals, die nie wieder benötigt werden, sobald der Code stabil ist, sorgen nur für Verwirrung und verursachen unnötigen Maintenance-Aufwand. Ich weiß, Code ändert sich und man muss wieder debuggen, aber dann passen meistens die alten Debughilfen eh nicht mehr zum neuen Code.

    pumuckl schrieb:

    asserts mag ich auch nicht unbedingt. Schließlich gibts unit-Tests, die ein Interface breit genug testen sollten.

    Hä??

    Ich nutze keine Assertions. Wozu auch? Wenn ein assert sicherstellen würde, dass die Eingaben in eine Schnittstelle bestimmte Kriterien erfüllen, dann stelle ich in meinen Unit-Tests sicher, dass die aufrufenden Funktionen die Schnittstelle immer mit ordentlichen Eingaben bestücken. Wenn ein assert sicherstellen würde, dass ich von irgendwo ordentliche Rückgabewerte bekomme, teste ich das genauso über Unit-Tests, Regressionstests usw.

    314159265358979 schrieb:

    @Topic: Eindeutig pfui. Ich bin die ganze try-catch-Scheiße leid.

    pumuckl schrieb:

    Das Wichtigste zum Umgang mit Exceptions ist allerdings die Aussage, dass exceptionsicherer Code nichts mit andauerndem try/catch zu tun hat. Leider verstehen das viele Leute immernoch nicht.



  • einwurf schrieb:

    Da ich mich gerade mit beschäftige - wie wird es richtig gemacht?
    Eventuell gibt es noch bessere Möglichkeiten

    Exception-Variante/nullptr-Variante.

    Da bei Multithreading immer ein anderer Thread dir den Stack vor der Nase weg leeren kann, würde ich die nullptr-Variante bevorzugen. Alternativ eine blockierende Variante, die bei leerem Stack wartet, bis wieder was zu poppen ist.



  • einwurf schrieb:

    Da ich mich gerade mit beschäftige - wie wird es richtig gemacht?
    Eventuell gibt es noch bessere Möglichkeiten

    Hast du schon das in Betracht gezogen?

    template <class T>
    bool tryPop(T &value);
    

    Die Variante mit dem unique_ptr blockiert mit dem unnötigen new den Stack relativ lange. Wenn es unbedingt unique_ptr sein muss, könnte man das Element erst bewegen, den Mutex wieder freigeben und dann new bemühen.



  • dot schrieb:

    Also ich find #pragma comment(lib, "bla") ist eine wirkliche Unart. Sowas hat im Source-Code nun wirklich nichts zu suchen, das ist Aufgabe des Buildsystems.

    Wenn Ihr dann wenigsten //#pragma comment(lib, "bla") in die Datei schreiben würdet! Dann konnte man sich das zusammenklauben und im Build-System eintragen. Aber das ist nicht der Fall.


Anmelden zum Antworten