Warum sind Ausnahmespezifikationen schlecht?

50

Vor mehr als 10 Jahren haben sie Ihnen beigebracht, Ausnahmespezifizierer zu verwenden. Da ich einer der torvaldischen C-Programmierer bin, der C ++ hartnäckig meidet, wenn ich nicht dazu gezwungen werde, lande ich nur sporadisch in C ++.

Die Mehrheit der C ++ - Programmierer scheint sich jedoch über Ausnahmespezifizierer hinwegzusetzen. Ich habe die Debatte und die Argumente von verschiedenen C ++ - Gurus wie diesen gelesen . Soweit ich es verstehe, läuft es auf drei Dinge hinaus:

  1. Ausnahmespezifizierer verwenden ein Typsystem, das nicht mit dem Rest der Sprache übereinstimmt ("Schattentypsystem").
  2. Wenn Ihre Funktion mit einem Ausnahmespezifizierer etwas anderes auslöst als das, was Sie angegeben haben, wird das Programm auf unerwartete Weise abgebrochen.
  3. Ausnahmespezifizierer werden im kommenden C ++ - Standard entfernt.

Vermisse ich hier etwas oder sind das alle Gründe?

Meine eigenen Meinungen:

Zu 1): Na und? C ++ ist in Bezug auf die Syntax wahrscheinlich die inkonsistenteste Programmiersprache, die jemals erstellt wurde. Wir haben die Makros, die goto / labels, die Horde (hoard?) Von undefiniertem / nicht spezifiziertem / implementierungsdefiniertem Verhalten, die schlecht definierten Integer-Typen, alle impliziten Regeln für die Typwerbung, Schlüsselwörter in Sonderfällen wie friend, auto , registrieren, explizit ... und so weiter. Jemand könnte wahrscheinlich mehrere dicke Bücher von all der Verrücktheit in C / C ++ schreiben. Warum reagieren die Menschen also auf diese besondere Inkonsistenz, die im Vergleich zu vielen anderen weitaus gefährlicheren Merkmalen der Sprache ein kleinerer Fehler ist?

Zu 2): Ist das nicht meine eigene Verantwortung? Es gibt so viele andere Möglichkeiten, wie ich einen fatalen Fehler in C ++ schreiben kann. Warum ist dieser spezielle Fall noch schlimmer? Anstatt throw(int)Crash_t zu schreiben und dann zu werfen, kann ich auch behaupten, dass meine Funktion einen Zeiger auf int zurückgibt, dann eine wilde, explizite Typumwandlung durchführt und einen Zeiger auf einen Crash_t zurückgibt. Der Geist von C / C ++ war es immer, den größten Teil der Verantwortung dem Programmierer zu überlassen.

Was ist dann mit Vorteilen? Am offensichtlichsten ist, dass der Compiler einen Fehler ausgibt, wenn Ihre Funktion versucht, einen anderen als den von Ihnen angegebenen Typ explizit auszulösen. Ich glaube, dass der Standard diesbezüglich klar ist (?). Fehler treten nur auf, wenn Ihre Funktion andere Funktionen aufruft, die wiederum den falschen Typ auslösen.

Aus einer Welt deterministischer, eingebetteter C-Programme kommend, würde ich mit Sicherheit lieber genau wissen, was eine Funktion auf mich wirft. Wenn es etwas in der Sprache gibt, das das unterstützt, warum nicht? Die Alternativen scheinen zu sein:

void func() throw(Egg_t);

und

void func(); // This function throws an Egg_t

Ich denke, es besteht eine große Chance, dass der Anrufer den Try-Catch im zweiten Fall ignoriert / vergisst, weniger im ersten Fall.

Soweit ich weiß, stürzt das Programm ab, wenn eine dieser beiden Formen plötzlich eine andere Art von Ausnahme auslöst. Im ersten Fall, weil es nicht erlaubt ist, eine weitere Ausnahme auszulösen, im zweiten Fall, weil niemand damit gerechnet hat, dass ein SpanishInquisition_t ausgelöst wird und daher dieser Ausdruck nicht dort abgefangen wird, wo er hätte sein sollen.

Im letzteren Fall scheint es nicht wirklich besser zu sein, als einen Programmabsturz (...) auf der höchsten Ebene des Programms zu haben: "Hey, irgendwo in Ihrem Programm hat etwas eine seltsame, unbehandelte Ausnahme ausgelöst ". Sie können das Programm nicht wiederherstellen, wenn Sie so weit von der Stelle entfernt sind, an der die Ausnahme ausgelöst wurde. Sie können lediglich das Programm beenden.

