2008-12-21 3 views
6

Bjarne Stroustrup schreibt in seinem C++ Style and Technique FAQ, Hervorhebung von mir:Gibt es Fälle, in denen ein "finally" -Konstrukt in C++ nützlich wäre?

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

class File_handle { 
    FILE* p; 
public: 
    File_handle(const char* n, const char* a) 
     { p = fopen(n,a); if (p==0) throw Open_error(errno); } 
    File_handle(FILE* pp) 
     { p = pp; if (p==0) throw Open_error(errno); } 

    ~File_handle() { fclose(p); } 

    operator FILE*() { return p; } 

    // ... 
}; 

void f(const char* fn) 
{ 
    File_handle f(fn,"rw"); // open fn for reading and writing 
    // use file through f 
} 

In einem System benötigen wir eine "Ressource Handle" -Klasse für jede Ressource. Für jeden Erwerb einer Ressource müssen wir jedoch keine "finally" -Klausel haben. In realistischen Systemen gibt es viel mehr Ressourcenakquisitionen als Arten von Ressourcen, so dass die Methode "Ressourcenakquisition ist Initialisierung" zu weniger Code führt als die Verwendung eines "endgültigen" Konstrukts.

Beachten Sie, dass Bjarne schreibt "fast immer besser" und nicht "immer besser". Nun zu meiner Frage: In welcher Situation wäre ein finally Konstrukt besser als das alternative Konstrukt (RAII) in C++?

+0

Sie stellen zwei verschiedene Fragen. Der Titel fragt, warum es schließlich keine gibt, und der eigentliche Beitrag fragt nach Beispielen für Fälle, in denen es letztendlich besser wäre als RAII. Ich schlage vor, dass Sie einen von ihnen bearbeiten, damit sie zusammenpassen. Es ist verwirrend für Leute, die bei einer Suche auf diesen Beitrag stoßen. – jalf

Antwort

6

Der einzige Grund, warum ich mir vorstellen kann, dass ein finally Block "besser" ist, ist, wenn weniger Code benötigt wird, um dasselbe zu erreichen. Wenn Sie beispielsweise eine Ressource haben, die aus irgendeinem Grund nicht RIIA verwendet, müssen Sie entweder eine Klasse schreiben, um die Ressource zu umbrechen und sie im Destruktor freizugeben, oder einen finally-Block (falls vorhanden) verwenden.

vergleichen:

class RAII_Wrapper 
{ 
    Resource *resource; 

public: 
    RAII_Wrapper() : resource(aquire_resource()) {} 

    ~RAII_Wrapper() { 
     free_resource(resource); 
     delete resource; 
    } 

    Resource *getResource() const { 
     return resource; 
    } 
}; 

void Process() 
{ 
    RAII_Resource wrapper; 
    do_something(wrapper.resource); 
} 

gegen:

void Process() 
{ 
    try { 
     Resource *resource = aquire_resource(); 
     do_something(resource); 
    } 
    finally { 
     free_resource(resource); 
     delete resource; 
    } 
} 

Die meisten Menschen (mich eingeschlossen) würde immer noch argumentieren, dass die erste Version ist besser, weil es Sie nicht zwingen, den Versuch zu verwenden. ..finally blockieren. Sie müssen die Klasse auch nur einmal schreiben, nicht den Code in jeder Funktion duplizieren, die die Ressource verwendet.

Bearbeiten: Wie litb erwähnt, sollten Sie eine Auto_ptr anstelle der Zeiger manuell löschen, was beide Fälle vereinfachen würde.

+0

kleiner Tippfehler. sollte RIIA_Resource-Wrapper sein; anstelle von RIIA_Resource wrapper(); . Sie können auto_ptr const p (aquire_resource()); auto_ptr kümmert sich um all die Sachen selbst. Ich verstehe, dass war nur ein Beispiel tho :) also, nachdem Sie den Tippfehler beheben und einen intelligenten Zeiger erwähnen, erhalten Sie +1 :) –

+0

