Fehlerbehandlung in einer Engine



  • Badestrand schrieb:

    Du hättest ein Problem, wenn die Datei existiert aber du keine Rechte zum öffnen hast. Oder sie wird zwischen exists(...) und load(...) gelöscht 😉

    Zu 1.: Dann nennen wir exists eben um in existsAndIsReadable oder bauen uns noch isReadable dazu.
    Zu 2.: Da muss ich dir recht geben. Das ist ganz klar eine knallharte Schwäche des ganzen.
    Hast du einen Gegenvorschlag?

    Vielleicht wäre es am besten die Datei einfahc selbst mit std::fstream zu öffnen und dann per std::fstream& and readTexture oder sowas weiterzureichen.
    Dann muss man als Benutzer natürlich die Flags von fstream überprüfen und readTexture wirft einem runtime_error falls die nicht stimmen.



  • Badestrand schrieb:

    [...]
    edit: Aber abgesehen davon, dass das Dateisystem-Beispiel ungünstig gewählt wurde, wäre ein Runtime-Error und/oder ein assert angebracht, wenn eine Pre-Condition nicht eingehalten wurde.

    Wie meinst du das? Ich habe doch oben angegeben, dass ein runtime_error geworfen wird, wenn die pre condition nicht stimmt. Oder ist das nur als Bestätigung gemeint?
    Wieso ist das Beispiel schlecht gewählt, es ist doch extra so gewählt, dass man sich fragen muss ob man es mit Ausnahmen(=exceptions) zu tun hat wenn eine Datei nicht gefunden wurde?
    Es gibt natürlich Fälle bei dennen klar ist, dass Exceptions perfekt sind und solche bei dennen klar ist, dass sie fehl am platz sind. Das hier soll ja absichtlich ein schweres Beispiel sein.



  • fuzzy-wuzzy-bear schrieb:

    Hast du einen Gegenvorschlag?

    Ich sag ja, das Beispiel ist einfach schlecht gewählt 😉 Genau das, was du hier skizziert hast, gibt's ja auch in der Standard-Bibliothek:

    std::vector<int> nums = ...;
    size_t index = ...;
    if ( index < nums.size() )         // Sowas wie exists(..), nur halt ob Index in Range
        cout << nums.at(index) << "\n";   // Zugriff, mit sonstwas-exception bei falschem Index
    


  • fuzzy-wuzzy-bear schrieb:

    Wie meinst du das? Ich habe doch oben angegeben, dass ein runtime_error geworfen wird, wenn die pre condition nicht stimmt. Oder ist das nur als Bestätigung gemeint?

    Das Problem mit der Datei-existiert Pre-Condition hier ist, dass du sie als Aufrufer nicht mit 100% Sicherheit erfüllen kannst. Eben weil OS und Treiber und Hardware als unberechenbare Faktoren dazwischen liegen. Bei dem vector-Beispiel kannst du beim at(...) -Aufruf sicher sein, dass der Index "in Range" ist, wenn du vorher mit index<v.size() prüfst (es sei denn, in einem anderen Thread wird mit einer Referenz auf den vector genau dieser verändert). Wenn eine Pre-Condition verletzt wird, hast du beim Programmieren einen Fehler gemacht. Wenn eine Exception fliegt, tritt nur ein unwahrscheinlicher Fall ein, was aber kein Programmfehler ist.

    fuzzy-wuzzy-luzzy-bear schrieb:

    Wieso ist das Beispiel schlecht gewählt, es ist doch extra so gewählt, dass man sich fragen muss ob man es mit Ausnahmen(=exceptions) zu tun hat wenn eine Datei nicht gefunden wurde?
    Es gibt natürlich Fälle bei dennen klar ist, dass Exceptions perfekt sind und solche bei dennen klar ist, dass sie fehl am platz sind. Das hier soll ja absichtlich ein schweres Beispiel sein.

    Ist es auch 🙂 std::ifstream bzw ofstream regeln das ja mit internen Fehler-Zuständen, die man abfragen kann.

    Ich würde an die "Exception oder nicht Exception"-Frage anders herangehen, nämlich was der Nutzer der Funktion wohl erwarten würde. Und das hängt von dem Rahmen, vom Namen und von den Parametern der Funktion ab.
    Während man bei bool tryLoadTexture( const std::string& filename, TexturePtr& tex ) klar erwartet, dass der Rückgabewert den Erfolg zurückgibt und keine Exception fliegt, würde ich bei TexturePtr loadTexture( filename ) definitiv im Fehlerfall Exceptions erwarten. Auch würde ich bei keiner der Funktionen Pre-Conditions erwarten.



  • Xebov schrieb:

    2. ist so zu verstehen, ich kann mir im Moment keinen Fehler vorstellen der mehr als ne Access Violation durch den Raum fliegen lassen kann, was schlimeres, oder anderes ist mir bisher nie passiert, deswegen fehlt mir schlicht die Vorstellung welche schlimmen fehler noch so auftreten können

    Okay, um den vollen Eindruck zu erhalten, muss man sowas schon mal selber erlebt haben. 🙂

    Nein im Ernst, bei Zeigerfehlern zum Beispiel kann wirklich fast alles passieren. Wenn du Glück hast, kommt es sofort zu einem Laufzeitfehler. Mit ein bisschen weniger Glück hast du nach einiger Zeit noch einen Laufzeitfehler. Entweder eine Zugriffsverletzung oder eine Heap Corruption, wie sie beispielsweise beim Freigeben von invalidem Speicher auftreten kann. Doch wenn ein Zeiger zufälligerweise auf andere, verwendete Daten zeigt (kann leicht bei Array-Überläufen passieren), können diese überschrieben werden. Es kann im Übrigen auch gar nichts passieren. Fakt ist, dass das Verhalten sehr gefährlich ist, vor allem, wenn man nachher mit beschädigten Daten weiterarbeitet. Da ist ein sofortiger Programmabbruch sicher besser.

    Xebov schrieb:

    3. ist folgendermaßen gemeint der Begriff RAII sagt mir erstmal garnichts, ich hab das ganze zwar in den Artikeln mit gelesen jedoch im Netz noch nichts gefunden was mir das ganze wirklich näher bringen kontne.

    Such doch bei Google, beim ersten Treffer findest du einen Wikipedia-Eintrag.

    Xebov schrieb:

    1. Der fehler wäre ja aber mit nem Logbuch nicht schwer zu fidnen, wenn Funktion x etwas tun soll und es schlägt Fehl, dann würde sie einfach ins Logbuch schreiben was los war, dh ein Absturz ja, aber der Fehler würde sich sehr leicht finden lassen.

    Aber was hast du davon, das Programm noch weiterlaufen zu lassen? Unter Umständen tritt in dieser Zeit wieder ein Fehler auf und schon ist das Logbuch nicht mehr eindeutig. Mit grosser Wahrscheinlichkeit ist es das sowieso nicht, denn es können auch gut Fehler auftreten, die du behandelst. Das Identifizieren des verantwortlichen Fehlers ist dann mehr als mühsam.



  • Das is gut zu wissen welche Auswirkungen das komplett haben kann.

    Ja ich hab den Eintrag mittlerweile gefunden, der wurde bei meinene rsten Suchen irgendwie unterschlagen.

    Behandelte Fehler würde ich trotzdem eintragen, dh ein Eintrag bevor er behandelt wird, das mans genau weiß.



  • Zu aller erst muss man doch zwischen Fehlern und schwerwiegenden Fehlern unterscheiden.

    Wenn in einem Programm eine Datei geöffnet wird ist es ein Unterschied ob diese Datei fester Bestandteil des Programms ist oder von dem Programm als Eingabe verarbeitet wird. In ersterem Fall ist der Fehler schwerwiegend und eine Exception sollte geworfen werden und zum Programmabbruch führen. In letzterem Fall ist ganz klar, dass das Prüfen ob die Datei gültig ist zum eigentlichen Programm gehört und man sogar erwarten kann, dass hierbei Müll heraus kommt.
    Das ist eben der Unterschied zwischen "input-validation" und programm-internen Fehlern. Ersteres würde ich nicht per Exception behandeln, letzteres schon.

    Der Grund ist auch ganz einfach: wenn ein Benutzer einen Dateipfad angibt, dann überprüfe ich den und im Fehlerfall bin ich bereits an der richtigen Stelle um entsprechend zu agieren. Fehlt dagegen eine wichtige Datei, so kann ich mitten in der Bibliothek unmöglich entscheiden was zu tun ist und werfe eine Exception um sie dann an passender Stelle zu behandeln, dabei kann die passende Stelle auch das top-level try-catch sein, das nur eine Dialog-Box anzeigt und anschließend das Programm beendet.



  • Nexus schrieb:

    Xebov schrieb:

    3. ist folgendermaßen gemeint der Begriff RAII sagt mir erstmal garnichts, ich hab das ganze zwar in den Artikeln mit gelesen jedoch im Netz noch nichts gefunden was mir das ganze wirklich näher bringen kontne.

    Such doch bei Google, beim ersten Treffer findest du einen Wikipedia-Eintrag.

    Also ich hab das Mal gelesen in Ruhe, jetzt wird mir auch der Zusammenhang mit Exceptions kalr, allerdings eine Frage bliebe dazu noch im Raum stehen. Im Grunde wäre RAII ja nichts anderes als die garantie das Objekte beim verlassen des Scopes zerstört werden sofern sie nicht mit new angelegt wurden, das heist aber das man sich wenn man ein Objekt über new anlegt nicht auf RAII verlassen kann und manuell aufräumen muß sehe ich das richtig?



  • Xebov schrieb:

    ...sehe ich das richtig?

    Ja.



  • Xebov schrieb:

    wenn man ein Objekt über new anlegt nicht auf RAII verlassen kann und manuell aufräumen muß sehe ich das richtig?

    Deshalb kapselt man den new Aufruf mit Hilfe eines smart pointers und hat wieder ein Stack objekt dass aufräumen kann.

    Also nur auf die Gefahr hin das du mich missverstanden hattest mit Teil 2 meinte ich den unteren Abschnitt des 2. Quotes. Du sagst da sind einem keine Exceptions im Weg, das wirft bei mir die Frage auf wer wirft die denn dann? Ich meine das Was da steht ist ja im Grunde Code der keine Exceptions wirft. Oder übersehe ich da was?

    Da ist schon alles korrekt. Die exceptions werden durchgereicht und treten irgendwo auf. eben bei new. und new wird irgendwo ganz weit weg aufgerufen.

    und so sieht eben code mit exceptions als fehlerbehandlung aus.

    das ist es auch was ich mit code verdopplung meine. wenn XX_FAILED returned wird, musst du überall XX_FAILED abfangen und behandeln. oder zumindest abfangen. das muss man bei exceptions nicht machen...

    @fuzzy-wuzzy-bear:
    um Fehlerbehandlung kannst du dich nicht drücken. Jedes load() kann fehlschlagen und wenn nur eine out of memory exception fliegt. selbst wenn du jetzt garantieren kannst dass du genug freien speicher hast, die datei existiert und lesbar ist und gültige daten enthält. in einer millisekunde später kann das alles anders sein (und wenn der anwender einfach die festplatte ausgesteckt hat).

    Das sind Fehler gegen die du dich nicht schützen kannst. Wenn du mit error codes hantierst, dann ignorierst du den fehler "weil er nie auftritt". mit exceptions musst du 0 code schreiben um den fehler korrekt zu behandeln. NULL CODE. KEINEN EINZIGEN STRICH. kein einziger gedanke muss an diesen fehler verschwendet werden und kein finger muss krumm gemacht werden. und dennoch wird der fehler korrekt behandelt.

    das ist der vorteil von exceptions.



  • Shade Of Mine schrieb:

    Deshalb kapselt man den new Aufruf mit Hilfe eines smart pointers und hat wieder ein Stack objekt dass aufräumen kann.

    Naja die Frage ist da nur wo fängt man da an und wo hört man da auf und wie evrhindert man das Objekte unabsichtlich gelöscht werden. Ich gebe dazu mal das größte zusammenhängende Geflächtd as ich hier habe, ich habe eine Modelklasse, die Lädt ein Modell und erstellt Indexbuffer und Vertexbufefr die jeweils eine eigene Klasse darstellen, per new, soviele wie gebraucht werden, die sidn wiederrum eine eigene Klasse, dann lädt er Effekte, die sind auch ne eigene Klasse, er lädt Texturen, die haben auch ne eigene Klasse, allerdings macht das Programm da sganze nicht im Konstruktor sondern in eienr Init methode und hat auch eine ShutDown methdoe, sollte Init erfolgreich gewesen sein würde der Konstruktor automatisch ShutDown aufrufen und alles aufräumen. Hier mit Smart Pointern zu Hantieren wäre ganz schon aufwendig allein beim Gedanken das die Objekte einfach so im Abfluß verschwinden könnten, oder eben auchmal garnicht verschwinden wird mir irgendwie schlecht.

    Shade Of Mine schrieb:

    Da ist schon alles korrekt. Die exceptions werden durchgereicht und treten irgendwo auf. eben bei new. und new wird irgendwo ganz weit weg aufgerufen.

    und so sieht eben code mit exceptions als fehlerbehandlung aus.

    OK, das klingt ja shconmal nicht schlecht wegen einiger Beispiele bin ich davon ausgegangen das man Exceptions überall von Hand weiterreichen muß, wenn die Exception aber einfach in nen Fluß fällt und bis sie irgendwo abgefangen wird treibt dann ist das ja garnichtmal so schlecht.



  • Xebov schrieb:

    [...]Hier mit Smart Pointern zu Hantieren wäre ganz schon aufwendig allein beim Gedanken das die Objekte einfach so im Abfluß verschwinden könnten, oder eben auchmal garnicht verschwinden wird mir irgendwie schlecht.

    Es ist aber bei jedem Smartpointer-Typen klar definiert, zu welchem Zeitpunkt das verwaltete Objekt zerstört wird. "Einfach so" oder "gar nicht" passiert da nichts.



  • Xebov schrieb:

    nicht im Konstruktor sondern in eienr Init methode und hat auch eine ShutDown methdoe, sollte Init erfolgreich gewesen sein würde der Konstruktor automatisch ShutDown aufrufen und alles aufräumen. Hier mit Smart Pointern zu Hantieren wäre ganz schon aufwendig allein beim Gedanken das die Objekte einfach so im Abfluß verschwinden könnten, oder eben auchmal garnicht verschwinden wird mir irgendwie schlecht.

    Keine init Methoden verwenden sondern im Konstruktor die Arbeit machen.

    Ich wette mit dir, du reduzierst den code dadurch ein gutes Stück.

    Wenn du RAII konsequent einsetzt, dann sparst du dir enorm viel arbeit...

    OK, das klingt ja shconmal nicht schlecht wegen einiger Beispiele bin ich davon ausgegangen das man Exceptions überall von Hand weiterreichen muß, wenn die Exception aber einfach in nen Fluß fällt und bis sie irgendwo abgefangen wird treibt dann ist das ja garnichtmal so schlecht.

    Genau so funktioniert es auch. Exceptions werden einfach weiter gereicht und alle RAII Objekte werden korrekt zerstört.

    Im idealfall hat man deshalb wie gesagt nur ein try/catch im ganzen Programm... (praktisch gesehen gibt es natürlich immer wieder stellen wo man fehler dann doch jetzt behandeln muss - aber der fehlerbehandlungs code ist im vergleich zu einer error code variante verschwindend gering)



  • Shade Of Mine schrieb:

    Keine init Methoden verwenden sondern im Konstruktor die Arbeit machen.

    Ich wette mit dir, du reduzierst den code dadurch ein gutes Stück.

    Wenn du RAII konsequent einsetzt, dann sparst du dir enorm viel arbeit...

    Naja so einafch ist das nicht, bei Modellen und effekten zB würde es bei mir gehen, alelrdings würde das die Wiederverwendbarkeit einschränken, die Init Variante die ich evrwende stützt sich auf eine bool Variable, im Konstruktor wird sie Falls, wenn Init abgeschlossen ist wird sie true, wird die Klasse dann gelöscht ohne ShutDown aufzurufen (was sie false machen würde) dann würde der destruktor automatisch ShutDown aufrufen. Mein größtes Problem wären aber meine Verwaltungsstützen, die sind nämlich Singleton Klassen, da ist der Konstruktor ja etwas beschrängt.

    Shade Of Mine schrieb:

    Genau so funktioniert es auch. Exceptions werden einfach weiter gereicht und alle RAII Objekte werden korrekt zerstört.

    Im idealfall hat man deshalb wie gesagt nur ein try/catch im ganzen Programm... (praktisch gesehen gibt es natürlich immer wieder stellen wo man fehler dann doch jetzt behandeln muss - aber der fehlerbehandlungs code ist im vergleich zu einer error code variante verschwindend gering)

    Naja ganz so leicht stell ich mir das nicht vor wenn man zB Load Model nimmt und das ganze etwas verschachtelt ist weil da viele Klassen am werkeln sind müsste man schon jeden Aufruf in einen Block einpacken, da käme man nicht drumherum, aber es klingt auf jedenfalls chonmal besser als es der Artikel suggeriert hat.



  • Xebov schrieb:

    Naja ganz so leicht stell ich mir das nicht vor wenn man zB Load Model nimmt und das ganze etwas verschachtelt ist weil da viele Klassen am werkeln sind müsste man schon jeden Aufruf in einen Block einpacken

    Warum? Wenn beim Laden irgendwo was schief geht (egal auf welcher Ebene), dann ist doch eh das gesamte Modell im Eimer. Dann kannst Du RAII alles wieder aufräumen lassen, und irgendwo an oberster Stelle eine Fehlermeldung o.ä generieren.



  • Tachyon schrieb:

    Warum? Wenn beim Laden irgendwo was schief geht (egal auf welcher Ebene), dann ist doch eh das gesamte Modell im Eimer. Dann kannst Du RAII alles wieder aufräumen lassen, und irgendwo an oberster Stelle eine Fehlermeldung o.ä generieren.

    Naja das ist so nicht ganz richtig, nur wiel Teile schiefgehen muß nicht alles Schiefgehen, nehmen wir zB ne Textur ist nichtd a, dann wäre das Modell in Ordnung weil das Modell aber die texturen lädt gäbe es nur die Möglichkeit entweder zu sagen ein Teil fehlt also mache ich nichts, oder eine Standartbehandlung hinzustellen die eine Exception vermeidet dafür aber den Anwende rzwingen würde zusätzliche Daten in die Klassen zu Packen, zB übr eine static Variable.

    Abe rim wesentlichen meinte ich das shcon so wie du man müsste eben beim aufrufen des Modelladens einfach nen tryBlock außenrum Packen.



  • Xebov schrieb:

    Naja das ist so nicht ganz richtig, nur wiel Teile schiefgehen muß nicht alles Schiefgehen, nehmen wir zB ne Textur ist nichtd a, dann wäre das Modell in Ordnung weil das Modell aber die texturen lädt gäbe es nur die Möglichkeit entweder zu sagen ein Teil fehlt also mache ich nichts, oder eine Standartbehandlung hinzustellen die eine Exception vermeidet dafür aber den Anwende rzwingen würde zusätzliche Daten in die Klassen zu Packen, zB übr eine static Variable.

    Nix static variable oder so.

    Du musst nur definieren ab wann ein Modell in einem gültigen Zustand ist. Ist ein Modell ohne Texturen gültig?

    Das ist die Frage.

    Was man zB machen kann ist wenn die eine Texture nicht lädt eine standard texture zu laden oder ähnliches.

    im prinzip ist das alles aber kein technisches problem sondern eine definitionsfrage. wenn du definiert hast was gültig ist und was nicht, dann ist der rest einfach.

    ich habe aber eher das gefühl aktuell ignorierst du das fehlschlagen vom laden einer textur. aber auch das ist mit exceptions möglich...



  • Xebov schrieb:

    Shade Of Mine schrieb:

    Keine init Methoden verwenden sondern im Konstruktor die Arbeit machen.

    Ich wette mit dir, du reduzierst den code dadurch ein gutes Stück.

    Wenn du RAII konsequent einsetzt, dann sparst du dir enorm viel arbeit...

    Naja so einafch ist das nicht, bei Modellen und effekten zB würde es bei mir gehen, alelrdings würde das die Wiederverwendbarkeit einschränken, die Init Variante die ich evrwende stützt sich auf eine bool Variable, im Konstruktor wird sie Falls, wenn Init abgeschlossen ist wird sie true, wird die Klasse dann gelöscht ohne ShutDown aufzurufen (was sie false machen würde) dann würde der destruktor automatisch ShutDown aufrufen. Mein größtes Problem wären aber meine Verwaltungsstützen, die sind nämlich Singleton Klassen, da ist der Konstruktor ja etwas beschrängt.

    Siehst du, genau deshalb solltest du dich wirklich nochmal mit den Kernkonzepten RAII und Exceptions in C++ beschäftigen. Für diesen aufwendigen bool-Mechanismus, ob ein Objekt jetzt korrkt konstruiert wurde, gibt es in C++ eine einfache Lösung. Und ja, sie lautet Exceptions im Fehlerfall werfen und RAII-Objekte benutzen. Wurde ein Objekt im Konstruktor "halb" konstruiert und es fliegt eine Exception, wird es auch wieder "halb" zerstört. Und das ganze voll automatisch ohne extra Zeile Code. Recht praktisch, oder?

    Gruß
    Don06



  • Shade Of Mine schrieb:

    im prinzip ist das alles aber kein technisches problem sondern eine definitionsfrage. wenn du definiert hast was gültig ist und was nicht, dann ist der rest einfach.

    ich habe aber eher das gefühl aktuell ignorierst du das fehlschlagen vom laden einer textur. aber auch das ist mit exceptions möglich...

    Ich ignoriere zzt alles an Fehlern, deswegen beschäftige ich mich ja mit dem thema und verfolge den thread um nen geigneten weg zu fidnen das ganze auf bzw umzurüsten.

    Naja definition ist so eine Sache, das Problem daran ist für mich ja folgendes, wirft die Textur eine Exception dann ist es ihr ansich ja erstmal egal wer der Auftraggeber des vorgangs war dh hat eine andere Klasse den Auftrag erteilt oder der Nutzer selbst, was mich zu dem Problem führt das man die Ausnahmebehandlung direkt in den Auftraggeber wie in diesem Fall den Modelloader einbinden müsste. denn wenn die Exception aus dem Model einmal raus ist müsste man ja erstmal geeignete Methoden einbauen die es ermöglichend en Fehler im top-level zu behandeln.

    Don06 schrieb:

    Für diesen aufwendigen bool-Mechanismus, ob ein Objekt jetzt korrkt konstruiert wurde, gibt es in C++ eine einfache Lösung.

    Aufwendig ist er garnicht, eine Variable und 3 stellen Code an der sie gesetzt wird sind nicht aufwendig, immerhin ermöglicht es so ein System die ganzen Objekte zu erstellen und sie hinterher zu Initialisieren, denn wenn ich im Konstruktor direkt zB eine Modeldatei anfordere dann gibts ein paar Probleme beim erstellen. Eine Init Variante ist nicht schlecht, denn damit kann man das Objekt erstellen und getrennt davon Initialisieren außerdem bliebe das ganze im Falle eienr exception Gültig, der Konstruktor tut nicht viel und das ModelObjekt bräuchte eine getrennte initialisierung, wenn irgendetwas schiefläuft stürzt die Initalisierung aber eben nicht das Objekt es wäre nach einer neuerlichen initalisierung gültig.



  • Xebov schrieb:

    Naja definition ist so eine Sache, das Problem daran ist für mich ja folgendes, wirft die Textur eine Exception dann ist es ihr ansich ja erstmal egal wer der Auftraggeber des vorgangs war dh hat eine andere Klasse den Auftrag erteilt oder der Nutzer selbst, was mich zu dem Problem führt das man die Ausnahmebehandlung direkt in den Auftraggeber wie in diesem Fall den Modelloader einbinden müsste. denn wenn die Exception aus dem Model einmal raus ist müsste man ja erstmal geeignete Methoden einbauen die es ermöglichend en Fehler im top-level zu behandeln.

    Warum ist das kompliziert?

    LoadModel lädt das Modell + alle Texturen. Wenn eine textur nicht lädt, dann default textur laden, lädt die auch nicht dann die exception weiter reichen an den anrufer und irgendwer wird sich verantwortlich fühlen und eine fehlermeldung ausgeben.

    Aufwendig ist er garnicht, eine Variable und 3 stellen Code an der sie gesetzt wird sind nicht aufwendig, immerhin ermöglicht es so ein System die ganzen Objekte zu erstellen und sie hinterher zu Initialisieren, denn wenn ich im Konstruktor direkt zB eine Modeldatei anfordere dann gibts ein paar Probleme beim erstellen.

    Welche Probleme denn?

    Init Methoden sind fast immer eine schlechte wahl.


Anmelden zum Antworten