Und aus der Sicht des Benutzers ist es ihnen egal, ob sie vom Betriebssystem ein böses Meldungsfeld mit der Aufschrift "Programm beendet. Blablabla bei Adresse 0x12345" oder ein böses Meldungsfeld von Ihrem Programm mit der Aufschrift "Unbehandelte Ausnahme: myclass" erhalten. func.something ". Der Bug ist immer noch da.


Mit dem kommenden C ++ - Standard habe ich keine andere Möglichkeit, als auf Ausnahmespezifizierer zu verzichten. Aber ich würde lieber ein solides Argument hören, warum sie schlecht sind, als "Seine Heiligkeit hat es gesagt und so ist es". Vielleicht gibt es mehr Argumente gegen sie als die, die ich aufgelistet habe, oder vielleicht steckt mehr dahinter, als mir klar wird?


quelle
30
Ich bin versucht, dies als "schimpfen, als Frage verkleidet" abzustimmen. Sie fragen nach drei gültigen Punkten zu E.specs, aber warum stören Sie uns mit Ihrem C ++ - ist es so ärgerlich und ich mag C-besser?
Martin Ba
10
@Martin: Weil ich darauf hinweisen möchte, dass ich sowohl voreingenommen als auch nicht mit allen Details auf dem Laufenden bin, sondern auch die Sprache mit naiven und / oder noch nicht ruinierten Augen betrachte. Aber auch, dass C und C ++ bereits unglaublich fehlerhafte Sprachen sind, so dass ein Fehler mehr oder weniger keine Rolle spielt. Der Beitrag war tatsächlich viel schlimmer, bevor ich ihn bearbeitet habe :) Die Argumente gegen Ausnahmespezifizierer sind auch ziemlich subjektiv und daher ist es schwierig, sie zu diskutieren, ohne selbst subjektiv zu werden.
SpanishInquisition_t! Urkomisch! Ich persönlich war beeindruckt von der Verwendung des Stack-Frame-Zeigers, um Ausnahmen auszulösen, und es scheint, dass dies den Code viel sauberer machen kann. Ich habe jedoch nie wirklich Code mit Ausnahmen geschrieben. Nennen Sie mich altmodisch, aber die Rückgabewerte funktionieren gut für mich.
Shahbaz
@ Shahbaz Wie man zwischen den Zeilen lesen kann, bin ich auch ziemlich altmodisch, aber trotzdem habe ich mich nie wirklich gefragt, ob Ausnahmen für sich genommen gut oder schlecht sind.
2
Die Vergangenheitsform von Wurf ist warf , nicht throwed . Es ist ein starkes Verb.
TRiG

Antworten:

48

Ausnahmebedingungsspezifikationen sind schlecht, weil sie schwach durchgesetzt werden und daher nicht viel bewirken. Sie sind auch schlecht, weil sie die Laufzeit auf unerwartete Ausnahmen prüfen lassen, damit sie terminate () ausführen können, anstatt UB aufzurufen Dies kann eine erhebliche Menge an Leistung verschwenden.

Zusammenfassend lässt sich sagen, dass Ausnahmespezifikationen in der Sprache nicht stark genug erzwungen werden, um den Code tatsächlich sicherer zu machen, und dass die Implementierung wie angegeben einen großen Leistungsverlust darstellte.

