Try-Catch oder Ifs zur Fehlerbehandlung in C ++

30

Werden im Game-Engine-Design häufig Ausnahmen verwendet, oder ist es besser, reine if-Anweisungen zu verwenden? Zum Beispiel mit Ausnahmen:

try {
    m_fpsTextId = m_statistics->createText( "FPS: 0", 16, 20, 20, 1.0f, 1.0f, 1.0f );
    m_cpuTextId = m_statistics->createText( "CPU: 0%", 16, 20, 40, 1.0f, 1.0f, 1.0f );
    m_frameTimeTextId = m_statistics->createText( "Frame time: 0", 20, 20, 60, 1.0f, 1.0f, 1.0f );
    m_mouseCoordTextId = m_statistics->createText( "Mouse: (0, 0)", 20, 20, 80, 1.0f, 1.0f, 1.0f );
    m_cameraPosTextId = m_statistics->createText( "Camera pos: (0, 0, 0)", 40, 20, 100, 1.0f, 1.0f, 1.0f );
    m_cameraRotTextId = m_statistics->createText( "Camera rot: (0, 0, 0)", 40, 20, 120, 1.0f, 1.0f, 1.0f );
} catch ... {
    // some info about error
}

und mit ifs:

m_fpsTextId = m_statistics->createText( "FPS: 0", 16, 20, 20, 1.0f, 1.0f, 1.0f );
if( m_fpsTextId == -1 ) {
    // show error
}
m_cpuTextId = m_statistics->createText( "CPU: 0%", 16, 20, 40, 1.0f, 1.0f, 1.0f );
if( m_cpuTextId == -1 ) {
    // show error
}
m_frameTimeTextId = m_statistics->createText( "Frame time: 0", 20, 20, 60, 1.0f, 1.0f, 1.0f );
if( m_frameTimeTextId == -1 ) {
    // show error
}
m_mouseCoordTextId = m_statistics->createText( "Mouse: (0, 0)", 20, 20, 80, 1.0f, 1.0f, 1.0f );
if( m_mouseCoordTextId == -1 ) {
    // show error
}
m_cameraPosTextId = m_statistics->createText( "Camera pos: (0, 0, 0)", 40, 20, 100, 1.0f, 1.0f, 1.0f );
if( m_cameraPosTextId == -1 ) {
    // show error
}
m_cameraRotTextId = m_statistics->createText( "Camera rot: (0, 0, 0)", 40, 20, 120, 1.0f, 1.0f, 1.0f );
if( m_cameraRotTextId == -1 ) {
    // show error
}

Ich habe gehört, dass die Ausnahmen etwas langsamer sind als ifs, und ich sollte Ausnahmen nicht mit der if-Prüfmethode mischen. Mit Ausnahmen habe ich jedoch nach jeder initialize () -Methode oder ähnlichem mehr lesbaren Code als mit Tonnen von ifs, obwohl sie meiner Meinung nach manchmal zu schwer für nur eine Methode sind. Sind sie in Ordnung für die Spieleentwicklung oder besser, um bei einfachen Wenns zu bleiben?

tobi
quelle
Viele Leute behaupten, Boost sei schlecht für die Spieleentwicklung, aber ich empfehle Ihnen, ["Boost.optional"] ( boost.org/doc/libs/1_52_0/libs/optional/doc/html/index.html ) anzusehen Ihr Wenns Beispiel ein wenig netter
ManicQin
Es gibt ein nettes Gespräch über die Fehlerbehandlung von Andrei Alexandrescu, ich habe ohne Ausnahmen eine ähnliche Herangehensweise an ihn gewählt.
Thelvyn

Antworten:

48

Kurze Antwort: Lesen Sie Sensible Error Handling 1 , Sensible Error Handling 2 und Sensible Error Handling 3 von Niklas Frykholm. Lesen Sie alle Artikel in diesem Blog, während Sie gerade dabei sind. Ich werde nicht sagen, dass ich mit allem einverstanden bin, aber das meiste davon ist Gold.

Verwenden Sie keine Ausnahmen. Es gibt eine Vielzahl von Gründen. Ich werde die wichtigsten auflisten.

