Warum gibt es in C ++ kein 'finally'-Konstrukt?

57

Die Ausnahmebehandlung in C ++ ist auf try / throw / catch beschränkt. Im Gegensatz zu Object Pascal, Java, C # und Python wurde das finallyKonstrukt auch in C ++ 11 nicht implementiert.

Ich habe sehr viel C ++ - Literatur gesehen, die sich mit "Exception Safe Code" befasst. Lippman schreibt, dass sicherer Code für Ausnahmen ein wichtiges, aber fortgeschrittenes, schwieriges Thema ist, das über den Rahmen seines Primers hinausgeht - was zu bedeuten scheint, dass sicherer Code für C ++ nicht grundlegend ist. Herb Sutter widmet dem Thema in seinem Exceptional C ++!

Dennoch scheint es mir, dass viele der Probleme, die beim Versuch aufgetreten sind, "ausnahmesicheren Code" zu schreiben, ziemlich gut gelöst werden könnten, wenn das finallyKonstrukt implementiert würde, so dass der Programmierer sicherstellen kann, dass das Programm selbst im Falle einer Ausnahme wiederhergestellt werden kann in einen sicheren, stabilen und leckagefreien Zustand, in der Nähe der Ressourcenzuweisung und des potenziell problematischen Codes. Als sehr erfahrener Delphi- und C # -Programmierer verwende ich try .. finally-Blöcke ziemlich ausführlich in meinem Code, wie es die meisten Programmierer in diesen Sprachen tun.

Angesichts all der in C ++ 11 implementierten "Schnickschnack" war ich erstaunt, dass "endlich" immer noch nicht da war.

Warum wurde das finallyKonstrukt nie in C ++ implementiert? Es ist wirklich kein sehr schwieriges oder fortgeschrittenes Konzept und trägt wesentlich dazu bei, dem Programmierer das Schreiben von "ausnahmesicherem Code" zu erleichtern.

Vektor
quelle
25
Warum nicht endlich? Weil Sie Dinge im Destruktor freigeben, die automatisch ausgelöst werden, wenn das Objekt (oder der Smart Pointer) den Gültigkeitsbereich verlässt. Destruktoren sind finally {} überlegen, da sie den Workflow von der Bereinigungslogik trennen. So wie Sie nicht möchten, dass Anrufe Ihre Arbeitsabläufe in einer überflüssig gesammelten Sprache überfrachten.
mike30
2
Siehe auch Haben die Java-Entwickler RAII bewusst aufgegeben?
BlueRaja - Danny Pflughoeft
8
Die Frage stellen: "Warum gibt es finallyin C ++ kein und welche Techniken für die Ausnahmebehandlung werden an seiner Stelle verwendet?" ist gültig und thematisch für diese Seite. Die vorhandenen Antworten decken dies gut ab, denke ich. In eine Diskussion verwandeln über "Sind die Gründe der C ++ - Designer, nicht einzubeziehen, finallylohnenswert?" und "Sollte finallyzu C ++ hinzugefügt werden?" und die Diskussion über Kommentare zu der Frage und jeder Antwort fortzusetzen, passt nicht zum Modell dieser Q & A-Site.
Josh Kelley
2
Wenn Sie es endlich geschafft haben, haben Sie bereits eine Trennung der Probleme: Der Hauptcodeblock ist hier, und die Bereinigungsproblematik wird hier erledigt.
Kaz
2
@ Kaz. Der Unterschied ist implizit und explizit zu bereinigen. Mit einem Destruktor werden Sie automatisch bereinigt, ähnlich wie ein einfaches altes Grundelement bereinigt wird, wenn es vom Stapel fällt. Sie müssen keine expliziten Aufräumarbeiten durchführen und können sich auf Ihre Kernlogik konzentrieren. Stellen Sie sich vor, wie kompliziert es wäre, wenn Sie stapelzugeordnete Primitive in einem try / finally-Vorgang bereinigen müssten. Implizite Bereinigung ist überlegen. Der Vergleich der Klassensyntax mit anonymen Funktionen ist nicht relevant. Durch die Übergabe erstklassiger Funktionen an eine Funktion, die ein Handle freigibt, könnte die manuelle Bereinigung zentralisiert werden.
mike30

Antworten:

57

Als zusätzlichen Kommentar zu @ Nemanjas Antwort (die, da sie Stroustrup zitiert, wirklich so gut ist, wie Sie sie erhalten können):