Hoppla. Als ich anfing, es zu schreiben, übergab ich die Ressource in den Konstruktor und es wurde in einer Referenz statt in einem Zeiger gespeichert. Guter Fang. –

+2

Sollte RAII sein, nicht RIIA. – dalle

6

Schließlich wäre besser, wenn Sie mit C-Code verbinden. Es kann ein Schmerz sein, bestehende C-Funktionalität in RAII zu verpacken.

7

Der Unterschied zwischen ihnen besteht darin, dass Destruktoren die Wiederverwendung der Bereinigungslösung betonen, indem sie sie mit dem verwendeten Typ assoziieren, während try/schließlich einmalige Bereinigungsroutinen betont. So versuchen Sie/finally ist sofort bequemer, wenn Sie eine einmalige einmalige Bereinigungsanforderung haben, die mit der Verwendungsstelle verknüpft ist, anstatt einer wiederverwendbaren Bereinigungslösung, die mit einem von Ihnen verwendeten Typ verknüpft werden kann.

Ich habe das nicht versucht (habe seit Monaten kein gcc heruntergeladen), aber es sollte wahr sein: mit dem Zusatz von lambdas zur Sprache, kann C++ jetzt das effektive Äquivalent von finally haben, nur durch Schreiben eine Funktion namens try_finally. Offensichtliche Nutzung:

try_finally([] 
{ 
    // attempt to do things in here, perhaps throwing... 
}, 
[] 
{ 
    // this always runs, even if the above block throws... 
} 

Natürlich haben Sie Try_ finally, aber nur einmal zu schreiben und dann sind Sie gut zu gehen. Lambdas ermöglichen neue Kontrollstrukturen.

Etwas wie:

template <class TTry, class TFinally> 
void try_finally(const TTry &tr, const TFinally &fi) 
{ 
    try 
    { 
     tr(); 
    } 
    catch (...) 
    { 
     fi(); 
     throw; 
    } 

    fi(); 
} 

Und es gibt überhaupt keine Verbindung zwischen der Gegenwart eines GC und einer Vorliebe für try/finally statt Destruktoren. C++/CLI hat Destruktoren und GC. Sie sind orthogonale Entscheidungen. Try/finally und Destruktoren sind leicht unterschiedliche Lösungen für das gleiche Problem, das für nicht-fungible Ressourcen benötigt wird.

C++ - Funktionsobjekte betonen die Wiederverwendbarkeit, machen aber einmalige anonyme Funktionen schmerzhaft. Durch das Hinzufügen von lambdas können nun anonyme Codeblöcke einfach erstellt werden, und dies vermeidet die traditionelle Betonung der "erzwungenen Wiederverwendbarkeit" von C++, die durch benannte Typen ausgedrückt wird.

+0

Nette Idee, endlich ohne das Sprachkonstrukt zu implementieren. Aber try_finally sollte fi() aufrufen, auch wenn tr() nicht wirft. – dalle

+0

Auch ein Text wurde nach oben verschoben, so dass er auch die Frage beantwortet. –

0

Bearbeiten nach sechs Antworten.

Was ist dieses:

class Exception : public Exception { public: virtual bool isException() { return true; } }; 
class NoException : public Exception { public: bool isException() { return false; } }; 


Object *myObject = 0; 

try 
{ 
    try 
    { 
    myObject = new Object(); // Create an object (Might throw exception) 
    } 
    catch (Exception &e) 
    { 
    // Do something with exception (Might throw if unhandled) 
    } 

    throw NoException(); 
} 
catch (Exception &e) 
{ 
    delete myObject; 

    if (e.isException()) throw e; 
} 
+0

Wenn Ihr Fangausdruck eine weitere Ausnahme auslöst, wird delete nie aufgerufen. – Kibbee

+0

Objekt * myObject = 0; versuchen { myObject = new Object(); // Exception werfen } catch (Exception & e) { try {// etwas mit Ausnahme tun} catch {}} myObject löschen; Problem gelöst :-) –

+0

Weil Sie in den meisten Fällen die Ausnahme NICHT abfangen wollen. Und wenn Sie es fangen, sollten Sie es fast immer wieder auflösen. – Roddy

3

Ich denke, dass scope guard gute Arbeit leistet die einmaligen Fälle bei der Handhabung schließlich gut behandelt, während im allgemeineren Sinne, besser zu sein, weil es mehr behandelt als ein Strömungsweg gut.

1

Die Hauptanwendung ich für finally finden würde wäre, wenn mit C-Code zu tun, wie andere darauf hingewiesen, wo eine C-Ressource nur einmal verwendet werden kann oder zweimal im Code und nicht wirklich wert in eine RAH-konformen Struktur gewickelt wird. Das heißt, mit Lambdas scheint es einfach zu sein, nur eine benutzerdefinierte Logik durch einen Aufruf eines Funktionsobjekts aufzurufen, das wir in der Funktion selbst angeben. Der andere Anwendungsfall, den ich finden würde, ist für exotischen gemischten Code, der ausgeführt werden sollte, unabhängig davon, ob wir uns in einem normalen oder außergewöhnlichen Ausführungspfad befinden, wie das Drucken eines Zeitstempels oder etwas beim Beenden einer Funktion. Das ist so ein seltener Fall für mich, dass es wie Overkill erscheint, ein Sprachfeature nur dafür zu haben, und es ist immer noch so einfach mit Lambdas zu machen, ohne eine separate Klasse nur für diesen Zweck schreiben zu müssen.

In den meisten Fällen würde ich sehr eingeschränkte Anwendungsfälle finden, die eine so große Änderung der Sprache nicht wirklich rechtfertigen. Mein kleiner Wunschtraum ist jedoch eine Möglichkeit, innerhalb eines Objekts zu sagen, ob das Objekt durch einen normalen Ausführungspfad oder einen außergewöhnlichen Pfad zerstört wird.

Das würde die Überwachung des Bereichs vereinfachen, so dass ein Aufruf commit/dismiss nicht mehr erforderlich ist, um die Änderungen zu akzeptieren, ohne sie automatisch wiederherzustellen, wenn der Schutz des Oszilloskops zerstört wird. Die Idee ist, um dies zu ermöglichen:

ScopeGuard guard(...); 

// Cause external side effects. 
... 

// If we managed to reach this point without facing an exception, 
// dismiss/commit the changes so that the guard won't undo them 
// on destruction. 
guard.dismiss(); 

Um dies einfach geworden:

ScopeGuard guard(...); 

// Cause external side effects. 
... 

Ich fand immer die Notwendigkeit zu entlassen Umfang schützt ein wenig umständlich zu machen sowie fehleranfällig, da ich manchmal vergessen, sie zu entlassen, nur um sie alle Änderungen rückgängig zu machen, mich für einen Moment meinen Kopf kratzen, warum meine Operation schien überhaupt nichts zu tun, bis ich realisierte, "oops, habe ich vergessen, den Schutzumfang zu entlassen.". Es ist eine kleine Sache, aber meistens würde ich es so viel eleganter finden, die Notwendigkeit einer expliziten Entfernung des Schutzbereichs zu eliminieren, die möglich wäre, wenn sie innerhalb ihrer Destruktoren einfach sagen könnten, ob sie durch normale Ausführungspfade zerstört werden zeigen die Nebenwirkungen sollten beibehalten werden) oder außergewöhnliche (zu welchem ​​Zeitpunkt die Nebenwirkungen rückgängig gemacht werden sollten).

Es ist die kleinste Sache, aber in der härtesten Bereich der Ausnahme-Sicherheit, um richtig zu machen: externe Nebenwirkungen zurückrollen. Ich könnte nicht mehr von C++ verlangen, wenn es darum geht, lokale Ressourcen einfach zu zerstören. Es ist schon ziemlich ideal für diesen Zweck. Aber externe Nebenwirkungen zurückzudrängen, war in jeder Sprache, in der sie überhaupt auftreten können, immer schwierig, und jedes kleine bisschen Hilfe, um das zu erleichtern, ist etwas, das ich immer schätzen würde.

Verwandte Themen