Was ist der konzeptionelle Unterschied zwischen finally und einem Destruktor?

12

Erstens ist mir klar, warum es in C ++ kein 'finally'-Konstrukt gibt. Eine lange Diskussion über Kommentare zu einer anderen Frage scheint jedoch eine gesonderte Frage zu rechtfertigen.

Abgesehen von dem Problem, dass finallyC # und Java grundsätzlich nur einmal (== 1) pro Bereich existieren können und ein einzelner Bereich mehrere (== n) C ++ - Destruktoren haben kann, denke ich, dass sie im Wesentlichen dasselbe sind. (Mit einigen technischen Unterschieden.)

Ein anderer Benutzer argumentierte jedoch :

... Ich habe versucht zu sagen, dass ein dtor von Natur aus ein Werkzeug für (Sematik freigeben) und schließlich von Natur aus ein Werkzeug für (Semantik festlegen) ist. Wenn Sie nicht sehen, warum: Überlegen Sie, warum es legitim ist, Ausnahmen in finally-Blöcken übereinander zu werfen, und warum dies nicht für Destruktoren gilt. (In gewissem Sinne handelt es sich um eine Daten- / Kontrollsache. Destruktoren dienen der Freigabe von Daten und schließlich der Freigabe von Kontrolle. Sie unterscheiden sich; es ist bedauerlich, dass C ++ sie zusammenhält.)

Kann jemand das klären?

Martin Ba
quelle

Antworten:

6
  • Transaktion ( try)
  • Fehlerausgabe / -antwort ( catch)
  • Externer Fehler ( throw)
  • Programmiererfehler ( assert)
  • Rollback (am nächsten könnten Scope Guards in Sprachen sein, die sie nativ unterstützen)
  • Ressourcen freigeben (Destruktoren)
  • Verschiedener transaktionsunabhängiger Kontrollfluss ( finally)

Es gibt keine bessere Beschreibung für einen finallybeliebigen transaktionsunabhängigen Kontrollfluss. Es ist nicht unbedingt so direkt auf ein Konzept auf hoher Ebene im Kontext einer Transaktions- und Fehlerbehebungs-Denkweise abgebildet, insbesondere in einer theoretischen Sprache, die sowohl Destruktoren als auch hatfinally .

Was mir am meisten inhärent fehlt, ist eine Sprachfunktion, die direkt das Konzept des Zurückrollens externer Nebenwirkungen darstellt. Scope Guards in Sprachen wie D sind das, was ich mir am ehesten vorstellen kann, um dieses Konzept zu repräsentieren. Vom Standpunkt des Kontrollflusses aus müsste ein Rollback im Bereich einer bestimmten Funktion einen außergewöhnlichen Pfad von einem regulären unterscheiden und gleichzeitig das Rollback implizit für alle durch die Funktion verursachten Nebenwirkungen automatisieren, falls die Transaktion fehlschlägt, jedoch nicht, wenn die Transaktion erfolgreich ist . Das ist mit Destruktoren einfach zu tun, wenn wir succeededzum Beispiel am Ende unseres try-Blocks einen Booleschen Wert auf true setzen, um die Rollback-Logik in einem Destruktor zu verhindern. Aber es ist ein ziemlich umständlicher Weg, dies zu tun.

Das scheint zwar nicht so viel zu sparen, aber die Umkehrung von Nebenwirkungen ist eine der schwierigsten Aufgaben (z. B .: Was es so schwierig macht, einen ausnahmesicheren generischen Container zu schreiben).


quelle
4

In gewisser Weise können sowohl ein Ferrari als auch ein Transit verwendet werden, um die Läden für einen halben Liter Milch zu durchsuchen, obwohl sie für unterschiedliche Verwendungszwecke konzipiert sind.

Sie können ein try / finally-Konstrukt in jeden Bereich einfügen und alle im Bereich definierten Variablen im finally-Block bereinigen, um einen C ++ - Destruktor zu emulieren. Dies ist konzeptionell das, was C ++ tut - der Compiler ruft den Destruktor automatisch auf, wenn eine Variable den Gültigkeitsbereich verlässt (dh am Ende des Gültigkeitsbereichsblocks). Sie müssten Ihren Versuch / schließlich so arrangieren, dass der Versuch das allererste und das allerletzte in jedem Bereich ist. Sie müssten auch einen Standard für jedes Objekt definieren, um eine speziell benannte Methode zu haben, die verwendet wird, um den Status zu bereinigen, den Sie im finally-Block aufrufen würden, obwohl Sie die normale Speicherverwaltung Ihrer Sprache beibehalten könnten Räumen Sie das jetzt geleerte Objekt auf, wenn es Ihnen gefällt.