Sie können in der Tat langsamer sein, obwohl dies auf neueren Compilern ziemlich minimiert ist. Einige Compiler unterstützen "Zero-Overhead-Ausnahmen" für Codepfade, die eigentlich keine Ausnahmen auslösen (obwohl dies eine kleine Lüge ist, da die Ausnahmebehandlung immer noch zusätzliche Daten erfordert, die die Größe Ihrer ausführbaren Datei / DLL aufblähen). Das Endergebnis ist jedoch, dass die Verwendung von Ausnahmen langsamer ist und dass Sie diese in allen leistungskritischen Codepfaden unbedingt vermeiden sollten. Abhängig von Ihrem Compiler kann die Aktivierung dieser Funktionen zusätzlichen Aufwand verursachen. Die Codegröße wird immer aufgebläht, was in vielen Fällen erheblich ist und die Leistung der heutigen Hardware erheblich beeinträchtigen kann.

Ausnahmen machen den Code viel zerbrechlicher. Es gibt eine berüchtigte Grafik (die ich derzeit leider nicht finden kann), die im Grunde genommen nur die Schwierigkeit zeigt, ausnahmesicheren Code im Vergleich zu ausnahmesicherem Code zu schreiben, und die erstere ist ein deutlich größerer Balken. Es gibt einfach viele kleine Fallstricke mit Ausnahmen und viele Möglichkeiten, Code dafür zu schreiben der aussieht Ausnahme sicher , aber wirklich nicht. Sogar das gesamte C ++ 11-Komitee hat diesen Fehler begangen und vergessen, wichtige Hilfsfunktionen für die korrekte Verwendung von std :: unique_ptr hinzuzufügen, und selbst bei diesen Hilfsfunktionen ist mehr Tipparbeit erforderlich, um sie zu verwenden, als nicht, und die meisten Programmierer haben gewonnen. ' Ich weiß nicht einmal, was los ist, wenn sie es nicht tun.

Insbesondere für die Spieleindustrie unterstützen die Compiler / Laufzeiten einiger Konsolen, die vom Hersteller bereitgestellt werden, Ausnahmen nicht oder überhaupt nicht. Wenn in Ihrem Code Ausnahmen verwendet werden, müssen Sie möglicherweise noch in der heutigen Zeit Teile Ihres Codes neu schreiben, um ihn auf neue Plattformen zu portieren. (Unsicher, ob sich dies in den 7 Jahren seit der Veröffentlichung der genannten Konsolen geändert hat. Wir verwenden nur keine Ausnahmen, haben sie sogar in den Compilereinstellungen deaktiviert, sodass ich nicht weiß, dass jemand, mit dem ich spreche, sie kürzlich überprüft hat.)

Die allgemeine Denkweise ist ziemlich klar: Verwenden Sie Ausnahmen für außergewöhnliche Umstände. Verwenden Sie sie, wenn in Ihrem Programm die Meldung "Ich habe keine Ahnung, was zu tun ist, hoffentlich jemand anderes. Ich werde also eine Ausnahme auslösen und sehen, was passiert." Verwenden Sie sie, wenn keine andere Option sinnvoll ist. Verwenden Sie sie, wenn es Ihnen egal ist, ob Sie versehentlich ein wenig Speicher verlieren oder eine Ressource nicht bereinigen, weil Sie die Verwendung geeigneter intelligenter Handles durcheinander gebracht haben. Verwenden Sie sie in allen anderen Fällen nicht.

In Bezug auf Code wie Ihr Beispiel haben Sie mehrere andere Möglichkeiten, um das Problem zu beheben. Eine der robusteren Methoden, die in Ihrem einfachen Beispiel nicht unbedingt die beste ist, ist die Tendenz zu monadischen Fehlertypen. Das heißt, createText () kann einen benutzerdefinierten Handle-Typ anstelle einer Ganzzahl zurückgeben. Dieser Handle-Typ verfügt über Accessoren zum Aktualisieren oder Steuern des Texts. Wenn das Handle in einen Fehlerzustand versetzt wird (weil createText () fehlgeschlagen ist), schlagen weitere Aufrufe des Handles einfach unbemerkt fehl. Sie können auch das Handle abfragen, um festzustellen, ob ein Fehler aufgetreten ist und wenn ja, wie der letzte Fehler lautete. Dieser Ansatz hat mehr Overhead als andere Optionen, ist aber ziemlich solide. Verwenden Sie es in Fällen, in denen Sie eine lange Reihe von Operationen in einem Kontext ausführen müssen, in dem eine einzelne Operation in der Produktion fehlschlagen könnte, Sie aber nicht / nicht / gewinnen können. '