Es geht wirklich nur darum, die Philosophie und Redewendungen von C ++ zu verstehen. Nehmen Sie ein Beispiel für eine Operation, die eine Datenbankverbindung für eine persistente Klasse öffnet und sicherstellen muss, dass diese Verbindung geschlossen wird, wenn eine Ausnahme ausgelöst wird. Dies ist eine Frage der Ausnahmesicherheit und gilt für alle Sprachen mit Ausnahmen (C ++, C #, Delphi ...).

In einer Sprache, die try/ verwendet finally, könnte der Code ungefähr so ​​aussehen:

database.Open();
try {
    database.DoRiskyOperation();
} finally {
    database.Close();
}

Einfach und unkompliziert. Es gibt jedoch einige Nachteile:

  • Wenn die Sprache keine deterministischen Destruktoren hat, muss ich immer den finallyBlock schreiben , sonst lecke ich Ressourcen.
  • Wenn DoRiskyOperationes sich um mehr als einen einzelnen Methodenaufruf handelt - wenn ich im tryBlock etwas zu verarbeiten habe -, ist die CloseOperation möglicherweise ein gutes Stück von der OpenOperation entfernt. Ich kann meine Bereinigung nicht direkt neben meiner Anschaffung schreiben.
  • Wenn ich mehrere Ressourcen habe, die auf eine ausnahmesichere Weise beschafft und dann freigegeben werden müssen, kann ich am Ende mehrere Ebenen mit einer Tiefe von try/ finallyBlöcken haben.

Der C ++ - Ansatz würde folgendermaßen aussehen:

ScopedDatabaseConnection scoped_connection(database);
database.DoRiskyOperation();

Dies löst alle Nachteile des finallyAnsatzes vollständig . Es hat ein paar eigene Nachteile, aber sie sind relativ gering:

  • Es besteht eine gute Chance, dass Sie die ScopedDatabaseConnectionKlasse selbst schreiben müssen . Es ist jedoch eine sehr einfache Implementierung - nur 4 oder 5 Codezeilen.
  • Es geht darum, eine zusätzliche lokale Variable zu erstellen - von der Sie anscheinend nicht begeistert sind, basierend auf Ihrem Kommentar zu "Klassen ständig zu erstellen und zu zerstören, um sich auf ihre Destruktoren zu verlassen, um Ihr Chaos zu bereinigen, ist sehr schlecht" -, aber ein guter Compiler wird optimieren die zusätzliche Arbeit, die eine zusätzliche lokale Variable mit sich bringt. Gutes C ++ - Design hängt in hohem Maße von solchen Optimierungen ab.

In Anbetracht dieser Vor- und Nachteile empfinde ich RAII als eine viel bessere Technik als RAII finally. Ihr Kilometerstand kann variieren.

Schließlich Scoped...gibt es Bibliotheken wie ScopeGuard und Boost.ScopeExit , die diese Art der deterministischen Bereinigung ermöglichen , da RAII in C ++ eine so gut etablierte Redewendung ist und die Entwickler von der Last des Schreibens zahlreicher Klassen entlastet .

Josh Kelley
quelle
8
C # verfügt über die usingAnweisung, mit der Objekte, die die IDisposableSchnittstelle implementieren, automatisch bereinigt werden. Während es also möglich ist, etwas falsch zu machen, ist es ziemlich einfach, es richtig zu machen.
Robert Harvey
18
Sie müssen eine völlig neue Klasse schreiben, um die vorübergehende Umkehrung der Zustandsänderung zu gewährleisten. Dabei wird eine Konstruktions-ID verwendet, die vom Compiler mit einem try/finallyKonstrukt implementiert wird, da der Compiler ein Konstrukt nicht verfügbar macht try/finallyund der Zugriff nur über die klassenbasierte Methode möglich ist Designsprache, ist kein "Vorteil"; Es ist die eigentliche Definition einer Abstraktionsinversion.
Mason Wheeler
15
@ MasonWheeler - Ähm, ich habe nicht gesagt, dass es von Vorteil ist, eine neue Klasse schreiben zu müssen. Ich sagte, es ist ein Nachteil. Auf der Balance, aber ich bevor RAII zu haben zu benutzen finally. Wie gesagt, Ihre Laufleistung kann variieren.
Josh Kelley
7
@JoshKelley: 'Gutes C ++ - Design hängt stark von diesen Optimierungen ab.' Das Schreiben von fremdem Code und das anschließende Verlassen auf Compileroptimierung ist gutes Design ?! IMO ist das Gegenteil von gutem Design. Zu den Grundlagen guten Designs gehört prägnanter, leicht lesbarer Code. Weniger zu debuggen, weniger zu halten, etc etc etc. Sie sollten NICHT Gobs Code schreiben und dann auf dem Compiler verlassen weg alles gehen zu machen - IMO das macht keinen Sinn , überhaupt nicht !
Vector
14
@Mikey: Das Duplizieren von Bereinigungscode (oder die Tatsache, dass Bereinigung stattfinden muss) auf der gesamten Codebasis ist also "präzise" und "leicht lesbar"? Mit RAII schreiben Sie einen solchen Code einmal und er wird automatisch überall angewendet.
Mankarse
55

Von Warum C nicht ein „endlich“ konstruieren ++ zur Verfügung stellen? in Bjarne Stroustrups C ++ Style und Technik FAQ :

Weil C ++ eine Alternative unterstützt, die fast immer besser ist: Die Technik "Ressourcenbeschaffung ist Initialisierung" (TC ++ PL3, Abschnitt 14.4). Die Grundidee besteht darin, eine Ressource durch ein lokales Objekt darzustellen, sodass der Destruktor des lokalen Objekts die Ressource freigibt. Auf diese Weise kann der Programmierer nicht vergessen, die Ressource freizugeben.

Nemanja Trifunovic
quelle
5
Aber es gibt nichts an dieser Technik, was spezifisch für C ++ ist, oder? Sie können RAII in jeder Sprache mit Objekten, Konstruktoren und Destruktoren ausführen. Es ist eine großartige Technik, aber das bloße Vorhandensein von RAII bedeutet nicht, dass ein finallyKonstrukt für immer und ewig unbrauchbar ist, ungeachtet dessen , was Strousup sagt. Die bloße Tatsache, dass das Schreiben von "Exception Safe Code" in C ++ eine große Rolle spielt, ist ein Beweis dafür. Heck, C # hat beide Destruktoren und finally, und sie werden beide verwendet.
Tacroy
28
@Tacroy: C ++ ist eine der wenigen Hauptsprachen mit deterministischen Destruktoren. C # "Destruktoren" sind für diesen Zweck nutzlos, und Sie müssen manuell "using" -Blöcke schreiben, um RAII zu haben.
Nemanja Trifunovic
15
@Mikey du hast die Antwort von "Warum liefert C ++ kein" endlich "Konstrukt?" direkt von Stroustrup selbst dort. Was kann man mehr verlangen? Das ist , warum.
5
@Mikey Wenn Sie befürchten, dass sich Ihr Code gut verhält, insbesondere keine Ressourcen verliert, wenn Ausnahmen ausgelöst werden, sorgen Sie sich um die Ausnahmesicherheit / versuchen, ausnahmesicheren Code zu schreiben. Sie nennen es einfach nicht so und aufgrund der unterschiedlichen Tools, die verfügbar sind, implementieren Sie es anders. Aber genau darüber reden die C ++ - Leute, wenn sie über Ausnahmesicherheit sprechen.
19
@Kaz: Ich muss nur einmal daran denken, die Bereinigung im Destruktor durchzuführen, und von da an verwende ich einfach das Objekt. Ich muss daran denken, die Bereinigung im finally-Block jedes Mal durchzuführen, wenn ich die Operation verwende, die zuordnet.
Deworde
19

Der Grund, den C ++ nicht hat, finallyist, dass es in C ++ nicht benötigt wird. finallywird verwendet, um Code auszuführen, unabhängig davon, ob eine Ausnahme aufgetreten ist oder nicht. Dies ist fast immer eine Art Bereinigungscode. In C ++ sollte sich dieser Bereinigungscode im Destruktor der betreffenden Klasse befinden, und der Destruktor wird immer wie ein finallyBlock aufgerufen . Die Redewendung, den Destruktor für die Bereinigung zu verwenden, heißt RAII .

In der C ++ - Community wird möglicherweise mehr über ausnahmesicheren Code gesprochen, in anderen Sprachen mit Ausnahmen ist dies jedoch fast genauso wichtig. Der springende Punkt bei "ausnahmesicherem" Code ist, dass Sie sich überlegen, in welchem ​​Zustand Ihr Code verbleibt, wenn bei einer der von Ihnen aufgerufenen Funktionen / Methoden eine Ausnahme auftritt.
In C ++ ist ausnahmesicherer Code etwas wichtiger, da in C ++ keine automatische Speicherbereinigung vorhanden ist, die Objekte berücksichtigt, die aufgrund einer Ausnahme verwaist sind.

Der Grund, warum die Ausnahmesicherheit in der C ++ - Community mehr diskutiert wird, beruht wahrscheinlich auch auf der Tatsache, dass Sie in C ++ besser wissen müssen, was schief gehen kann, da die Sprache weniger Standardsicherheitsnetze enthält.

Bart van Ingen Schenau
quelle
2
Hinweis: Bitte behaupten Sie nicht, dass C ++ deterministische Destruktoren hat. Object Pascal / Delphi hat auch deterministische Destruktoren, unterstützt aber auch 'finally', aus den sehr guten Gründen, die ich in meinen ersten Kommentaren unten erläutert habe.
Vector
13
@Mikey: Angesichts der Tatsache, dass es noch nie einen Vorschlag zur Erweiterung finallydes C ++ - Standards gegeben hat, kann ich mit Sicherheit den Schluss ziehen, dass die C ++ - Community kein the absence of finallyProblem sieht . Den meisten Sprachen finallyfehlt die konsistente deterministische Zerstörung von C ++. Ich sehe, dass Delphi beide hat, aber ich kenne die Geschichte nicht gut genug, um zu wissen, welche zuerst da war.
Bart van Ingen Schenau
3
Dephi unterstützt keine stapelbasierten Objekte - nur heapbasierte Objekte und Objektreferenzen auf dem Stapel. Daher ist 'finally' erforderlich, um Destruktoren usw. bei Bedarf explizit aufzurufen.
Vector
2
In C ++ gibt es eine ganze Menge Cruft, die wohl nicht benötigt werden, daher kann dies nicht die richtige Antwort sein.
Kaz
15
In den mehr als zwei Jahrzehnten, in denen ich die Sprache verwendet habe und mit anderen Leuten zusammengearbeitet habe, die die Sprache verwendet haben, bin ich noch keinem C ++ - Programmierer begegnet, der sagte: "Ich wünschte wirklich, die Sprache hätte eine finally". Ich kann mich nie an eine Aufgabe erinnern, die mir leichter gefallen wäre, wenn ich Zugang dazu gehabt hätte.
Gort the Robot
12

Andere haben RAII als Lösung diskutiert. Es ist eine vollkommen gute Lösung. Aber das spricht nicht wirklich an, warum sie nicht finallyso gut hinzugefügt haben, da es eine weithin gewünschte Sache ist. Die Antwort darauf ist grundlegender für das Design und die Entwicklung von C ++: Während der gesamten Entwicklung von C ++ haben sich die Beteiligten nachdrücklich gegen die Einführung von Designmerkmalen gewehrt, die mit anderen Features ohne großen Aufwand erreicht werden können, und insbesondere dort, wo dies die Einführung erfordert von neuen Schlüsselwörtern, die älteren Code inkompatibel machen könnten. Da RAII eine hochfunktionale Alternative zu C ++ 11 bietet finallyund Sie Ihre eigenen finallysowieso in C ++ 11 rollen können, gab es wenig Nachfrage danach.

Sie müssen lediglich eine Klasse erstellen Finally, die die Funktion aufruft, die in ihrem Destruktor an ihren Konstruktor übergeben wird. Dann können Sie dies tun:

try
{
    Finally atEnd([&] () { database.close(); });

    database.doRisky();
}

Die meisten nativen C ++ - Programmierer bevorzugen jedoch im Allgemeinen sauber gestaltete RAII-Objekte.

Jack Aidley
quelle
3
Sie vermissen die Referenzaufnahme in Ihrem Lambda. Sollte auch sein Finally atEnd([&] () { database.close(); });, ich denke, das Folgende ist besser: { Finally atEnd(...); try {...} catch(e) {...} }(Ich habe den Finalizer aus dem Try-Block gehoben, damit er nach den Catch-Blöcken ausgeführt wird.)
Thomas Eding
2

Sie können ein "Trap" -Muster verwenden - auch wenn Sie den try / catch-Block nicht verwenden möchten.

Stellen Sie ein einfaches Objekt in den gewünschten Bereich. In den Destruktor dieses Objekts legen Sie Ihre "endgültige" Logik. Egal was passiert, wenn der Stapel abgewickelt wird, wird der Destruktor des Objekts aufgerufen und Sie erhalten Ihre Süßigkeiten.

Arie R
quelle
1
Dies beantwortet die Frage nicht und beweist einfach, dass es schließlich doch keine so schlechte Idee ist ...
Vector
2

Nun, Sie könnten finallymit Lambdas eine Art Roll-Your-Own- Methode verwenden, mit der sich Folgendes gut kompilieren lässt (anhand eines Beispiels ohne RAII, natürlich nicht das schönste Stück Code):

{
    FILE *file = fopen("test","w");

    finally close_the_file([&]{
        cout << "We're closing the file in a pseudo-finally clause." << endl;
        fclose(file);
    });
}

Siehe diesen Artikel .

einpoklum - Monica wieder einsetzen
quelle
-2

Ich bin mir nicht sicher, ob ich der Behauptung zustimme, dass RAII eine Obermenge von ist finally. Die Achillesferse von RAII ist einfach: Ausnahmen. RAII wird mit Destruktoren implementiert, und es ist in C ++ immer falsch, einen Destruktor zu löschen. Das bedeutet, dass Sie RAII nicht verwenden können, wenn Sie Ihren Bereinigungscode zum Werfen benötigen. Wenn finallyes dagegen implementiert würde, gäbe es keinen Grund zu der Annahme, dass es nicht legal wäre, aus einem finallyBlock zu werfen .

Betrachten Sie einen Codepfad wie diesen:

void foo() {
    try {
        ... stuff ...
        complex_cleanup();
    } catch (A& a) {
        handle_a(a);
        complex_cleanup();
        throw;
    } catch (B& b) {
        handle_b(b);
        complex_cleanup();
        throw;
    } catch (...) {
        handle_generic();
        complex_cleanup();
        throw;
    }
}

Wenn wir hätten finally, könnten wir schreiben:

void foo() {
    try {
        ... stuff ...
    } catch (A& a) {
        handle_a(a);
        throw;
    } catch (B& b) {
        handle_b(b);
        throw;
    } catch (...) {
        handle_generic();
        throw;
    } finally {
        complex_cleanup();
    }
}

Aber ich kann keine Möglichkeit finden, mit RAII ein gleichwertiges Verhalten zu erzielen.

Wenn jemand weiß, wie man das in C ++ macht, interessiert mich die Antwort sehr. Ich würde mich sogar über etwas freuen, das sich darauf stützt, dass zum Beispiel alle Ausnahmen, die von einer einzelnen Klasse mit einigen besonderen Fähigkeiten geerbt wurden, durchgesetzt werden.

Verrückter Wissenschaftler
quelle
1
Wenn complex_cleanupSie in Ihrem zweiten Beispiel werfen können, könnten Sie einen Fall haben, in dem zwei nicht erfasste Ausnahmen auf einmal im Flug sind, genau wie Sie es mit RAII / destructors tun würden, und C ++ weigert sich, dies zuzulassen. Wenn Sie möchten, dass die ursprüngliche Ausnahme angezeigt wird, complex_cleanupsollten Sie Ausnahmen verhindern, genau wie bei RAII / destructors. Wenn Sie möchten complex_cleanup, dass die Ausnahme angezeigt wird, können Sie verschachtelte try / catch-Blöcke verwenden. Dies ist jedoch eine Tangente, die sich nur schwer in einen Kommentar einfügen lässt. Daher ist es eine separate Frage wert.
Josh Kelley
Ich möchte RAII verwenden, um das gleiche Verhalten wie im ersten Beispiel sicherer zu erhalten. Ein Wurf in einen mutmaßlichen finallyBlock würde genau so funktionieren wie ein Wurf in einen catchBlock. WRT-Ausnahmen während des Flugs - kein Aufruf std::terminate. Die Frage ist "warum nicht finallyin C ++?" und die Antworten sagen alle: "Du brauchst es nicht ... RAII FTW!" Mein Punkt ist, dass RAII für einfache Fälle wie die Speicherverwaltung in Ordnung ist, aber bis das Problem der Ausnahmen gelöst ist, sind zu viele Überlegungen / Overheads / Bedenken / Neugestaltungen erforderlich, um eine Allzwecklösung zu sein.
MadScientist
3
Ich verstehe Ihren Punkt - es gibt einige legitime Probleme mit Destruktoren, die werfen könnten - aber diese sind selten. Zu sagen, dass RAII + -Ausnahmen ungelöste Probleme haben oder dass RAII keine Allzwecklösung ist, entspricht einfach nicht der Erfahrung der meisten C ++ - Entwickler.
Josh Kelley
1
Wenn Sie die Notwendigkeit haben, Ausnahmen in Destruktoren auszulösen, tun Sie etwas Falsches - möglicherweise verwenden Sie Zeiger an anderen Stellen, wenn sie nicht erforderlich sind.
Vector
1
Dies ist zu umständlich für Kommentare. Stellen Sie eine Frage dazu: Wie würden Sie mit diesem Szenario in C ++ mit dem RAII-Modell umgehen ... es scheint nicht zu funktionieren ... Auch hier sollten Sie Ihre Kommentare richten : Geben Sie @ und den Namen des Mitglieds ein, mit dem Sie sich unterhalten bis am Anfang Ihres Kommentars. Wenn sich Kommentare auf Ihrem eigenen Beitrag befinden, werden Sie über alles informiert, andere jedoch nicht, es sei denn, Sie richten einen Kommentar an sie.
Vector