Es wäre allerdings nicht schön, dies zu tun, und obwohl .NET IDispose als manuell verwalteten Destruktor einführte und mit Blöcken versucht, diese manuelle Verwaltung ein wenig zu vereinfachen, ist dies in der Praxis immer noch nichts, was Sie tun möchten .

gbjbaanb
quelle
4

Aus meiner Sicht besteht der Hauptunterschied darin, dass ein Destruktor in c ++ ein impliziter Mechanismus ist (der automatisch aufgerufen wird), um zugewiesene Ressourcen freizugeben, während try ... schließlich als expliziter Mechanismus verwendet werden kann, um dies zu tun.

In c ++ - Programmen ist der Programmierer für die Freigabe der zugewiesenen Ressourcen verantwortlich. Dies wird normalerweise im Destruktor einer Klasse implementiert und sofort ausgeführt, wenn eine Variable den Gültigkeitsbereich verlässt oder wenn delete aufgerufen wird.

Wenn in c ++ eine lokale Variable einer Klasse erstellt wird, ohne newdie Ressourcen dieser Instanzen zu verwenden, werden diese vom Destruktor implizit freigegeben, wenn es eine Ausnahme gibt.

// c++
void test() {
    MyClass myClass(someParameter);
    // if there is an exception the destructor of MyClass is called automatically
    // this does not work with
    // MyClass* pMyClass = new MyClass(someParameter);

} // on test() exit the destructor of myClass is implicitly called

In Java, C # und anderen Systemen mit automatischer Speicherverwaltung entscheidet der System Garbage Collector, wann eine Klasseninstanz zerstört wird.

// c#
void test() {
    MyClass myClass = new MyClass(someParameter);
    // if there is an exception myClass is NOT destroyed so there may be memory/resource leakes

    myClass.destroy(); // this is never called
}

Es gibt keinen impliziten Mechanismus, so dass Sie dies explizit mit try finally programmieren müssen

// c#
void test() {
    MyClass myClass = null;

    try {
        myClass = new MyClass(someParameter);
        ...
    } finally {
        // explicit memory management
        // even if there is an exception myClass resources are freed
        myClass.destroy();
    }

    myClass.destroy(); // this is never called
}
k3b
quelle
Warum wird der Destruktor in C ++ automatisch nur mit einem Stack-Objekt und nicht mit einem Heap-Objekt im Falle einer Ausnahme aufgerufen?
Giorgio
@Giorgio Da Heap-Ressourcen in einem Speicherbereich gespeichert sind, der nicht direkt mit dem Aufrufstapel verknüpft ist. Stellen Sie sich zum Beispiel eine Multithread-Anwendung mit 2 Threads Aund vor B. Wenn ein Thread ausgelöst wird, sollte das Zurücksetzen der A'sTransaktion nicht die zugewiesenen Ressourcen zerstören B, z. In der Regel ist der Heapspeicher in C ++ jedoch weiterhin an Objekte auf dem Stapel gebunden.
@Giorgio Ein std::vectorObjekt befindet sich möglicherweise auf dem Stapel, zeigt jedoch auf den Speicher des Heapspeichers. In diesem Fall werden sowohl das Vektorobjekt (auf dem Stapel) als auch dessen Inhalt (auf dem Heapspeicher) während des Abwickelns des Stapels freigegeben Wenn Sie den Vektor auf dem Stapel zerstören, wird ein Destruktor aufgerufen, der den zugehörigen Speicher auf dem Heap freigibt (und diese Heap-Elemente ebenfalls zerstört). In der Regel befinden sich die meisten C ++ - Objekte aus Gründen der Ausnahmesicherheit auf dem Stapel, auch wenn sie nur auf den Arbeitsspeicher auf dem Heap verweisen, wodurch die Freigabe des Heap- und des Stapelspeichers beim Abwickeln des Stapels automatisiert wird.
4

Ich bin froh, dass du dies als Frage gepostet hast. :)

