Du bist nicht angemeldet.

Stilllegung des Forums
Das Forum wurde am 05.06.2023 nach über 20 Jahren stillgelegt (weitere Informationen und ein kleiner Rückblick).
Registrierungen, Anmeldungen und Postings sind nicht mehr möglich. Öffentliche Inhalte sind weiterhin zugänglich.
Das Team von spieleprogrammierer.de bedankt sich bei der Community für die vielen schönen Jahre.
Wenn du eine deutschsprachige Spieleentwickler-Community suchst, schau doch mal im Discord und auf ZFX vorbei!

Werbeanzeige

1

31.01.2015, 19:51

[C++11] Fehlerbehandlung Best Practices

Hi, ich habe im Moment mit verschiedenen Möglichkeiten experimentiert, ungültige Parameter für Funktionsaufrufe zu behandeln. Aber so richtig bin ich mir noch nicht darüber im klaren, was so gängige Praxis ist..

  • Einerseits kann man aus Sicht der Dokumentation die Parameter spezifizieren, z.B. einen Wertebereich für einen int festlegen und für sonstige Fälle die "undefined behavior"-Schiene fahren. Das beruhigt vielleicht das Gewissen ("es steht doch in der Doku..."), hilft aber nicht viel wenn man man einen falschen Wert verwendet. Dazu kommt, dass man sich dadurch das Testen von Edgecases verbaut.
  • Eine andere Möglichkeit wäre, gewisse Bedingungen mit assert() abzuprüfen. Dann crasht das Programm zwar mit Zeilenangabe, zum Testen ist es aber immernoch schlecht, wenn man einen Edgecase testen will, und die Testsuite abstürzt - auch wenn es explizit zu erwarten ist ^^
  • Was mit Testframeworks gut zusammenarbeitet, ist die Rückgabe eines Bool's. Aber eigentlich ist diese Technik nicht wirklich "modern". Einerseits werden Rückgabewerte schnell mal "übersehen" oder aus Nachlässigkeit absichtlich nicht geprüft. Zum anderen fehlt dann die Möglichkeit des eigentlichen Rückgabewertes.... den dann prinzipiell auf call-by-reference zu verschieben erscheint mir keine gute Lösung.
  • Was aus meiner Sicht übrig bleibt, sind Exceptions. Die lassen sich mit eigentlich jedem Testframework "erwarten" ala REQUIRE_THROWS(...) etc. Baut man seinen Code RAII-konform, braucht man sich über das Leaken im Grunde keine Gedanken zu machen. Und verwendet man Exceptions wirklich nur für Fehlerfälle (und nicht um irgendwelche Pseudo-Kontrollflusssachen zu machen), sollte es auch performance-technisch nicht so einbrechen... die Exceptions werden dann ja eigentlich nur in den Testfällen geworfen - oder im eigentlichen Code, wenn irgendwas nicht stimmt... dann den Code-Dump genommen und zum Debugger gelaufen... Tja eigentlich optimal...

Welche Möglichkeiten seht ihr noch? Ich persönlich würde den Exception-Ansatz bevorzugen. Dann stellt sich mir allerdings nur die Frage: Welche Exception-Typen werde ich? Exceptions der Standardlib mit eigenen Exception-Meldungen? Oder sollte ich mir für meine Fehlerfälle eigene Exception-Klassen bauen? Wie exzessiv würdet ihr das betreiben? Beispiel: Angenommen ich habe eine API um GameObjects irgendwie anzusprechen und verwende dafür stehts die ObjectID. Würdet ihr dann für ungültige IDs (z.B. wenn das Objekt gar nicht existiert) z.B. eine InvalidObjectException bauen und werfen?

Exception-technisch habe ich bisher nur grundlegende Erfahrungen gemacht... Daher wäre etwas "Inspiration" gut :) Zum dem Thema habe ich mir von der letzten CppCon schon die Videos von Jon Kalb über exception-safe code angesehen....

Dazu kommt: Wie handhabt man das gewöhnlich im Bezug auf Debug- und Release-Code? z.B. in viel aufgerufenen Funktionen ständig die Parameter auf Gültigkeit zu überprüfen könnte vielleicht manchmal nicht förderlich sein ... oder mache ich mir da unnötige Sorgen?