DeadMG
quelle
2
Die Laufzeitprüfung ist jedoch nicht die Schuld der Ausnahmespezifikation als solcher, sondern eines Teils des Standards, der besagt, dass eine Beendigung erfolgen soll, wenn der falsche Typ ausgelöst wird. Wäre es nicht klüger, einfach die Anforderung zu entfernen, die zu dieser dynamischen Prüfung führt, und alles vom Compiler statisch auswerten zu lassen? Es hört sich so an, als ob RTTI der wahre Schuldige ist, oder ...?
7
@Lundin: Es ist nicht möglich, alle Ausnahmen, die von einer Funktion in C ++ ausgelöst werden könnten, statisch zu bestimmen, da die Sprache willkürliche und nicht deterministische Änderungen des Kontrollflusses zur Laufzeit zulässt (z. B. über Funktionszeiger, virtuelle Methoden und dergleichen). Die Implementierung einer statischen Ausnahmebedingungsüberprüfung zur Kompilierungszeit wie Java würde grundlegende Änderungen an der Sprache und die Deaktivierung vieler C-Kompatibilitäten erfordern.
3
@OrbWeaver: Ich denke, statische Überprüfungen sind durchaus möglich - die Ausnahmespezifikation wäre Teil der Spezifikation einer Funktion (wie der Rückgabewert), und Funktionszeiger, virtuelle Überschreibungen usw. müssten kompatible Spezifikationen haben. Der wichtigste Einwand wäre, dass es sich um eine Alles-oder-Nichts-Funktion handelt - Funktionen mit statischen Ausnahmespezifikationen konnten keine Legacy-Funktionen aufrufen. (Aufrufen von C-Funktionen wäre in Ordnung, da sie nichts werfen können).
Mike Seymour
2
@Lundin: Ausnahmespezifikationen wurden nicht entfernt, sondern sind nur veraltet. Legacy-Code, der sie verwendet, ist weiterhin gültig.
Mike Seymour
1
@Lundin: Alte Versionen von C ++ mit Ausnahme von Spezifikationen sind perfekt kompatibel. Sie werden weder unvorhergesehen kompiliert noch ausgeführt, wenn der Code zuvor gültig war.
DeadMG
18

Ein Grund, warum niemand sie benutzt, ist, dass Ihre Hauptannahme falsch ist:

"Der offensichtlichste Vorteil ist, dass der Compiler einen Fehler ausgibt, wenn Ihre Funktion versucht, einen anderen als den von Ihnen angegebenen Typ explizit auszulösen."

struct foo {};
struct bar {};

struct test
{
    void baz() throw(foo)
    {
        throw bar();
    }
};

int main()
{
    test x;
    try { x.baz(); } catch(bar &b) {}
}

Dieses Programm kompiliert ohne Fehler oder Warnungen .

Darüber hinaus wird das Programm beendet , obwohl die Ausnahme abgefangen worden wäre .


Edit: ein Punkt in Ihrer Frage zu beantworten, catch (...) (oder besser, catch (std :: exeption &) oder eine andere Basisklasse, und dann fangen (...)) ist sinnvoll , noch , auch wenn Sie dies nicht tun weiß genau was schief gelaufen ist.

Bedenken Sie, dass Ihr Benutzer eine Menüschaltfläche für "Speichern" gedrückt hat. Der Handler für die Menüschaltfläche lädt die Anwendung zum Speichern ein. Dies könnte aus unzähligen Gründen fehlschlagen: Die Datei befand sich in einer verschwundenen Netzwerkressource, es gab eine schreibgeschützte Datei und sie konnte nicht überschrieben werden usw. Dem Handler ist es jedoch egal, warum etwas fehlgeschlagen ist. Es ist nur wichtig, dass es entweder erfolgreich war oder fehlgeschlagen ist. Wenn es gelungen ist, ist alles gut. Wenn es fehlgeschlagen ist, kann es dem Benutzer mitteilen. Die genaue Art der Ausnahme ist unerheblich. Wenn Sie richtigen, ausnahmesicheren Code schreiben, bedeutet dies außerdem, dass sich ein solcher Fehler in Ihrem System ausbreiten kann, ohne dass er heruntergefahren wird - selbst bei Code, der den Fehler nicht berücksichtigt. Dies erleichtert die Erweiterung des Systems. Beispielsweise speichern Sie jetzt über eine Datenbankverbindung.