Ich habe versucht zu sagen, dass Destruktoren und finallykonzeptionell unterschiedlich sind:

  • Destruktoren sind für die Freigabe von Ressourcen ( Daten )
  • finallyist für die Rückkehr zum Anrufer ( Kontrolle )

Betrachten Sie beispielsweise diesen hypothetischen Pseudocode:

try {
    bar();
} finally {
    logfile.print("bar has exited...");
}

finallyHier wird ein Steuerungsproblem und kein Ressourcenverwaltungsproblem vollständig gelöst.
Es wäre aus verschiedenen Gründen nicht sinnvoll, dies in einem Destruktor zu tun:

  • Kein Ding wird „erworben“ oder „geschaffen“
  • Wenn nicht in die Protokolldatei gedruckt wird, führt dies nicht zu Ressourcenlecks, Datenbeschädigungen usw. (vorausgesetzt, die Protokolldatei wird hier nicht an anderer Stelle in das Programm zurückgespeist).
  • Es ist legitim logfile.printzu scheitern, wohingegen Zerstörung (konzeptionell) nicht scheitern kann

Hier ist ein weiteres Beispiel, diesmal wie in Javascript:

var mo_document = document, mo;
function observe(mutations) {
    mo.disconnect();  // stop observing changes to prevent re-entrance
    try {
        /* modify stuff */
    } finally {
        mo.observe(mo_document);  // continue observing (conceptually, this can fail)
    }
}
mo = new MutationObserver(observe);
return observe();

Auch im obigen Beispiel müssen keine Ressourcen freigegeben werden.
In der Tat, das finallyist Block Erwerb Ressourcen intern ihr Ziel zu erreichen, die möglicherweise ausfallen könnten. Daher ist es nicht sinnvoll, einen Destruktor zu verwenden (falls Javascript einen hat).

Andererseits in diesem Beispiel:

b = get_data();
try {
    a.write(b);
} finally {
    free(b);
}

finallyeine Ressource zerstört, b. Es ist ein Datenproblem. Das Problem besteht nicht darin, die Kontrolle sauber an den Anrufer zurückzugeben, sondern vielmehr, Ressourcenlecks zu vermeiden.
Ein Ausfall ist keine Option und sollte (konzeptionell) niemals auftreten.
Jedes Release von bist zwangsläufig mit einer Akquisition verbunden, und es ist sinnvoll, RAII zu verwenden.

Mit anderen Worten, nur weil Sie entweder zum Simulieren verwenden können, bedeutet dies nicht, dass beide ein und dasselbe Problem sind oder dass beide geeignete Lösungen für beide Probleme sind.

user541686
quelle
Vielen Dank. Ich bin anderer Meinung, aber hey :-) Ich denke, ich werde in den nächsten Tagen eine gründliche Antwort auf die gegenteilige Ansicht hinzufügen können ...
Martin Ba
2
Wie wirkt sich die Tatsache aus, finallydie hauptsächlich zur Freigabe von Ressourcen (ohne Arbeitsspeicher) verwendet wird?
Bart van Ingen Schenau
1
@BartvanIngenSchenau: Ich habe nie behauptet, dass eine Sprache, die derzeit existiert, eine Philosophie oder Implementierung hat, die der von mir beschriebenen entspricht. Die Leute haben noch nicht alles erfunden, was noch existieren könnte. Ich habe nur argumentiert, dass es sinnvoll wäre, die beiden Begriffe zu trennen, da es sich um unterschiedliche Ideen und Anwendungsfälle handelt. Um Ihre Neugier zu befriedigen, glaube ich, dass D beides hat. Es gibt wahrscheinlich auch andere Sprachen. Ich halte es jedoch nicht für relevant und es ist mir egal, warum z. B. Java dafür war finally.
user541686
1
Ein praktisches Beispiel, auf das ich in JavaScript gestoßen bin, sind Funktionen, die den Mauszeiger während einer längeren Operation vorübergehend in eine Sanduhr ändern (was möglicherweise eine Ausnahme auslöst) und ihn dann in der finallyKlausel wieder auf Normal zurücksetzen. Die C ++ - Weltanschauung würde eine Klasse einführen, die diese "Ressource" einer Zuweisung zu einer pseudo-globalen Variablen verwaltet. Welchen begrifflichen Sinn macht das? Destruktoren sind jedoch C ++ 'Hammer für die Ausführung des erforderlichen Blockende-Codes.
Dan04
1
@ dan04: Vielen Dank, das ist das perfekte Beispiel dafür. Ich könnte schwören, dass ich auf so viele Situationen gestoßen bin, in denen RAII keinen Sinn ergab, aber ich hatte es so schwer, an sie zu denken.
user541686
1