LG Glocke

Schrompf

Alter Hase

Beiträge: 1 470

Wohnort: Dresden

Beruf: Softwareentwickler

  • Private Nachricht senden

2

31.01.2015, 21:11

Da machst Du Dir unnötige Sorgen :) Die paar if() sind im allgemeinen Ablauf für die Performance irrelevant, und spätestens mit Profile Guided Optimisation wird die Sprungvorhersage jedes aktuellen Prozessors da einfach linear drüberjagen. Also keine Sorge. Im Umgang mit DirectX würde ich genauso wie im Umgang mit der WinAPI immer Rückgabewerte prüfen. Höchstens bei Release() erspare ich mir das, außer ich jage gerade einene Fehler in dieser Richtung.

Ich würde Dir zu Exceptions raten, aber eigentlich reicht genau eine eigene Klasse YourOwnException, die Du von std::exception ableitest. Wenn man sich an DirectX wendet, geht man eigentlich davon aus, dass es gut gehen wird, sonst würde man es gar nicht erst probieren. Jedes Abweichen von dieser Annahme ist eine Ausnahme, also Exception. Und eine Exception reicht, weil Du ja dann nicht wirklich unterscheiden willst, warum genau es schiefgegangen ist, sondern willst nur möglichst sauber abschmieren und im Log eine Info für die Fehlersuche hinterlassen. Also reicht eine Exception-Klasse, theoretisch sogar die std::exception direkt benutzen.

Meine persönliche Richtlinie sieht so aus:

Wenn die Operation üblicherweise gut geht, und Du beim Scheitern eigentlich nicht sinnvoll weitermachen kannst, und es Dir eigentlich wurscht ist, woran genau die Operation schiefgegangen ist -> Ausnahme. Beispiele: Texturen anlegen, DrawCalls ausführen, Thread anlegen.

Wenn die Operation durchaus mal scheitern kann oder/und Du im Scheiterfall die Fehlerursache kommunizieren willst, weil es für den Aufrufer tatsächlich relevant ist -> Funktion mit Rückgabewert. Beispiele: Datei öffnen, IP-Verbindung, Nutzerdaten-Abfrage.
Häuptling von Dreamworlds. Baut aktuell an nichts konkretem, weil das Vollzeitangestelltenverhältnis ihn fest im Griff hat. Baut daneben nur noch sehr selten an der Open Asset Import Library mit.

3

31.01.2015, 21:30

Da machst Du Dir unnötige Sorgen :)

*Schweiß-von-der-Stirn-wisch*

Im Umgang mit DirectX würde ich genauso wie im Umgang mit der WinAPI immer Rückgabewerte prüfen.

:D Ich hab nichtmal nen Windows-Rechner xD Spaß beiseite :D

Und eine Exception reicht, weil Du ja dann nicht wirklich unterscheiden willst, warum genau es schiefgegangen ist, sondern willst nur möglichst sauber abschmieren und im Log eine Info für die Fehlersuche hinterlassen. Also reicht eine Exception-Klasse, theoretisch sogar die std::exception direkt benutzen.

Das stimmt natürlich! Prinzipiell würde wirklich eine eigene LoggedException-Klasse reichen..

Meine persönliche Richtlinie sieht so aus:

Wenn die Operation üblicherweise gut geht, und Du beim Scheitern eigentlich nicht sinnvoll weitermachen kannst, und es Dir eigentlich wurscht ist, woran genau die Operation schiefgegangen ist -> Ausnahme. Beispiele: Texturen anlegen, DrawCalls ausführen, Thread anlegen.

Wenn die Operation durchaus mal scheitern kann oder/und Du im Scheiterfall die Fehlerursache kommunizieren willst, weil es für den Aufrufer tatsächlich relevant ist -> Funktion mit Rückgabewert. Beispiele: Datei öffnen, IP-Verbindung, Nutzerdaten-Abfrage.

Das macht wirklich einen sehr runden Eindruck!! :) :thumbup:

DeKugelschieber

Community-Fossil

Beiträge: 2 641

Wohnort: Rheda-Wiedenbrück

Beruf: Software-Entwickler

  • Private Nachricht senden

4

01.02.2015, 01:30