Kaz Dragon
quelle
4
@Lundin hat es ohne Warnungen kompiliert . Und nein, das kannst du nicht. Nicht zuverlässig. Sie können über Funktionszeiger aufrufen oder es kann sich um eine virtuelle Memberfunktion handeln, und die abgeleitete Klasse löst derivative_exception aus (von der die Basisklasse möglicherweise nichts wissen kann).
Kaz Dragon
Pech. Embarcadero / Borland gibt dazu eine Warnung aus: "Throw Exception verletzt Exception Specifier". Anscheinend nicht GCC. Es gibt jedoch keinen Grund, warum sie keine Warnung implementieren könnten. In ähnlicher Weise könnte ein statisches Analysewerkzeug diesen Fehler leicht finden.
14
@Lundin: Bitte fangen Sie nicht an, falsche Informationen zu diskutieren und zu wiederholen. Ihre Frage war schon eine Schande, und falsche Dinge zu behaupten, hilft nicht weiter. Ein statisches Analysetool KANN NICHT feststellen, ob dies geschieht. Dazu müsste das Halteproblem gelöst werden. Außerdem müsste das gesamte Programm analysiert werden, und sehr oft ist nicht die gesamte Quelle verfügbar.
David Thornley
1
@Lundin dasselbe statische Analysetool könnte Ihnen sagen, welche ausgelösten Ausnahmen Ihren Stack verlassen. In diesem Fall erhalten Sie mit Ausnahme der Ausnahmespezifikationen nichts außer einem möglichen falsch-negativen Ergebnis, das Ihr Programm zum Absturz bringt, in dem Sie den Fehlerfall hätten behandeln können Eine viel schönere Möglichkeit (z. B. das Ankündigen des Fehlers und die Fortsetzung der Ausführung: Ein Beispiel finden Sie in der zweiten Hälfte meiner Antwort).
Kaz Dragon
1
@Lundin Eine gute Frage. Die Antwort ist, dass Sie im Allgemeinen nicht wissen, ob die von Ihnen aufgerufenen Funktionen Ausnahmen auslösen. Die sichere Annahme ist also, dass alle dies tun. Wo man diese fängt, ist eine Frage, die ich in stackoverflow.com/questions/4025667/… beantwortet habe . Insbesondere Event-Handler bilden natürliche Modulgrenzen. Das Zulassen, dass Ausnahmen in ein generisches Fenster- / Ereignissystem (z. B.) gelangen, wird bestraft. Der Handler sollte sie alle fangen, wenn das Programm dort überleben soll.
Kaz Dragon
17

Dieses Interview mit Anders Hejlsberg ist ziemlich berühmt. Darin erklärt er, warum das C # -Designteam überprüfte Ausnahmen überhaupt verworfen hat. Kurz gesagt, es gibt zwei Hauptgründe: Versions- und Skalierbarkeit.

Ich weiß, dass sich das OP auf C ++ konzentriert, während Hejlsberg C # diskutiert, aber die Punkte, die Hejlsberg hervorhebt, sind auch perfekt auf C ++ anwendbar.

CesarGon
quelle
5
"Die Punkte, die Hejlsberg macht, sind auch für C ++ perfekt anwendbar" .. Falsch! Überprüfte Ausnahmen, wie sie in Java implementiert sind, zwingen den Anrufer, mit ihnen umzugehen (und / oder den Anrufer des Anrufers usw.). Ausnahme - Spezifikationen in C ++ (während ziemlich schwach und nutzlos) gelten nur für eine einzige „Funktionsaufruf-Grenze“ und sie nicht „das Call - Stack propagieren up“ wie überprüft Ausnahmen machen.
Martin Ba
3
@Martin: Ich stimme Ihnen in Bezug auf das Verhalten von Ausnahmespezifikationen in C ++ zu. Aber mein Satz, den Sie zitieren "die Punkte, die Hejlsberg macht, sind auch perfekt auf C ++ anwendbar", bezieht sich eher auf die Punkte, die Hejlsberg macht, als auf die C ++ - Spezifikationen. Mit anderen Worten, ich spreche hier eher von Versions- und Skalierbarkeit des Programms als von Ausnahmeverbreitung.
CesarGon
3
Ich war ziemlich früh anderer Meinung als Hejlsberg: "Die Sorge, die ich bei geprüften Ausnahmen habe, sind die Handschellen, die sie Programmierern anlegen. Man sieht, wie Programmierer neue APIs mit all diesen Throws-Klauseln auswählen und dann sieht man, wie verworren ihr Code wird, und man merkt Die geprüften Ausnahmen helfen ihnen nicht. Es ist eine Art diktatorische API-Designer, die Ihnen erklären, wie Sie Ihre Ausnahmebehandlung durchführen. " --- Wenn Ausnahmen aktiviert sind oder nicht, müssen Sie die von der aufgerufenen API ausgelösten Ausnahmen weiterhin verarbeiten. Überprüfte Ausnahmen machen es einfach explizit.
quant_dev
5
@quant_dev: Nein, Sie müssen die von der aufgerufenen API ausgelösten Ausnahmen nicht behandeln. Eine absolut gültige Option besteht darin , die Ausnahmen nicht zu behandeln und sie den Anrufstapel in die Luft sprudeln zu lassen, damit der Anrufer sie verarbeiten kann.
CesarGon
4
@CesarGon Das Nicht-Behandeln von Ausnahmen bestimmt auch, wie mit ihnen umgegangen werden soll.
quant_dev