Die Antwort von k3b drückt es wirklich gut aus:

Ein Destruktor in c ++ ist ein impliziter Mechanismus (der automatisch aufgerufen wird), um zugewiesene Ressourcen freizugeben, während try ... schließlich als expliziter Mechanismus verwendet werden kann, um dies zu tun.

Bezüglich "Ressourcen" verweise ich gerne auf Jon Kalb: RAII sollte bedeuten, dass Verantwortungsübernahme Initialisierung ist .

Wie auch immer, was implizites vs. explizites betrifft, scheint dies wirklich so zu sein:

  • Ein d'tor ist ein Werkzeug, um zu definieren, welche Operationen implizit stattfinden sollen, wenn die Lebensdauer eines Objekts endet (was häufig mit dem Ende des Gültigkeitsbereichs zusammenfällt).
  • Ein finally-Block ist ein Tool, mit dem explizit festgelegt wird, welche Operationen am Ende des Gültigkeitsbereichs ausgeführt werden sollen.
  • Außerdem darf man technisch gesehen immer abwerfen, aber siehe unten.

Ich denke, das ist es für den konzeptionellen Teil, ...


... jetzt gibt es meiner Meinung nach einige interessante Details:

Ich denke auch nicht, dass c'tor / d'tor konzeptionell etwas "erwerben" oder "erstellen" muss, abgesehen von der Verantwortung, Code im Destruktor auszuführen. Welches ist, was schließlich auch tut: Führen Sie einen Code aus.

Und während Code in einem finally-Block mit Sicherheit eine Ausnahme auslösen kann, reicht mir der Unterschied nicht aus, um zu sagen, dass sie sich konzeptionell über explizit und implizit unterscheiden.

(Außerdem bin ich überhaupt nicht davon überzeugt, dass "guter" Code endlich herauskommen sollte - vielleicht ist das eine ganz andere Frage an sich.)

Martin Ba
quelle
Was halten Sie von meinem Javascript-Beispiel?
user541686
In Bezug auf Ihre anderen Argumente: "Würden wir wirklich das Gleiche unabhängig davon protokollieren wollen?" Ja, es ist nur ein Beispiel und Sie verpassen den Punkt, und ja, niemand hat jemals verboten, spezifischere Details für jeden Fall zu protokollieren. Der Punkt hier ist, dass Sie sicherlich nicht behaupten können, dass es niemals eine Situation gibt, in der Sie etwas protokollieren möchten, das beiden gemeinsam ist. Einige Protokolleinträge sind generisch, andere spezifisch. du willst beides. Und wieder verpassen Sie den Punkt, indem Sie sich auf die Protokollierung konzentrieren. 10-Zeilen-Beispiele zu motivieren ist schwierig; Bitte versuchen Sie, den Punkt nicht zu verpassen.
user541686
Sie haben diese nie angesprochen ...
user541686
@Mehrdad - Ich habe Ihr Javascript-Beispiel nicht angesprochen, weil ich auf einer anderen Seite darüber diskutieren müsste, was ich davon halte. (Ich habe es versucht, aber es hat so lange gedauert, bis ich etwas Kohärentes formuliert habe, dass ich es übersprungen habe :-)
Martin Ba
@Mehrdad - wie für Ihre anderen Punkte - es scheint, wir müssen zustimmen, nicht zuzustimmen. Ich verstehe, wohin du mit dem Unterschied zielst, aber ich bin einfach nicht überzeugt, dass es sich konzeptionell um etwas anderes handelt: Hauptsächlich, weil ich meistens im Lager bin, das es für eine wirklich schlechte Idee hält, von irgendetwas zu werfen ( Anmerkung : Ich auch) Denken Sie in Ihrem observerBeispiel, dass das Werfen eine wirklich schlechte Idee wäre.) Sie können gerne einen Chat eröffnen, wenn Sie dies weiter diskutieren möchten. Es hat auf jeden Fall Spaß gemacht, über Ihre Argumente nachzudenken. Prost.
Martin Ba