Eine Alternative zur Implementierung der monadischen Fehlerbehandlung besteht darin, die Methoden des Kontextobjekts nicht mit benutzerdefinierten Handle-Objekten, sondern mit ungültigen Handle-IDs umzugehen. Wenn createText () beispielsweise -1 zurückgibt, wenn dies fehlschlägt, sollten alle anderen Aufrufe von m_statistics, die eines dieser Handles verwenden, ordnungsgemäß beendet werden, wenn -1 übergeben wird.

Sie können den Fehlerdruck auch in die Funktion einfügen, die tatsächlich fehlschlägt. In Ihrem Beispiel hat createText () wahrscheinlich viel mehr Informationen darüber, was schief gelaufen ist, sodass ein aussagekräftigerer Fehler in das Protokoll geschrieben werden kann. In diesem Fall ist es wenig vorteilhaft, die Fehlerbehandlung / den Ausdruck an die Anrufer weiterzuleiten. Tun Sie dies, wenn die Anrufer die Behandlung anpassen müssen (oder die Abhängigkeitsinjektion verwenden müssen). Beachten Sie, dass eine In-Game-Konsole, die angezeigt wird, wenn ein Fehler protokolliert wird, eine gute Idee ist und auch hier Abhilfe schafft.

Die beste Option (bereits in der verknüpften Artikelserie oben dargestellt) für Anrufe, von denen Sie nicht erwarten, dass sie in einer vernünftigen Umgebung fehlschlagen - wie das einfache Erstellen von Text-Blobs für ein Statistiksystem - besteht darin, nur die Funktion zu haben Das ist fehlgeschlagen (createText in Ihrem Beispiel). Sie können sich ziemlich sicher sein, dass createText () in der Produktion nicht fehlschlägt, es sei denn, etwas wurde vollständig gelöscht (z. B. der Benutzer hat Schriftartendatendateien gelöscht oder hat aus irgendeinem Grund nur 256 MB Speicher usw.). In vielen dieser Fälle kann man nicht einmal etwas Gutes tun, wenn ein Fehler auftritt. Zu wenig Speicher? Möglicherweise können Sie nicht einmal eine Zuordnung vornehmen, um ein ansprechendes GUI-Panel zu erstellen, das dem Benutzer den OOM-Fehler anzeigt. Fehlende Schriften? Erschwert die Anzeige von Fehlern für den Benutzer. Was auch immer falsch ist,

Nur Abstürze sind völlig in Ordnung, solange Sie (a) den Fehler in einer Protokolldatei protokollieren und (b) nur Fehler beheben, die nicht durch regelmäßige Benutzeraktionen verursacht werden.

Für viele Server-Apps, bei denen die Verfügbarkeit von entscheidender Bedeutung ist und die Überwachung des Watchdogs nicht ausreicht, würde ich nicht dasselbe sagen, aber das ist etwas ganz anderes als bei der Entwicklung von Spieleclients . Ich möchte Sie auch nachdrücklich davon abhalten, C / C ++ zu verwenden, da die Ausnahmebehandlungsfunktionen anderer Sprachen in der Regel nicht wie C ++ funktionieren, da es sich um verwaltete Umgebungen handelt, die nicht alle Ausnahmesicherheitsprobleme von C ++ aufweisen. Performance-Bedenken werden gemindert, da sich Server in der Regel mehr auf Parallelität und Durchsatz konzentrieren als auf Garantien mit minimaler Latenz wie bei den Spieleclients. Sogar Action-Game-Server für Schützen und Ähnliches können recht gut funktionieren, wenn sie beispielsweise in C # geschrieben sind, da sie die Hardware selten an ihre Grenzen bringen, wie es FPS-Clients tun.

