Für mich kann die Ausnahmebehandlung großartig sein, wenn alles RAII-konform ist und Sie Ausnahmen für wirklich außergewöhnliche Pfade reservieren (z. B. eine beschädigte Datei, die gelesen wird), und Sie niemals Modulgrenzen überschreiten müssen und Ihr gesamtes Team an Bord ist ......
Zero-Cost EH
Die meisten Compiler implementieren heutzutage eine kostengünstige Ausnahmebehandlung, die es sogar billiger machen kann als die manuelle Verzweigung unter Fehlerbedingungen in regulären Ausführungspfaden, obwohl im Gegenzug außergewöhnliche Pfade enorm teuer werden (es gibt auch einige Code-Aufblähungen, wenn auch wahrscheinlich nicht mehr als wenn Sie jeden möglichen Fehler gründlich manuell behandelt hätten). Die Tatsache, dass das Werfen enorm teuer ist, sollte jedoch keine Rolle spielen, wenn Ausnahmen so verwendet werden, wie sie in C ++ vorgesehen sind (für wirklich außergewöhnliche Umstände, die unter normalen Bedingungen nicht auftreten sollten).
Umkehrung von Nebenwirkungen
Trotzdem ist es viel einfacher gesagt als getan, alles an RAII anzupassen. Es ist für lokale Ressourcen, die in einer Funktion gespeichert sind, einfach, sie in C ++ - Objekte mit Destruktoren zu verpacken, die sich selbst bereinigen, aber es ist nicht so einfach, eine Rückgängig- / Rollback-Logik für jeden einzelnen Nebeneffekt zu schreiben, der in der gesamten Software auftreten kann. Überlegen Sie nur, wie schwierig es ist, einen Container zu schreiben, std::vector
der die einfachste Sequenz mit wahlfreiem Zugriff darstellt, um absolut ausnahmesicher zu sein. Multiplizieren Sie diese Schwierigkeit nun über alle Datenstrukturen einer gesamten großen Software.
Betrachten Sie als grundlegendes Beispiel eine Ausnahme, die beim Einfügen von Elementen in ein Szenendiagramm auftritt. Um diese Ausnahme ordnungsgemäß zu beheben, müssen Sie möglicherweise diese Änderungen im Szenendiagramm rückgängig machen und das System wieder in einen Zustand versetzen, in dem der Vorgang nie stattgefunden hat. Das bedeutet, dass die in das Szenendiagramm eingefügten untergeordneten Elemente automatisch entfernt werden, wenn eine Ausnahme auftritt, möglicherweise von einem Scope Guard.
Dies gründlich in einer Software zu tun, die viele Nebenwirkungen verursacht und mit vielen dauerhaften Zuständen umgeht, ist viel einfacher gesagt als getan. Oft denke ich, dass die pragmatische Lösung darin besteht, sich nicht darum zu kümmern, um die Dinge zu versenden. Mit der richtigen Art von Codebasis, die über ein zentrales Rückgängig-System verfügt, und möglicherweise dauerhaften Datenstrukturen und der minimalen Anzahl von Funktionen, die Nebenwirkungen verursachen, können Sie möglicherweise auf ganzer Linie Ausnahmesicherheit erreichen ... aber es ist eine Menge Dinge, die Sie brauchen, wenn Sie nur versuchen wollen, Ihren Code überall ausnahmesicher zu machen.
Dort stimmt praktisch etwas nicht, da die konzeptionelle Umkehrung von Nebenwirkungen ein schwieriges Problem darstellt, das in C ++ jedoch schwieriger wird. Manchmal ist es tatsächlich einfacher, Nebenwirkungen in C als Reaktion auf einen Fehlercode umzukehren, als als Reaktion auf eine Ausnahme in C ++. Einer der Gründe ist, dass jeder Punkt einer Funktion möglicherweise throw
in C ++ vorhanden sein kann. Wenn Sie versuchen, einen generischen Container zu schreiben, der mit type arbeitet T
, können Sie nicht einmal vorhersehen, wo sich diese Austrittspunkte befinden, da alles, T
was dazu gehört, werfen könnte. Sogar ein Vergleich T
untereinander für Gleichheit könnte werfen. Ihr Code muss alle Möglichkeiten bewältigen , und die Anzahl der Möglichkeiten multipliziert sich in C ++ exponentiell, während sie in C viel weniger sind.
Fehlende Standards
Ein weiteres Problem der Ausnahmebehandlung ist das Fehlen zentraler Standards. Es gibt einige, denen hoffentlich alle zustimmen, dass Destruktoren niemals werfen sollten, da Rollbacks niemals fehlschlagen sollten, aber das deckt nur einen pathologischen Fall ab, der die Software im Allgemeinen zum Absturz bringen würde, wenn Sie gegen die Regel verstoßen.
Es sollte vernünftigere Standards geben, wie niemals von einem Vergleichsoperator werfen (alle Funktionen, die logisch unveränderlich sind, sollten niemals werfen), niemals von einem Verschiebungs-ctor werfen usw. Solche Garantien würden es so viel einfacher machen, ausnahmesicheren Code zu schreiben. Wir haben jedoch keine solchen Garantien. Wir müssen uns an die Regel halten, dass alles werfen könnte - wenn nicht jetzt, dann möglicherweise in der Zukunft.
Schlimmer noch, Menschen mit unterschiedlichem Sprachhintergrund betrachten Ausnahmen sehr unterschiedlich. In Python haben sie tatsächlich diese "Sprung vor dem Blick" -Philosophie, bei der Ausnahmen in regulären Ausführungspfaden ausgelöst werden! Wenn Sie dann solche Entwickler dazu bringen, C ++ - Code zu schreiben, könnten Sie nach Ausnahmen suchen, die für Dinge, die nicht wirklich außergewöhnlich sind, nach links und rechts geworfen werden, und alles zu handhaben kann ein Albtraum sein, wenn Sie versuchen, generischen Code zu schreiben, z
Fehlerbehandlung
Die Fehlerbehandlung hat den Nachteil, dass Sie nach jedem möglichen Fehler suchen müssen, der auftreten könnte. Es hat jedoch den Vorteil, dass Sie im Nachhinein manchmal etwas spät nach Fehlern suchen können glGetError
, um beispielsweise die Austrittspunkte in einer Funktion erheblich zu reduzieren und sie explizit zu machen. Manchmal können Sie den Code etwas länger ausführen, bevor Sie einen Fehler überprüfen, weitergeben und beheben, und manchmal ist dies wirklich einfacher als die Behandlung von Ausnahmen (insbesondere bei Umkehrung von Nebenwirkungen). Aber vielleicht beschäftigen Sie sich auch nicht so sehr mit Fehlern in einem Spiel, sondern schließen das Spiel möglicherweise nur mit einer Meldung, wenn Sie auf beschädigte Dateien, Speicherfehler usw. stoßen.
Wie es sich um die Entwicklung einer Bibliothek handelt, die in mehreren Projekten verwendet wird.
Ein unangenehmer Teil von Ausnahmen in DLL-Kontexten ist, dass Sie Ausnahmen nicht sicher von einem Modul auf ein anderes werfen können, es sei denn, Sie können garantieren, dass sie von demselben Compiler erstellt werden, dieselben Einstellungen verwenden usw. Wenn Sie also wie eine Plugin-Architektur schreiben Ausnahmen, die für Benutzer aller Arten von Compilern und möglicherweise sogar aus verschiedenen Sprachen wie Lua, C, C #, Java usw. verwendet werden sollen, werden häufig zu einem großen Ärgernis, da Sie jede Ausnahme verschlucken und in Fehler übersetzen müssen Codes sowieso überall.
Was bedeutet es, Bibliotheken von Drittanbietern zu verwenden?
Wenn es sich um Dylibs handelt, können sie aus den oben genannten Gründen keine Ausnahmen sicher auslösen. Sie müssten Fehlercodes verwenden, was auch bedeutet, dass Sie bei Verwendung der Bibliothek ständig nach Fehlercodes suchen müssen. Sie könnten ihre Dylib mit einer statisch verknüpften C ++ - Wrapper-Bibliothek umschließen, die Sie selbst erstellen und die Fehlercodes aus der Dylib in ausgelöste Ausnahmen übersetzt (im Grunde genommen aus Ihrer Binärdatei in Ihre Binärdatei). Im Allgemeinen denke ich, dass die meisten Bibliotheken von Drittanbietern sich nicht einmal darum kümmern sollten und sich nur an Fehlercodes halten sollten, wenn sie Dylibs / Shared Libs verwenden.
Wie wirkt es sich auf Unit-Tests, Integrationstests usw. aus?
Ich treffe im Allgemeinen nicht so oft auf Teams, die außergewöhnliche / fehlerhafte Pfade ihres Codes testen. Früher habe ich es getan und hatte tatsächlich Code, von dem bad_alloc
ich mich richtig erholen konnte, weil ich meinen Code extrem robust machen wollte, aber es hilft nicht wirklich, wenn ich der einzige bin, der es in einem Team tut. Am Ende hörte ich auf zu stören. Ich stelle mir für eine unternehmenskritische Software vor, dass ganze Teams prüfen könnten, ob ihr Code in allen möglichen Fällen zuverlässig von Ausnahmen und Fehlern wiederhergestellt wird, aber das ist viel Zeit für Investitionen. Die Zeit macht in unternehmenskritischer Software Sinn, aber vielleicht kein Spiel.
Hassliebe
Ausnahmen sind auch meine Art von Liebes- / Hass-Funktion von C ++ - eine der wirkungsvollsten Funktionen der Sprache, die RAII eher zu einer Anforderung als zu einer Annehmlichkeit macht, da es die Art und Weise ändert, wie Sie Code verkehrt herum schreiben müssen , C, um die Tatsache zu behandeln, dass jede gegebene Codezeile ein impliziter Austrittspunkt für eine Funktion sein könnte. Manchmal wünschte ich mir fast, die Sprache hätte keine Ausnahmen. Manchmal finde ich Ausnahmen wirklich nützlich, aber sie sind für mich ein zu dominierender Faktor, wenn ich einen Code in C oder C ++ schreibe.
Sie wären für mich exponentiell nützlicher, wenn sich das Standardkomitee wirklich stark auf die Festlegung von ABI-Standards konzentrieren würde, die es ermöglichen, Ausnahmen sicher über Module hinweg zu werfen, zumindest von C ++ bis C ++ - Code. Es wäre großartig, wenn Sie sicher über Sprachen hinwegwerfen könnten, obwohl das eine Art Wunschtraum ist. Die andere Sache, die Ausnahmen für mich exponentiell nützlicher machen würde, ist, wenn noexcept
Funktionen tatsächlich einen Compilerfehler verursachten, wenn sie versuchten, Funktionen aufzurufen, die ausgelöst werden könnten, anstatt nur std::terminate
auf eine Ausnahme zu stoßen. Das ist so gut wie nutzlos (könnte das genauso gut nennen, icantexcept
anstatt tatsächlich einen Compilerfehler zu verursachen, wenn der Destruktor irgendetwas getan hat, das werfen könnte. Wenn das zu teuer ist, um es zur Kompilierungszeit gründlich zu bestimmen, dann machen Sie es einfach sonoexcept
). Es wäre viel einfacher sicherzustellen, dass alle Destruktoren ausnahmesicher sind, zum Beispiel, wennnoexcept
noexcept
Funktionen (die alle Destruktoren implizit sein sollten) dürfen nur ebenfalls noexcept
Funktionen / Operatoren aufrufen und es zu einem Compilerfehler machen, von einem von ihnen zu werfen.