C ++ scheint es vorzuziehen, Ausnahmen häufiger zu verwenden.
Ich würde in mancher Hinsicht tatsächlich weniger als Objective-C vorschlagen, da die C ++ - Standardbibliothek im Allgemeinen keine Programmiererfehler wie den grenzüberschreitenden Zugriff auf eine Direktzugriffssequenz in der am häufigsten verwendeten Entwurfsform (in operator[]
, dh) oder auslöst versuchen, einen ungültigen Iterator zu dereferenzieren. Die Sprache greift nicht auf den Zugriff auf ein Array außerhalb der Grenzen oder die Dereferenzierung eines Nullzeigers oder dergleichen zurück.
Wenn Programmiererfehler weitgehend aus der Ausnahmebehandlungsgleichung herausgenommen werden, wird tatsächlich eine sehr große Kategorie von Fehlern entfernt, auf die andere Sprachen häufig reagieren throwing
. C ++ tendiert assert
in solchen Fällen dazu (was nicht in Release- / Produktions-Builds kompiliert wird, sondern nur in Debug-Builds) oder nur ausfällt (häufig abstürzt), wahrscheinlich teilweise, weil die Sprache die Kosten für solche Laufzeitprüfungen nicht auferlegen möchte Dies wäre erforderlich, um solche Programmiererfehler zu erkennen, es sei denn, der Programmierer möchte die Kosten ausdrücklich durch Schreiben von Code bezahlen, der solche Überprüfungen selbst durchführt.
Sutter empfiehlt sogar, in solchen Fällen Ausnahmen in C ++ - Codierungsstandards zu vermeiden:
Der Hauptnachteil der Verwendung einer Ausnahme zum Melden eines Programmierfehlers besteht darin, dass das Abwickeln des Stapels nicht wirklich erfolgen soll, wenn der Debugger genau in der Zeile gestartet werden soll, in der der Verstoß festgestellt wurde, wobei der Status der Zeile intakt ist. Zusammenfassend: Es gibt Fehler, von denen Sie wissen, dass sie auftreten können (siehe Punkte 69 bis 75). Für alles andere, was nicht sollte, und es ist die Schuld des Programmierers, wenn es das tut, gibt es assert
.
Diese Regel ist nicht unbedingt in Stein gemeißelt. In einigen geschäftskritischeren Fällen kann es vorzuziehen sein, beispielsweise Wrapper und einen Codierungsstandard zu verwenden, der einheitlich protokolliert, wo Programmiererfehler auftreten, und throw
bei Programmiererfehlern wie dem Versuch, etwas Ungültiges zu respektieren oder außerhalb der Grenzen darauf zuzugreifen, weil In diesen Fällen kann es zu kostspielig sein, keine Wiederherstellung durchzuführen, wenn die Software eine Chance hat. Insgesamt tendiert der häufigere Gebrauch der Sprache jedoch dazu, Programmierfehlern nicht ins Auge zu sehen.
Externe Ausnahmen
Wo ich sehe, dass Ausnahmen in C ++ am häufigsten empfohlen werden (laut Standardausschuss, z. B.), handelt es sich um "externe Ausnahmen", wie in einem unerwarteten Ergebnis in einer externen Quelle außerhalb des Programms. Ein Beispiel ist, dass kein Speicher zugewiesen werden kann. Zum anderen kann eine kritische Datei nicht geöffnet werden, die für die Ausführung der Software erforderlich ist. Ein anderer kann keine Verbindung zu einem erforderlichen Server herstellen. Ein anderer ist ein Benutzer, der eine Abbruchschaltfläche blockiert, um eine Operation abzubrechen, deren allgemeiner Fallausführungspfad ohne diese externe Unterbrechung einen Erfolg erwartet. All diese Dinge liegen außerhalb der Kontrolle der unmittelbaren Software und der Programmierer, die sie geschrieben haben. Es handelt sich um unerwartete Ergebnisse aus externen Quellen, die verhindern, dass die Operation (die in meinem Buch * eigentlich als unteilbare Transaktion angesehen werden sollte) erfolgreich sein kann.
Transaktionen
Ich empfehle oft, einen try
Block als "Transaktion" zu betrachten, da Transaktionen als Ganzes erfolgreich sein oder als Ganzes fehlschlagen sollten. Wenn wir versuchen, etwas zu tun, und es auf halbem Weg fehlschlägt, müssen alle im Programmstatus vorgenommenen Nebenwirkungen / Mutationen im Allgemeinen zurückgesetzt werden, um das System wieder in einen gültigen Zustand zu versetzen, als ob die Transaktion überhaupt nicht ausgeführt worden wäre. Ebenso wie ein RDBMS, das eine Abfrage nicht zur Hälfte verarbeitet, die Integrität der Datenbank nicht beeinträchtigen sollte. Wenn Sie den Programmstatus direkt in dieser Transaktion ändern, müssen Sie die Stummschaltung aufheben, wenn ein Fehler auftritt (und hier können Scope Guards bei RAII hilfreich sein).
Die viel einfachere Alternative besteht darin , den ursprünglichen Programmstatus nicht zu ändern . Sie können eine Kopie davon mutieren und dann, wenn dies erfolgreich ist, die Kopie gegen das Original austauschen (um sicherzustellen, dass der Austausch nicht ausgelöst werden kann). Wenn dies fehlschlägt, verwerfen Sie die Kopie. Dies gilt auch dann, wenn Sie im Allgemeinen keine Ausnahmen für die Fehlerbehandlung verwenden. Eine "Transaktions" -Mentalität ist der Schlüssel zur ordnungsgemäßen Wiederherstellung, wenn vor dem Auftreten eines Fehlers Programmstatusmutationen aufgetreten sind. Es gelingt entweder als Ganzes oder scheitert als Ganzes. Es gelingt ihm nicht auf halbem Weg, seine Mutationen vorzunehmen.
Dies ist bizarrerweise eines der am seltensten diskutierten Themen, wenn ich Programmierer sehe, die nach der richtigen Fehler- oder Ausnahmebehandlung fragen, aber es ist das schwierigste von allen, in einer Software, die den Programmstatus in vielen von ihnen direkt mutieren möchte, das Richtige zu finden seine Operationen. Reinheit und Unveränderlichkeit können hier ebenso zur Erreichung der Ausnahmesicherheit beitragen wie zur Gewindesicherheit, da eine nicht auftretende Mutation / äußere Nebenwirkung nicht rückgängig gemacht werden muss.
Performance
Ein weiterer entscheidender Faktor für die Verwendung von Ausnahmen ist die Leistung, und ich meine nicht auf eine obsessive, knifflige, kontraproduktive Weise. Viele C ++ - Compiler implementieren das sogenannte "Zero-Cost Exception Handling".
Es bietet keinen Laufzeitaufwand für eine fehlerfreie Ausführung, der sogar den der Fehlerbehandlung mit C-Rückgabewert übertrifft. Als Kompromiss hat die Weitergabe einer Ausnahme einen großen Overhead.
Nach dem, was ich darüber gelesen habe, erfordern Ihre Ausführungspfade für allgemeine Fälle keinen Overhead (nicht einmal den Overhead, der normalerweise mit der Behandlung und Weitergabe von C-Fehlercodes einhergeht), im Gegenzug dafür, dass die Kosten stark auf die außergewöhnlichen Pfade verschoben werden ( was bedeutet, throwing
ist jetzt teurer als je zuvor).
"Teuer" ist etwas schwer zu quantifizieren, aber für den Anfang möchten Sie wahrscheinlich nicht millionenfach in eine enge Schleife werfen. Bei dieser Art von Design wird davon ausgegangen, dass Ausnahmen nicht immer links und rechts auftreten.
Nicht-Fehler
Und dieser Leistungspunkt bringt mich zu Nicht-Fehlern, was überraschend unscharf ist, wenn wir alle möglichen anderen Sprachen betrachten. Aber ich würde angesichts des oben erwähnten kostengünstigen EH-Designs sagen, dass Sie mit ziemlicher Sicherheit nicht throw
auf einen Schlüssel reagieren möchten, der nicht in einem Set gefunden wird. Denn das ist wohl nicht nur ein Fehler (die Person, die nach dem Schlüssel sucht, hat möglicherweise das Set erstellt und erwartet, nach Schlüsseln zu suchen, die nicht immer existieren), sondern es wäre in diesem Zusammenhang auch enorm teuer.
Beispielsweise möchte eine Satzkreuzungsfunktion möglicherweise zwei Sätze durchlaufen und nach Schlüsseln suchen, die sie gemeinsam haben. Wenn Sie keinen Schlüssel finden threw
, werden Sie eine Schleife durchlaufen und möglicherweise in der Hälfte oder mehr der Iterationen auf Ausnahmen stoßen:
Set<int> set_intersection(const Set<int>& a, const Set<int>& b)
{
Set<int> intersection;
for (int key: a)
{
try
{
b.find(key);
intersection.insert(other_key);
}
catch (const KeyNotFoundException&)
{
// Do nothing.
}
}
return intersection;
}
Das obige Beispiel ist absolut lächerlich und übertrieben, aber ich habe im Produktionscode einige Leute gesehen, die aus anderen Sprachen kommen und Ausnahmen in C ++ verwenden, und ich denke, es ist eine ziemlich praktische Aussage, dass dies überhaupt keine angemessene Verwendung von Ausnahmen ist in C ++. Ein weiterer Hinweis oben ist, dass Sie feststellen werden, dass der catch
Block absolut nichts zu tun hat und nur geschrieben wurde, um solche Ausnahmen gewaltsam zu ignorieren. Dies ist normalerweise ein Hinweis (wenn auch kein Garant), dass Ausnahmen in C ++ wahrscheinlich nicht sehr angemessen verwendet werden.
Für diese Arten von Fällen ist eine Art von Rückgabewert, der auf einen Fehler hinweist (alles von der Rückkehr false
zu einem ungültigen Iterator oder nullptr
was auch immer im Kontext sinnvoll ist), normalerweise weitaus geeigneter und auch oft praktischer und produktiver als ein fehlerfreier Typ von In diesem Fall ist normalerweise kein Stapelabwicklungsprozess erforderlich, um die analoge catch
Site zu erreichen .
Fragen
Ich müsste mit internen Fehlerflags arbeiten, wenn ich Ausnahmen vermeiden möchte. Wird es zu viel Mühe geben, damit umzugehen, oder wird es vielleicht sogar besser funktionieren als Ausnahmen? Ein Vergleich beider Fälle wäre die beste Antwort.
Das direkte Vermeiden von Ausnahmen in C ++ erscheint mir äußerst kontraproduktiv, es sei denn, Sie arbeiten in einem eingebetteten System oder in einem bestimmten Fall, der deren Verwendung verbietet (in diesem Fall müssten Sie auch alles tun, um alles zu vermeiden Bibliotheks- und Sprachfunktionalität, die sonst throw
gerne strikt verwendet würde nothrow
new
).
Wenn Sie Ausnahmen aus irgendeinem Grund unbedingt vermeiden müssen (z. B. Arbeiten über C-API-Grenzen eines Moduls, dessen C-API Sie exportieren), stimmen viele möglicherweise nicht mit mir überein, aber ich würde tatsächlich vorschlagen, einen globalen Fehlerbehandler / Status wie OpenGL mit zu verwenden glGetError()
. Sie können dafür sorgen, dass threadlokaler Speicher verwendet wird, um einen eindeutigen Fehlerstatus pro Thread zu erhalten.
Mein Grund dafür ist, dass ich es nicht gewohnt bin, Teams in Produktionsumgebungen gründlich auf mögliche Fehler zu prüfen, wenn Fehlercodes zurückgegeben werden. Wenn sie gründlich wären, könnten einige C-APIs bei nahezu jedem einzelnen C-API-Aufruf auf einen Fehler stoßen, und eine gründliche Überprüfung würde Folgendes erfordern:
if ((err = ApiCall(...)) != success)
{
// Handle error
}
... mit fast jeder einzelnen Codezeile, die die API aufruft und solche Überprüfungen erfordert. Trotzdem hatte ich nicht das Glück, mit so gründlichen Teams zusammenzuarbeiten. Sie ignorieren solche Fehler oft die Hälfte, manchmal sogar die meiste Zeit. Das ist für mich der größte Reiz von Ausnahmen. Wenn wir diese API umschließen und sie throw
bei Auftreten eines Fehlers einheitlich gestalten , kann die Ausnahme möglicherweise nicht ignoriert werden , und meiner Ansicht nach und meiner Erfahrung nach liegt hier die Überlegenheit der Ausnahmen.
Wenn jedoch keine Ausnahmen verwendet werden können, hat der globale Fehlerstatus pro Thread zumindest den Vorteil (ein großer im Vergleich zur Rückgabe von Fehlercodes an mich), dass er möglicherweise einen früheren Fehler etwas später als zu diesem Zeitpunkt abfängt trat in einer schlampigen Codebasis auf, anstatt sie völlig zu verpassen und uns völlig bewusst zu machen, was passiert ist. Der Fehler ist möglicherweise einige Zeilen zuvor oder in einem früheren Funktionsaufruf aufgetreten. Vorausgesetzt, die Software ist noch nicht abgestürzt, können wir uns möglicherweise rückwärts arbeiten und herausfinden, wo und warum er aufgetreten ist.
Da Zeiger selten sind, muss ich interne Fehlerflags verwenden, wenn ich Ausnahmen vermeiden möchte.
Ich würde nicht unbedingt sagen, dass Zeiger selten sind. In C ++ 11 und höher gibt es sogar Methoden, um auf die zugrunde liegenden Datenzeiger von Containern und ein neues nullptr
Schlüsselwort zuzugreifen. Es wird im Allgemeinen als unklug angesehen, unformatierte Zeiger zu verwenden, um Speicher zu besitzen / zu verwalten, wenn Sie unique_ptr
stattdessen etwas verwenden können, wenn man bedenkt , wie wichtig es ist, RAII-konform zu sein, wenn Ausnahmen vorliegen. Aber rohe Zeiger, die keinen Speicher besitzen / verwalten, werden nicht unbedingt als so schlecht angesehen (selbst von Leuten wie Sutter und Stroustrup) und manchmal sehr praktisch, um auf Dinge zu verweisen (zusammen mit Indizes, die auf Dinge verweisen).
Sie sind wohl nicht weniger sicher als die Standard-Container-Iteratoren (zumindest in der Version, ohne überprüfte Iteratoren), die nicht erkennen, ob Sie versuchen, sie nach ihrer Ungültigmachung zu dereferenzieren. C ++ ist immer noch unverschämt eine gefährliche Sprache, würde ich sagen, es sei denn, Ihre spezifische Verwendung möchte alles einpacken und auch nicht besitzende Rohzeiger verstecken. Mit Ausnahmen ist es fast kritisch, dass Ressourcen RAII-konform sind (was im Allgemeinen keine Laufzeitkosten verursacht), aber ansonsten wird nicht unbedingt versucht, die sicherste Sprache zu sein, um Kosten zu vermeiden, die ein Entwickler nicht explizit wünscht gegen etwas anderes tauschen. Die empfohlene Verwendung versucht nicht, Sie sozusagen vor baumelnden Zeigern und ungültigen Iteratoren zu schützen (andernfalls würden wir zur Verwendung ermutigtshared_ptr
überall, was Stroustrup vehement ablehnt). Es versucht, Sie davor zu schützen, eine Ressource nicht ordnungsgemäß freizugeben, freizugeben, zu zerstören, freizuschalten oder zu bereinigen, wenn etwas passiert throws
.