Jo Exceptions, dafür sind sie da. Ständig Rückgabewerte von Funktionen zu testen ist einerseits lästig, andererseits verzweigst du dein Programm dadurch unnötig und machst es komplexer. Bei vielen Funktionen die so getestet werden kann es dann schwieriger werden Fehler zu finden.
Gibt bei Exceptions immer sinnvolle Informationen aus, z.B. welche Reichweite von Werten denn gültig wäre.

xardias

Community-Fossil

Beiträge: 2 731

Wohnort: Santa Clara, CA

Beruf: Software Engineer

  • Private Nachricht senden

5

01.02.2015, 05:31

Du kannst assertions auch mit Exceptions kombinieren indem du dein eigenes assert Makro schreibst, welches eine beliebige Exception wirft.

Alternativ gibt es in den meisten Test Frameworks auch ne moeglichkeit auf fehlgeschlagene asserts zu testen (z.B. EXPECT_DEATH in Google Test): http://stackoverflow.com/questions/37564…ith-google-test

6

01.02.2015, 09:32

Du kannst assertions auch mit Exceptions kombinieren indem du dein eigenes assert Makro schreibst, welches eine beliebige Exception wirft.

Das hatte ich auch mal probiert. Aber dabei die fehlgeschlagene Prüfbedingung als Fehlergrund in eine Logfile zu schreiben und dann die Exception zu werfen, hat mir nicht gefallen.

C-/C++-Quelltext

1
2
3
4
5
6
void Grid::spawn(ObjectID id, Vector2u position) {
    if (id > MAX_OBJECTS) {
        throw MyException("Invalid object #" + std::to_string(id));
    }
    // ...
}

vs.

C-/C++-Quelltext

1
2
3
4
void Grid::spawn(ObjectID id, Vector2u position) {
    MyAssertion(id <= MAX_OBJECTS);
    // ...
}


Beim Assertion-Ansatz fehlen mir dann irgendwie zusätzliche Daten, z.B. die ObjectID, die das ganze verursache hat. An was ich (in der Makro-Implementierung des Asserts) rankomme, ist der String "id > MAX_OBJECTS" und die Tatsache, dass der Ausdruck false ist. Von daher würde ichd en Exception-Ansatz verfolgen wollen.

Alternativ gibt es in den meisten Test Frameworks auch ne moeglichkeit auf fehlgeschlagene asserts zu testen (z.B. EXPECT_DEATH in Google Test): http://stackoverflow.com/questions/37564…ith-google-test

Ok, dann verwende ich entweder die falsche Testsuite oder ich bin zu blöd das in meiner zu finden xD

Gibt bei Exceptions immer sinnvolle Informationen aus, z.B. welche Reichweite von Werten denn gültig wäre.

Dabei stehe ich nun vor der Frage: Wirklich eine Exception-Klasse, für deren Werfen ich den String "mühsam" (d.h. oft mit std::to_string() und damit einigen Stringverknüpfungen) zusammenbauen muss - oder mehrere Exception-Klassen, die ich mit den Einzeldaten "füttere" und dann später den die Daten "einfach" z.B. mit std::cout << bla << foo zusammenbaue. z.B. habe ich eine Logging-Klasse, die verschiedene operator<<()-Überladungen für im Endeffekt alle Typen hat, die ich verwende (d.h. z.B. auch um Vektoren "einfach" zu printen ohne mit selber noch mit std::to_string() arbeiten zu müssen.

Wegen der Logger-Implementierung überlege ich, ob ich mehrere Exception-Typen baue, die im ctor die Daten direkt bekommen, z.B.

C-/C++-Quelltext

1
2
3
throw ObjectNotFound(scene_id, object_id);
throw PositionNotFound(scene_id, tile_pos);
// usw.


Das blöde ist nur, dass ich dann viele Exception-Klassen brauche. Einfach eine mit verschiedenen ctor's klappt nicht gut, wenn ich verschiedene Fehlerfälle habe, die identische Variablentypen und -anzahl mit sich bringen. z.B. könnte InvalidObject(int) eine Ausgabe a la

Quellcode

1
ObjectID #17 is not used.

in die Log-File schreiben. Eine Exception InvalidScene(int) wäre (beim Überladen des ctor's) nicht von der ersteren Unterscheidbar, so dass ich nur noch

Quellcode

1
Invalid id #17.

ausgeben könnte, aber nicht mehr wüsste: ObjectID? SceneID? ItemID? foo?

Die aussagekräftigen Logging-Einträge sollen ja beim Identifizieren von Fehlern suchen ... :) Habt ihr da einen möglichen Ausweg für mich? :)