Sean Middleditch
quelle
1
+1. Zusätzlich gibt es warn_unused_resultin einigen Compilern die Möglichkeit, atribute-like- Funktionen hinzuzufügen , die das Abfangen von nicht behandeltem Fehlercode ermöglichen.
Maciej Piechotka
Kam hierher und sah C ++ im Titel und war absolut sicher, dass monadische Fehlerbehandlung nicht schon hier wäre. Gutes Zeug!
Adam
Was ist mit Orten, an denen die Leistung nicht kritisch ist und bei denen Sie hauptsächlich eine schnelle Implementierung anstreben? Zum Beispiel, wenn Sie Spielelogik implementieren?
Ali1S232
Was ist auch mit der Idee, die Ausnahmebehandlung nur zum Debuggen zu verwenden? Wenn Sie das Spiel veröffentlichen, erwarten Sie, dass alles reibungslos verläuft. Sie verwenden Ausnahmen, um Fehler während der Entwicklung zu finden und zu beheben, und entfernen später im Veröffentlichungsmodus alle diese Ausnahmen.
Ali1S232
1
@ Gajoo: Bei der Spielelogik erschweren Ausnahmen lediglich die Verfolgung der Logik (da der gesamte Code dadurch schwerer zu befolgen ist). Selbst in Pythnn und C # sind Ausnahmen für die Spielelogik meiner Erfahrung nach selten. Für das Debuggen ist die Hard-Assert-Methode in der Regel weitaus praktischer. Brechen Sie und stoppen Sie genau in dem Moment, in dem etwas schief geht, nicht nachdem große Teile des Stapels abgewickelt wurden und alle Arten von Kontextinformationen aufgrund einer Semantik, die Ausnahmen wirft, verloren gegangen sind. Für die Debugging-Logik möchten Sie Tools erstellen und GUIs für Informationen und Bearbeitungen erstellen, damit Designer sie überprüfen und optimieren können.
Sean Middleditch
13

Die alte Weisheit von Donald Knuth selbst ist:

"Wir sollten kleine Ineffizienzen vergessen, sagen wir in 97% der Fälle: Vorzeitige Optimierung ist die Wurzel allen Übels."

Drucken Sie dieses als großes Poster aus und hängen Sie es überall dort auf, wo Sie ernsthaft programmieren.

Also auch wenn try / catch etwas langsamer ist, würde ich es standardmäßig verwenden:

  • Der Code muss so lesbar und verständlich wie möglich sein. Sie könnten den Code einmal schreiben, aber Sie werden ihn noch mehrmals lesen, um diesen Code zu debuggen, anderen Code zu debuggen, zu verstehen, zu verbessern, ...

  • Richtigkeit über Leistung. Das if / else-Zeug richtig zu machen, ist nicht trivial. In Ihrem Beispiel wird das falsch gemacht, weil nicht ein Fehler angezeigt werden kann, sondern mehrere. Sie müssen kaskadiert verwenden, wenn / then / else.

  • Einfacher durchgängig zu verwenden: Try / catch umfasst einen Stil, bei dem nicht jede Zeile auf Fehler überprüft werden muss. Auf der anderen Seite: Ein fehlendes if / else und Ihr Code könnten das Chaos zunichte machen. Natürlich nur unter nicht reproduzierbaren Umständen.

Also der letzte Punkt:

Ich habe gehört, dass die Ausnahmen etwas langsamer sind als wenns

Ich habe das vor ungefähr 15 Jahren gehört, habe das in letzter Zeit nicht aus glaubwürdigen Quellen gehört. Compiler haben sich möglicherweise verbessert oder was auch immer.

Der Hauptpunkt ist: Keine vorzeitige Optimierung. Tun Sie dies nur, wenn Sie anhand eines Benchmarks nachweisen können, dass der vorliegende Code die enge innere Schleife eines häufig verwendeten Codes ist und dass der Schaltstil die Leistung erheblich verbessert . Dies ist der 3% -Fall.

