Spiel schreiben um C++ zu erlernen - Exceptions



  • aus leidvoller Erfahrung kann ich dir sagen:
    verwende Exceptions nur dann, wenn es sich um das handelt, was der Name schon andeutet. Nämlich Ausnahmen. Und die sollten nicht allzu oft auftreten.
    Jedenfalls sollten diese niemals als eine Art return Wert verstanden werden, um normale Betriebszustände auszudrücken.

    Gerade bei den von dir genannten Beispielen würde ich mir etwas genauer überlegen, ob dort wirklich Exceptions nötig sind.
    Ich greife jetzt nur mal eines deiner Beispiele auf:

    Der Spielername ist ungültig

    Warum sollte das eine Ausnahme sein?
    Die SetName Funktion braucht nur einen bool zurückgeben, um zu zeigen, ob der Name angenommen wurde:
    bool CPlayer::SetName(const std::string& aName);

    Und wenn jetzt das Argument kommt, von wegen den bool return Wert kann man ja ignorieren.
    Stimmt. Aber Exceptions kann man genauso schön ignorieren:
    try
    {
    foo();
    }
    catch(...)
    {
    // ... interessiert mich sowieso nicht diese Exception ...
    }

    Und bedenke bitte auch, dass Exceptions nicht ganz "billig" sind. Gut, auf dem PC merkt man es nicht so, aber jeder der schon mal mit embedded systems gearbeitet hat, wird Exceptions gegenüber etwas kritischer eingestellt sein.
    Hab einmal durch das Tauschen einer Exception gegen einen return Wert die Laufzeit auf 1/3 der ursprünglichen verkürzen können.

    Ansonsten fang einfach mal an zu programmieren, und lies nebenbei ein gutes C++ Buch (z.B. "Der C++ Programmierer"). Dann ergeben sich Fragen und Antworten gleichzeitig.
    Und ich denke, deine Fragen werden dann auch nicht mehr so theoretisch sein wie jetzt.
    Viel Spaß dabei!



  • Wenn man übrigens die Variant-Methode ordentlich haben möchte, dann will man Boost.Optional bzw. im neuen C++14-Standard std::optional.
    Funktioniert natürlich nur, wenn man tatsächlich entweder Fehler oder den entsprechenden Rückgabewert zurückgeben will.



  • Vielen Dank für die hilfreichen Antworten 🙂



  • Also vielleicht geht's nur mir so, aber ich habe jetzt aus dem ganzen Thread (und allen anderen, die in diese Richtung gehen) nur mitgenommen, wie man's nicht machen sollte, was ob der genannten komplizierten pseudo-geekigen Konstrukte fast schon offensichtlich war.
    Wie macht man's nun? Ja, hrmmm, das ist so eine Sache, da muss man halt im Spezialfall mal nachschauen, es gibt da viele Möglichkeiten, hrmmm, kann man ohne den genauen Spezialfall eigentlich gar nicht sagen, hrmmmm, naja, aber so nicht!
    Ein bool-Rückgabewert ist sowas wie ein Spiel für den Affen: Wenn er zweimal grunzt und sich unter der Achsel kratzt fällt eine Banane aus der Klappe, ansonsten nicht. Nach 1000+ Versuchen kann er die versteckte Regel dann durch einen KI-Lernalgorithmus bestimmen.
    Dass man mit catch(...) als fauler Programmierer alle Exceptions schlucken kann, wird wohl kaum als Gegenargument dienen dürfen, schließlich kann man auch sämtliche Errorcodes oder äquivalentes Gedöns einfach ignorieren.



  • ztrzrt schrieb:

    aus leidvoller Erfahrung kann ich dir sagen:
    verwende Exceptions nur dann, wenn es sich um das handelt, was der Name schon andeutet. Nämlich Ausnahmen.

    Kannst Du näher darauf eingehen, auf welche Erfahrungen Du dich beziehst? Da denkt meist jeder an irgendetwas anderes. Einige kommen gar nicht erst auf die Ideen, mit denen andere auf Grund gelaufen sind. Daher fehlt mir hier so ein bisschen der Kontext, um das einordnen zu können.

    ztrzrt schrieb:

    Der Spielername ist ungültig

    Warum sollte das eine Ausnahme sein?
    Die SetName Funktion braucht nur einen bool zurückgeben, um zu zeigen, ob der Name angenommen wurde:
    bool CPlayer::SetName(const std::string& aName);

    Was ist jetzt mit ungültigen Zeichen? Oder verletzung der Eindeutigkeit? Oder einem zu kurzen Namen? Viel zu differenzieren gibt es ja so nicht.



  • in der Software gibt es sehr unterschiedliche Arten von Fehlern, z.B.

    - fataler Programmierfehler (Array Index out of range usw.)
    - numerischer Fehler
    - Ressourcenfehler
    - fehlerhafter Userinput
    usw.

    wie darauf zu reagieren ist, hängt von der Aufgabenstellung ab, z.B.

    Aufgabe A: Wenn das Configfile nicht geladen werden kann, gibt das Programm eine Fehlermeldung aus und beendet sich.
    Aufgabe B: Wenn das Configfile nicht geladen werden kann, gibt das Programm eine Fehlermeldung aus und arbeitet mit Defaultwerten weiter.

    in sicherheitskritischen Bereichen werden oftmals auch fatale Programmierfehler auf bestimmte Art behandelt. wenn z.B. ein Arrayindex zu groß ist, kann man z.B. das erste Element zurückgeben (Aufgabe: Programm muß auch bei fatalem Programmierfehler weiterlaufen)

    ob man C++ Exceptions verwenden soll, wird in der Fachwelt kontrovers diskutiert, z.B. hier:

    http://www.wikiservice.at/dse/wiki.cgi?KategorieException

    einige Firmen untersagen C++ Exceptions, z.B.

    http://google-styleguide.googlecode.com/svn/trunk/cppguide.xml

    Cons:

    When you add a throw statement to an existing function, you must examine all of its transitive callers. Either they must make at least the basic exception safety guarantee, or they must never catch the exception and be happy with the program terminating as a result. For instance, if f() calls g() calls h(), and h throws an exception that f catches, g has to be careful or it may not clean up properly.
    More generally, exceptions make the control flow of programs difficult to evaluate by looking at code: functions may return in places you don't expect. This causes maintainability and debugging difficulties. You can minimize this cost via some rules on how and where exceptions can be used, but at the cost of more that a developer needs to know and understand.
    Exception safety requires both RAII and different coding practices. Lots of supporting machinery is needed to make writing correct exception-safe code easy. Further, to avoid requiring readers to understand the entire call graph, exception-safe code must isolate logic that writes to persistent state into a "commit" phase. This will have both benefits and costs (perhaps where you're forced to obfuscate code to isolate the commit). Allowing exceptions would force us to always pay those costs even when they're not worth it.
    Turning on exceptions adds data to each binary produced, increasing compile time (probably slightly) and possibly increasing address space pressure.
    The availability of exceptions may encourage developers to throw them when they are not appropriate or recover from them when it's not safe to do so. For example, invalid user input should not cause exceptions to be thrown. We would need to make the style guide even longer to document these restrictions!



  • fataler Programmierfehler (Array Index out of range usw.)

    Lol? Das nennst du fatal?



  • Hallo,
    zu dieser Thematik habe ich auch eine Frage, weshalb ich sie hier gleich mal stelle.

    Ich habe eine Methode, die etwas berechnen soll und als Ergebnis ein Objekt der Klasse A zurück gibt.
    Allerdings kann diese Methode aus unterschiedlichen Gründen scheitern. Das heißt, ich will als Antwort entweder die Fehlerart oder wenn es erfolgreich war, das Objekt der Klasse A.
    Da ich mehr mit C gemacht habe würde ich es so umsetzen:

    typedef enum
    {
        SUCCESS,
        ERROR_ONE,
        ERROR_TWO
    } CALCULATE_STATUS;
    
    CALCULATE_STATUS calculate(A &result);
    

    Als Rückgabewert habe ich also entweder den Fehler, den ich anhand des Enum-Types erkennen kann oder SUCCESS. Wenn es SUCCESS ist weiß ich, dass ich im Übergabeparameter meine Antwort finde.

    Hier habe ich rausgelesen, dass man diese vorgehen bei C++ nicht macht. Des weiteren habe ich das Gefühl, dass Boost.Optional genau das macht. Ich will allerdings auf Boost verzichten.

    Welches Vorgehen ist hier eine normale, gute Art in C++, um mein oben beschriebenes Problem umzusetzen?



  • Sone schrieb:

    fataler Programmierfehler (Array Index out of range usw.)

    Lol? Das nennst du fatal?

    Ach, Sone, halt manchmal einfach nur die Klappe.



  • Hambrana schrieb:

    Hallo,
    zu dieser Thematik habe ich auch eine Frage, weshalb ich sie hier gleich mal stelle.

    Ich habe eine Methode, die etwas berechnen soll und als Ergebnis ein Objekt der Klasse A zurück gibt.
    Allerdings kann diese Methode aus unterschiedlichen Gründen scheitern. Das heißt, ich will als Antwort entweder die Fehlerart oder wenn es erfolgreich war, das Objekt der Klasse A.
    Da ich mehr mit C gemacht habe würde ich es so umsetzen:

    typedef enum
    {
        SUCCESS,
        ERROR_ONE,
        ERROR_TWO
    } CALCULATE_STATUS;
    
    CALCULATE_STATUS calculate(A &result);
    

    Als Rückgabewert habe ich also entweder den Fehler, den ich anhand des Enum-Types erkennen kann oder SUCCESS. Wenn es SUCCESS ist weiß ich, dass ich im Übergabeparameter meine Antwort finde.

    Hier habe ich rausgelesen, dass man diese vorgehen bei C++ nicht macht. Des weiteren habe ich das Gefühl, dass Boost.Optional genau das macht. Ich will allerdings auf Boost verzichten.

    Welches Vorgehen ist hier eine normale, gute Art in C++, um mein oben beschriebenes Problem umzusetzen?

    viele asserts, wenige Exceptions, Rest unter ferner liefen.



  • volkard schrieb:

    viele asserts, wenige Exceptions, Rest unter ferner liefen.

    Hm, die Antwort verstehe ich nicht ganz


  • Mod

    Hambrana schrieb:

    volkard schrieb:

    viele asserts, wenige Exceptions, Rest unter ferner liefen.

    Hm, die Antwort verstehe ich nicht ganz

    Du sollst viele Assertions benutzen, die während der Entwicklung Logikfehler im Programm selber aufdecken (z.B. falsche Benutzung von Funktionen). Für richtige Laufzeitausnahmen, die auch in einem ansonsten korrekten Programm auftreten können, da sie sich der Kontrolle des Programmierers entziehen, nimmst du Exceptions. Dies sollten in der Regel nicht mehr all zu viele Möglichkeiten sein. Ein typisches Beispiel wäre das Nicht-Vorhandensein einer externen Ressource, ohne die das Programm nicht fortgesetzt werden kann. Was dann noch übrig bleibt sind eher hypothetische Fehler. Der Hund frisst mitten im Betrieb die Hausaufgaben Grafikkarte. Sonnenwinde, die Bitfehler im RAM verursachen. Dagegen braucht man nicht defensiv zu programmieren. Let it crash!

    Die vielen Assertions haben den Vorteil, dass man sich ob der korrekten Funktion des Programms relativ sicher sein kann und das sollte doch immer das Hauptziel bei der Entwicklung sein. In der Releaseversion sind diese Tests dann aber nicht mehr vorhanden, man hat also keinerlei Nachteile. Exceptions sollten "richtigen" Ausnahmen in einem ansonsten korrekten Programm vorbehalten sein, nicht zum Aufspüren von Programmierfehlern.



  • SeppJ schrieb:

    Die vielen Assertions haben den Vorteil, dass man sich ob der korrekten Funktion des Programms relativ sicher sein kann und das sollte doch immer das Hauptziel bei der Entwicklung sein. In der Releaseversion sind diese Tests dann aber nicht mehr vorhanden, man hat also keinerlei Nachteile. Exceptions sollten "richtigen" Ausnahmen in einem ansonsten korrekten Programm vorbehalten sein, nicht zum Aufspüren von Programmierfehlern.

    Klingt zwar schön in der Theorie, funktioniert aber in der Praxis nicht. Bei komplexen Programmen wird es immer irgendwo möglich sein, eine Assertion zu verletzen.

    Das führt dann dazu, dass komplexere Programme (wie GCC) selbst im Release mit Assertions gebaut werden. Die Technik ist nicht schlecht, aber auf Assertions im Release zu verzichten ist kaum möglich.


  • Mod

    Das macht es aber auch nicht falsch, viel mit Assertions zu arbeiten. Eine Assertion schlägt auf einen Programmierfehler an. Da ist ein try..catch nicht das richtige Mittel dafür, auch wenn uns der Standard so schöne Exceptions wie logic_error, invalid_argument & Co. anbietet (benutzt die irgendjemand?).

    Und für mittelgroße Programme funktioniert die Technik mit dem Ausschalten der Assertions im Release auch praktisch sehr gut.



  • SeppJ schrieb:

    Das macht es aber auch nicht falsch, viel mit Assertions zu arbeiten. Eine Assertion schlägt auf einen Programmierfehler an. Da ist ein try..catch nicht das richtige Mittel dafür, auch wenn uns der Standard so schöne Exceptions wie logic_error, invalid_argument & Co. anbietet (benutzt die irgendjemand?).

    Nein, Assertions sind genau der richtige Weg, weil es sich um echte Bugs handelt. Es ist nur kritisch, sie ganz auszuschalten.

    GTK+ geht einen anderen Weg: Fehlgeschlagene Assertions werden nach stderr ausgegeben, haben aber ansonsten keine Konsequenzen.



  • SeppJ schrieb:

    Du sollst viele Assertions benutzen, die während der Entwicklung Logikfehler im Programm selber aufdecken (z.B. falsche Benutzung von Funktionen). Für richtige Laufzeitausnahmen, die auch in einem ansonsten korrekten Programm auftreten können, da sie sich der Kontrolle des Programmierers entziehen, nimmst du Exceptions.

    Danke für die Erklärung.

    Nun weiß ich glaub ich, wieso ich die Antwort von volkard nicht verstanden habe.

    Bei meinen "Fehlern" ging es mir nicht um Programmierfehler oder schwere Laufzeitfehler.

    Ich versuche das Beispiel genauer zu machen:
    In einer Methode wird ein älteres Kamerabild mit einem neuen verglichen. Die Methode soll erkennen, was sich verändert hat. Dafür gibt es ganz bestimmte, vordefinierte Veränderungen die möglich sind und erkannt werden können.
    Diese Veränderungen sind über ein großes Projekt hinweg in einem Enum gespeichert, was von allen verwendet wird und eigentlich nicht geändert werden soll, Beispiel:

    typedef enum
    {
        LEFT,
        RIGHT,
        UP,
        DOWN
    } CHANGES;
    

    Aber es können auch zwei Ausnahmen vorkommen: "Keine Änderung" oder "Es ist so viel geändert worden, dass keine der vordefinierten Veränderungen passt.
    Da das Enum CHANGES nicht geändert werden soll und es außerdem Logiken sind, die nur diese Methode betrifft, bin ich am überlegen, wie ich den Rückgabewert nun richtig mache.

    So könnte im C Style die oben genannte Möglichkeit verwendet werden:

    typedef enum
    {
        SUCCESS,
        NOTHING,
        EXACT_CHANGE_NOT_FOUND
    } CHANGE_STATUS;
    
    CHANGE_STATUS foundChange(const Img old, const Img new, CHANGES &result);
    

    Da aber oben gesagt wurde, dass "jede Methode gibt OK oder ein Fehlerobjekt zurück und der ehemalige Rückgabewert ist eine Referenz als erstes Argument." "NIE NIE NIE" verwendet werden soll, wollte ich nun wissen, was die angemessene C++-Art dafür ist.

    Hier eine Exception werfen die andere mit einem try-catch-Block abfangen sehe ich als falsch an. Es ist ja keine "Exception" im herkömmlichen Sinne.



  • Hambrana schrieb:

    SeppJ schrieb:

    Du sollst viele Assertions benutzen, die während der Entwicklung Logikfehler im Programm selber aufdecken (z.B. falsche Benutzung von Funktionen). Für richtige Laufzeitausnahmen, die auch in einem ansonsten korrekten Programm auftreten können, da sie sich der Kontrolle des Programmierers entziehen, nimmst du Exceptions.

    Danke für die Erklärung.

    Nun weiß ich glaub ich, wieso ich die Antwort von volkard nicht verstanden habe.

    Bei meinen "Fehlern" ging es mir nicht um Programmierfehler oder schwere Laufzeitfehler.

    Ich versuche das Beispiel genauer zu machen:
    In einer Methode wird ein älteres Kamerabild mit einem neuen verglichen. Die Methode soll erkennen, was sich verändert hat. Dafür gibt es ganz bestimmte, vordefinierte Veränderungen die möglich sind und erkannt werden können.
    Diese Veränderungen sind über ein großes Projekt hinweg in einem Enum gespeichert, was von allen verwendet wird und eigentlich nicht geändert werden soll, Beispiel:

    typedef enum
    {
        LEFT,
        RIGHT,
        UP,
        DOWN
    } CHANGES;
    

    Aber es können auch zwei Ausnahmen vorkommen: "Keine Änderung" oder "Es ist so viel geändert worden, dass keine der vordefinierten Veränderungen passt.
    Da das Enum CHANGES nicht geändert werden soll und es außerdem Logiken sind, die nur diese Methode betrifft, bin ich am überlegen, wie ich den Rückgabewert nun richtig mache.

    So könnte im C Style die oben genannte Möglichkeit verwendet werden:

    typedef enum
    {
        SUCCESS,
        NOTHING,
        EXACT_CHANGE_NOT_FOUND
    } CHANGE_STATUS;
     
    CHANGE_STATUS foundChange(const Img old, const Img new, CHANGES &result);
    

    Da aber oben gesagt wurde, dass "jede Methode gibt OK oder ein Fehlerobjekt zurück und der ehemalige Rückgabewert ist eine Referenz als erstes Argument." "NIE NIE NIE" verwendet werden soll, wollte ich nun wissen, was die angemessene C++-Art dafür ist.

    Hier eine Exception werfen die andere mit einem try-catch-Block abfangen sehe ich als falsch an. Es ist ja keine "Exception" im herkömmlichen Sinne.

    Mooomentchen mal!
    Ein schwerer Designfehler, daß der enum CHANGES bereits zu schwach ist bzw daß der enum CHANGES überhaupt benutzt wird.

    **Es ist immer ein verdammt schlechter Ratgeber, nach gutem Stil zu suchen, indem man schaut, wie man ein total verkorkstes Projekt am besten rettet. Schlechter Anfang sorgt zwingend zu einem schlechten Ende.
    **

    In dem Fall fühlt sich

    std::pair<SUCCESS,CHANGES> foundChange(Img const& old,Img const& new);
    

    für mich auf den ersten Blick als geringstes Übel an.

    Vielleicht besser, einen eigenen enum zu bauen, der CHANGES vollständig enthält.



  • Danke für deine Antwort.

    volkard schrieb:

    Ein schwerer Designfehler, daß der enum CHANGES bereits zu schwach ist bzw daß der enum CHANGES überhaupt benutzt wird.

    Was wäre denn eine bessere Lösung?
    Also das zu "schwach" verstehe ich noch in Hinsicht, dass es "NO_CHANGES" enthalten könnte. Allerdings ist "EXACT_CHANGE_NOT_FOUND" so speziell für die Methode und wird von den restlichen Komponenten nicht benötigt, die CHANGES benutzen, dass es meiner Ansicht nach darin nichts verloren hat.
    Was ist an der Angabe mit Enum so schlecht? Wenn du schreibst, dass es ein Designfehler ist CHANGES überhaupt zu verwenden, dann denkst du sicher bereits an eine andere Umsetzungsart. Was wäre da die schönere Alternative? - will ja für die Zukunft lernen.



  • Hambrana schrieb:

    Danke für deine Antwort.

    volkard schrieb:

    Ein schwerer Designfehler, daß der enum CHANGES bereits zu schwach ist bzw daß der enum CHANGES überhaupt benutzt wird.

    Was wäre denn eine bessere Lösung?
    Also das zu "schwach" verstehe ich noch in Hinsicht, dass es "NO_CHANGES" enthalten könnte. Allerdings ist "EXACT_CHANGE_NOT_FOUND" so speziell für die Methode und wird von den restlichen Komponenten nicht benötigt, die CHANGES benutzen, dass es meiner Ansicht nach darin nichts verloren hat.
    Was ist an der Angabe mit Enum so schlecht? Wenn du schreibst, dass es ein Designfehler ist CHANGES überhaupt zu verwenden, dann denkst du sicher bereits an eine andere Umsetzungsart. Was wäre da die schönere Alternative? - will ja für die Zukunft lernen.

    Meinte "CHANGES überhaupt bei dieser konkreten Funktion zu verwenden"



  • Ich weiß, das gehört hier in diesen fremden Thread nicht mehr. Aber ich will nur einmal noch kurz nachfragen, da ich gerade einiges lernen kann:
    Wenn du CHANGES nicht verwenden würdest aber dies die entscheidende Wert ist, weshalb man die Methode aufruft, wie würdest du dies designmäßig richtig machen?
    Wäre dein Vorschlag, "ein eigenes Enum das CHANGES vollständig enthält", der beste Weg? Oder hast du an einen grundlegend anderen Aufbau der Methode gedacht, als du von Designfehler gesprochen hast? Noch ist die Methode nicht fest eingemeiselt und ich kann sich nach belieben anpassen.

    Bin sehr dankbar für eure Tipps. Solang man glaubt etwas richtig zu machen wird man sich nicht verbessern. Deshalb bin ich froh über jede Kritik, um mich und meinen Stil weiterentwickeln zu können.


Anmelden zum Antworten