Und noch eine zweite Überlegung: Wenn ich die Exceptions im Programm (z.B. im mainloop) abfange (um z.B. noch den Spielstand zu speichern) könnte ich die Logfile auch schreiben lassen. Denkbar wäre dann sogar die Exceptions in folgendem Stile zu bauen (auch wenn es keine wirklichen C++-Exceptions mehr sind, aber ich könnte ja sogar einen int werfen xD)

C-/C++-Quelltext

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class GameError {
    public:
        virtual void dump(Logger& logger) = 0;
};

class ObjectNotFound: public GameError {
    private:
        int scene_id, object_id;
    public:
        ObjectNotFound(int scene_id, int object_id)
            : GameError{}
            , scene_id{scene_id}
            , object_id{object_id} {
        }

        void dump(Logger& logger) override {
            logger << "Cannot find object #" << object_id << " at scene # " << scene_id << "\n";
        }
};
// etc.


So dass ich beim Auffangen der Exception nur e.dump(error_logger); und einen Rethrow durchführe.... Könnte ich direkt mal probieren, ob das klappt^^

LG Glocke

/EDIT: Klappt, nur geht der Callstack flöten ;(

Dieser Beitrag wurde bereits 1 mal editiert, zuletzt von »Glocke« (01.02.2015, 10:11)


DeKugelschieber

Community-Fossil

Beiträge: 2 641

Wohnort: Rheda-Wiedenbrück

Beruf: Software-Entwickler

  • Private Nachricht senden

7

01.02.2015, 11:00

Du brauchst für die Exceptions eine einheitliche Schnittstelle für den Logger und ansonsten würde ich tatsächlich mehrere Klassen anlegen. Das ist auch sprechender im Code als überall nur std::exception. Um Dopplungen ein wenig zu reduzieren kannst du ja mehrere Konstruktoren bereitstellen, die dann die Parameter in eine sinnvolle Nachricht umbauen.

8

01.02.2015, 11:24

Du brauchst für die Exceptions eine einheitliche Schnittstelle für den Logger

Hmm eigentlich das noch nicht einmal. Mein Logger ist (absichtlich!) eine globale Variable, so dass ich im ctor meiner Exception X die Werte direkt in den Logger schreiben kann. Jede Unter-Exception bekommt andere Daten und schreibt etwas anderes in den Logger.

Um Dopplungen ein wenig zu reduzieren kannst du ja mehrere Konstruktoren bereitstellen, die dann die Parameter in eine sinnvolle Nachricht umbauen.

z.B. der Art?

C-/C++-Quelltext

1
2
3
4
5
6
7
8
9
10
11
class SceneException {
    SceneException(int scene_id) {
        logger << "Invalid Scene #" << scene_id << "\n";
    }
    SceneException(int scene_id, int object_id) {
        logger << "Object #" << object_id << " not found in Scene #" << scene_id << "\n";
    }
    SceneException(int scene_id, Point pos) {
        logger << "Scene #" << scene << " has no position " << pos << "\n";
    }
};

Wenn mir beim rethrowen der Callstack verloren geht überlege ich, das Loggen direkt im ctor machen zu lassen und die Exception "komplett durchzuwerfen", so dass das Progamm mit dem richtige Callstack abbricht. Das Speichern des Spielstandes müsste ich dann in den Destruktor einer geeigneten Klasse verschieben, der dank RAII aufgerufen wird; z.B. eine GameState-Klasse.

DeKugelschieber

Community-Fossil

Beiträge: 2 641

Wohnort: Rheda-Wiedenbrück

Beruf: Software-Entwickler

  • Private Nachricht senden

9

01.02.2015, 11:31

Jap so. Wo du eine Exception abfängst musst du natürlich entscheiden.

10

01.02.2015, 11:35

Ich habe jetzt parallel schon angefangen die ersten Code-Passagen zu modifizieren .. irgendwie liest es sich auch viel schöner :D

Werbeanzeige