AH
quelle
22
Ich werde dies bis unendlich -1. Ich kann mir nicht vorstellen, welchen Schaden dieses Knuth-Zitat den Entwicklern zugefügt hat. Wenn man bedenkt, ob Ausnahmen verwendet werden sollen, ist dies keine vorzeitige Optimierung. Es handelt sich um eine Entwurfsentscheidung, eine wichtige Entwurfsentscheidung, deren Auswirkungen weit über die Leistung hinausgehen.
Sam Hocevar
3
@SamHocevar: Es tut mir leid, ich verstehe nicht Ihren Standpunkt: Die Frage bezieht sich auf den möglichen Leistungseinbruch bei der Verwendung von Ausnahmen. Mein Punkt: Denken Sie nicht darüber nach, der Treffer ist nicht so schlimm (wenn überhaupt) und andere Dinge sind viel wichtiger. Hier scheinen Sie zuzustimmen, indem Sie "Entwurfsentscheidung" zu meiner Liste hinzufügen. OKAY. Andererseits sagen Sie, dass Knuths Zitat schlecht ist, was bedeutet, dass vorzeitige Optimierung nicht schlecht ist. Aber genau das ist hier passiert. IMO: Der Q denkt nicht an Architektur, Design oder andere Algorithmen, sondern nur an Ausnahmen und deren Auswirkungen auf die Leistung.
AH
2
Ich würde der Klarheit in C ++ nicht zustimmen. In C ++ wurden Ausnahmen deaktiviert, während einige Compiler überprüfte Rückgabewerte implementieren. Als Ergebnis haben Sie Code, der klarer aussieht, aber möglicherweise einen verborgenen Speicherverlust aufweist oder den Code sogar in einen undefinierten Zustand versetzt. Auch Code von Drittanbietern ist möglicherweise nicht ausnahmesicher. (Die Situation ist in Java / C # anders, wo Ausnahmen, GC, ... geprüft wurden.) Ab dem Zeitpunkt der Designentscheidung - wenn sie keine API-Punkte überschreiten, kann das Refactoring von / zu jedem Stil mit Perl-Einzeilern halbautomatisch durchgeführt werden.
Maciej Piechotka
1
@SamHocevar: "Bei der Frage geht es darum, was besser ist, nicht was schneller. " Lesen Sie den letzten Absatz der Frage noch einmal durch. Der einzige Grund, warum er darüber nachdenkt, keine Ausnahmen zu verwenden, ist, dass er glaubt, sie könnten langsamer sein. Wenn Sie jetzt der Meinung sind, dass es andere Bedenken gibt, die das OP nicht berücksichtigt hat, können Sie diese veröffentlichen oder diejenigen, die dies getan haben, positiv bewerten. Das OP konzentriert sich jedoch ganz klar auf die Durchführung von Ausnahmen.
Nicol Bolas
3
@AH - wenn Sie Knuth zitieren wollen, dann vergessen Sie den Rest nicht (und IMO den wichtigsten Teil): " Wir sollten unsere Chancen in diesen kritischen 3% jedoch nicht verpassen . Ein guter Programmierer wird es nicht sein Er wird durch solche Überlegungen zur Selbstzufriedenheit gebracht, und es wird ratsam sein, sich den kritischen Code genau anzuschauen, aber erst, nachdem dieser Code identifiziert wurde . " Allzu oft wird dies als eine Entschuldigung dafür angesehen, überhaupt nicht zu optimieren, oder zu versuchen, zu rechtfertigen, dass Code langsam ist, wenn Leistung keine Optimierung ist, sondern eine grundlegende Anforderung darstellt.
Maximus Minimus
10

Es gab bereits eine wirklich gute Antwort auf diese Frage, aber ich möchte einige Gedanken zur Codelesbarkeit und Fehlerbehebung in Ihrem speziellen Fall hinzufügen.

Ich glaube, Ihr Code könnte tatsächlich so aussehen:

m_fpsTextId = m_statistics->createText( "FPS: 0", 16, 20, 20, 1.0f, 1.0f, 1.0f );
m_cpuTextId = m_statistics->createText( "CPU: 0%", 16, 20, 40, 1.0f, 1.0f, 1.0f );
m_frameTimeTextId = m_statistics->createText( "Frame time: 0", 20, 20, 60, 1.0f, 1.0f, 1.0f );
m_mouseCoordTextId = m_statistics->createText( "Mouse: (0, 0)", 20, 20, 80, 1.0f, 1.0f, 1.0f );
m_cameraPosTextId = m_statistics->createText( "Camera pos: (0, 0, 0)", 40, 20, 100, 1.0f, 1.0f, 1.0f );
m_cameraRotTextId = m_statistics->createText( "Camera rot: (0, 0, 0)", 40, 20, 120, 1.0f, 1.0f, 1.0f );

Keine Ausnahmen, kein Wenn. Die Fehlermeldung kann in erfolgen createText. Und createTextkann eine Standardtextur-ID zurückgeben, bei der Sie den Rückgabewert nicht überprüfen müssen, sodass der Rest des Codes genauso gut funktioniert.

sam hocevar
quelle