Spiel schreiben um C++ zu erlernen - Exceptions



  • Hallo,

    ich möchte meine Fähigkeiten in C++11 verbessern.
    Deswegen habe ich vor ein kleines Spiel (OpenGL) zu programmieren.

    Was ich mich jedoch Frage ist, wie ich Ausnahmen "behandeln" bzw. darstellen soll.

    • try , throw und co.
    • (fast) gar keine Exceptions
    • jede Methode gibt OK oder ein Fehlerobjekt zurück und der ehemalige Rückgabewert ist eine Referenz als erstes Argument.
    • boost::variant<Exception1, Exception2, ExpectedReturnType> -like Rückgabewerte
    • Globale Fehlerbehandlung wie glGetError
    • exit und atexit 🙄
    • ...

    Was wäre der C++11-igste, sauberste oder portableste Weg?
    Wird sich was mit C++14 ändern?



  • Exceptions (das mit try und catch).



  • Nathan schrieb:

    Exceptions (das mit try und catch).

    Das kommt völlig drauf an.
    Eigentlich wollte ich die Nase raus halten aus diesem Thread, aber so pauschal ist die Aussage falsch.

    Was ich mich jedoch Frage ist, wie ich Ausnahmen "behandeln" bzw. darstellen soll.

    Was für Ausnahmen? Das hängt völlig davon ab, wie gravierend die Ausnahme ist, in welchem Kontext, usw.


  • Mod

    Wir können aber relativ pauschal sagen, dass

    • jede Methode gibt OK oder ein Fehlerobjekt zurück und der ehemalige Rückgabewert ist eine Referenz als erstes Argument.
    • boost::variant<Exception1, Exception2, ExpectedReturnType>-like Rückgabewerte
    • Globale Fehlerbehandlung wie glGetError
    • exit und atexit 🙄

    nicht empfehlenswert sind. Das sind Konstrukte die man sich baut, wenn man sonst keine Möglichkeiten der Fehlerbehandlung hat. Sie machen den Code unleserlich, verkomplizieren den Kontrollfluss (können dadurch sogar die Performance mindern) und benötigen mehr Disziplin beim Programmieren.



  • SeppJ schrieb:

    nicht empfehlenswert sind. Das sind Konstrukte die man sich baut, wenn man sonst keine Möglichkeiten der Fehlerbehandlung hat. Sie machen den Code unleserlich, verkomplizieren den Kontrollfluss (können dadurch sogar die Performance mindern) und benötigen mehr Disziplin beim Programmieren.

    Das stimmt. Ich würde sogar sagen, einige sind C-Style Konstrukte - siehe das Exception-Äquivalent.
    Und das mit dem "Fehlercode im Rückgabewert" ist AFAIR ein Klassiker der WinAPI.
    Aber das Ding mit dem Variant ist tatsächlich einfach nur Mist. Wer zum Teufel benutzt das? Wenn überhaupt dann die Kombination wie std::pair<iterator, bool> ...



  • Was ich mich jedoch Frage ist, wie ich Ausnahmen "behandeln" bzw. darstellen soll.

    Nun, definiere doch erstmal deine Ausnahmen, welche koennte es den bei einem Spiel geben, die nicht zum Programmabbruch fuehren sollten?

    Welche Ausnahmen fuehren bei einem Spiel wie Tetris nicht zum Programmabbruch?

    Aber das Ding mit dem Variant ist tatsächlich einfach nur Mist. Wer zum Teufel benutzt das? Wenn überhaupt dann die Kombination wie std::pair<iterator, bool>

    Nun optional ist was er meint. Kennst du Maybe aus Haskell? http://isocpp.org/blog/2012/12/systematic-error-handling-in-c-andrei-alexandrescu



    • try , throw und co.//Ja, wo es nötig ist, also nur 1% von wo man in Java Excrptions wirft.
    • (fast) gar keine Exceptions//Genau
    • jede Methode gibt OK oder ein Fehlerobjekt zurück und der ehemalige Rückgabewert ist eine Referenz als erstes Argument.//NIE NIE NIE
    • boost::variant<Exception1, Exception2, ExpectedReturnType> -like Rückgabewerte//KOTZ, wer kommt denn auf sowas?
    • Globale Fehlerbehandlung wie glGetError//Wenn das angemessen erscheint, dann Exceptions
    • exit und atexit 🙄//haha
    • ...

    [/quote]



  • knivil schrieb:

    Aber das Ding mit dem Variant ist tatsächlich einfach nur Mist. Wer zum Teufel benutzt das? Wenn überhaupt dann die Kombination wie std::pair<iterator, bool>

    Nun optional ist was er meint. Kennst du Maybe aus Haskell? http://isocpp.org/blog/2012/12/systematic-error-handling-in-c-andrei-alexandrescu

    Ja an Maybe/Optinal hatte ich auch gedacht.

    Vielen Dank für den Link.

    knivil schrieb:

    Was ich mich jedoch Frage ist, wie ich Ausnahmen "behandeln" bzw. darstellen soll.

    Nun, definiere doch erstmal deine Ausnahmen, welche koennte es den bei einem Spiel geben, die nicht zum Programmabbruch fuehren sollten?

    Welche Ausnahmen fuehren bei einem Spiel wie Tetris nicht zum Programmabbruch?

    Wenn möglich soll kein Fehler zu einem Programmabbruch führen sondern sinnvolle Hinweisdialoge erezugen.

    Sone schrieb:

    Was für Ausnahmen? Das hängt völlig davon ab, wie gravierend die Ausnahme ist, in welchem Kontext, usw.

    Von OpenGL konnte nicht gestarten werden! über Dieses Level ist ungültig. Bitte wählen Sie ein neues aus. oder Der Spielername ist ungültig. (Ich weiß, dass dies nicht umbedingt eine Exception sein muss - Nur eben ein Beispiel).



  • 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.


Anmelden zum Antworten