Unterstützt C ++ "finally" -Blöcke? (Und was ist das für ein 'RAII', von dem ich immer wieder höre?)

Antworten:

273

Nein, C ++ unterstützt keine 'finally'-Blöcke. Der Grund dafür ist, dass C ++ stattdessen RAII unterstützt: "Resource Acquisition Is Initialization" - ein schlechter Name für ein wirklich nützliches Konzept.

Die Idee ist, dass der Destruktor eines Objekts für die Freigabe von Ressourcen verantwortlich ist. Wenn das Objekt eine automatische Speicherdauer hat, wird der Destruktor des Objekts aufgerufen, wenn der Block, in dem es erstellt wurde, beendet wird - auch wenn dieser Block bei Vorhandensein einer Ausnahme beendet wird. Hier ist die Erklärung von Bjarne Stroustrup des Themas.

Eine häufige Verwendung für RAII ist das Sperren eines Mutex:

// A class with implements RAII
class lock
{
    mutex &m_;

public:
    lock(mutex &m)
      : m_(m)
    {
        m.acquire();
    }
    ~lock()
    {
        m_.release();
    }
};

// A class which uses 'mutex' and 'lock' objects
class foo
{
    mutex mutex_; // mutex for locking 'foo' object
public:
    void bar()
    {
        lock scopeLock(mutex_); // lock object.

        foobar(); // an operation which may throw an exception

        // scopeLock will be destructed even if an exception
        // occurs, which will release the mutex and allow
        // other functions to lock the object and run.
    }
};

RAII vereinfacht auch die Verwendung von Objekten als Mitglieder anderer Klassen. Wenn die besitzende Klasse 'zerstört wird, wird die von der RAII-Klasse verwaltete Ressource freigegeben, da der Destruktor für die von RAII verwaltete Klasse als Ergebnis aufgerufen wird. Dies bedeutet, dass Sie bei Verwendung von RAII für alle Mitglieder einer Klasse, die Ressourcen verwalten, einen sehr einfachen, möglicherweise sogar standardmäßigen Destruktor für die Eigentümerklasse verwenden können, da die Lebensdauer der Mitgliederressourcen nicht manuell verwaltet werden muss . (Vielen Dank an Mike B für diesen Hinweis.)

Für diejenigen, die mit C # oder VB.NET vertraut sind, können Sie erkennen, dass RAII der deterministischen Zerstörung von .NET mit IDisposable- und 'using'-Anweisungen ähnlich ist . In der Tat sind die beiden Methoden sehr ähnlich. Der Hauptunterschied besteht darin, dass RAII jede Art von Ressource - einschließlich Speicher - deterministisch freigibt. Bei der Implementierung von IDisposable in .NET (auch in der .NET-Sprache C ++ / CLI) werden Ressourcen mit Ausnahme des Speichers deterministisch freigegeben. In .NET wird der Speicher nicht deterministisch freigegeben. Der Speicher wird nur während der Speicherbereinigungszyklen freigegeben.

 

† Einige Leute glauben, dass "Zerstörung ist Ressourcenabgabe" ein genauerer Name für die RAII-Sprache ist.

Kevin
quelle
18
"Zerstörung ist Ressourcenabgabe" - DIRR ... Nein, funktioniert bei mir nicht. = P
Erik Forbes
14
RAII steckt fest - es ändert sich wirklich nichts. Der Versuch wäre dumm. Sie müssen jedoch zugeben, dass "Resource Acquisition Is Initialization" immer noch ein ziemlich schlechter Name ist.
Kevin
162
SBRM == Umfang gebundenes Ressourcenmanagement
Johannes Schaub - litb
10
Jeder, der nicht nur Software im Allgemeinen entwickeln kann, geschweige denn verbesserte Techniken, kann keine würdige Entschuldigung für solch ein schreckliches Akronym geben.
Hardryv
54
Dies lässt Sie stecken, wenn Sie etwas bereinigen müssen, das nicht der Lebensdauer eines C ++ - Objekts entspricht. Ich denke, Sie haben am Ende Lifetime Equals C ++ Class Liftime oder sonst wird es hässlich (LECCLEOEIGU?).
Warren P
79

In C ++ ist das schließlich wegen RAII NICHT erforderlich.

RAII überträgt die Verantwortung für die Ausnahmesicherheit vom Benutzer des Objekts auf den Designer (und Implementierer) des Objekts. Ich würde argumentieren, dass dies der richtige Ort ist, da Sie dann die Ausnahmesicherheit nur einmal korrekt ausführen müssen (im Design / in der Implementierung). Wenn Sie schließlich verwenden, müssen Sie die Ausnahmesicherheit jedes Mal korrekt einstellen, wenn Sie ein Objekt verwenden.

Auch IMO sieht der Code ordentlicher aus (siehe unten).

Beispiel:

Ein Datenbankobjekt. Um sicherzustellen, dass die DB-Verbindung verwendet wird, muss sie geöffnet und geschlossen werden. Mit RAII kann dies im Konstruktor / Destruktor erfolgen.

C ++ Wie RAII

void someFunc()
{
    DB    db("DBDesciptionString");
    // Use the db object.

} // db goes out of scope and destructor closes the connection.
  // This happens even in the presence of exceptions.

Die Verwendung von RAII macht die korrekte Verwendung eines DB-Objekts sehr einfach. Das DB-Objekt wird sich durch die Verwendung eines Destruktors korrekt schließen, unabhängig davon, wie wir versuchen, es zu missbrauchen.

Java wie endlich

void someFunc()
{
    DB      db = new DB("DBDesciptionString");
    try
    {
        // Use the db object.
    }
    finally
    {
        // Can not rely on finaliser.
        // So we must explicitly close the connection.
        try
        {
            db.close();
        }
        catch(Throwable e)
        {
           /* Ignore */
           // Make sure not to throw exception if one is already propagating.
        }
    }
}

Bei der endgültigen Verwendung wird die korrekte Verwendung des Objekts an den Benutzer des Objekts delegiert. dh Es liegt in der Verantwortung des Objektbenutzers, die DB-Verbindung korrekt explizit zu schließen. Jetzt könnten Sie argumentieren, dass dies im Finalisierer möglich ist, aber Ressourcen möglicherweise nur eine begrenzte Verfügbarkeit oder andere Einschränkungen aufweisen. Daher möchten Sie im Allgemeinen die Freigabe des Objekts steuern und sich nicht auf das nicht deterministische Verhalten des Garbage Collectors verlassen.

Auch dies ist ein einfaches Beispiel.
Wenn Sie mehrere Ressourcen haben, die freigegeben werden müssen, kann der Code kompliziert werden.

Eine detailliertere Analyse finden Sie hier: http://accu.org/index.php/journals/236

Martin York
quelle
16
// Make sure not to throw exception if one is already propagating.Aus diesem Grund ist es für C ++ - Destruktoren wichtig, keine Ausnahmen auszulösen.
Cemafor
10
@Cemafor: Der Grund für C ++, keine Ausnahmen aus dem Destruktor zu werfen, unterscheidet sich von Java. In Java wird es funktionieren (Sie verlieren nur die ursprüngliche Ausnahme). In C ++ ist es wirklich schlecht. Der Punkt in C ++ ist jedoch, dass Sie dies nur einmal tun müssen (vom Designer der Klasse), wenn er den Destruktor schreibt. In Java müssen Sie dies am Verwendungsort tun. Es liegt also in der Verantwortung des Benutzers der Klasse, immer die gleiche Kesselplatte zu schreiben.
Martin York
1
Wenn es darum geht, "gebraucht" zu werden, brauchen Sie auch kein RAII. Lass es uns loswerden! :-) Spaß beiseite, RAII ist in vielen Fällen in Ordnung. Was RAII macht, ist umständlicher, wenn Sie Code ausführen möchten (nicht ressourcenbezogen), selbst wenn der obige Code vorzeitig zurückgegeben wurde. Dazu verwenden Sie entweder gotos oder trennen es in zwei Methoden.
Trinidad
1
@Trinidad: Es ist nicht einfach, wie Sie denken (da alle Ihre Vorschläge die schlechtesten möglichen Optionen zu wählen scheinen). Aus diesem Grund ist eine Frage möglicherweise ein besserer Ort, um dies zu untersuchen als die Kommentare.
Martin York
1
Kritik an "ist wegen RAII NICHT erforderlich": Es gibt viele Fälle, in denen das Hinzufügen von Ad-hoc-RAII zu viel Code für das Hinzufügen von Boilerplates wäre und ein Versuch-Endlich nur äußerst angemessen wäre.
Ceztko
63

RAII ist normalerweise besser, aber Sie können leicht die endgültige Semantik in C ++ haben. Mit einer winzigen Menge Code.

Außerdem geben die C ++ Core Guidelines endlich.

Hier ist ein Link zur Implementierung von GSL Microsoft und ein Link zur Implementierung von Martin Moene

Bjarne Stroustrup sagte mehrmals, dass alles, was in der GSL enthalten ist, irgendwann in den Standard aufgenommen werden soll. Es sollte also eine zukunftssichere Möglichkeit sein, sie endlich zu verwenden .

Sie können sich leicht implementieren, wenn Sie möchten, lesen Sie weiter.

In C ++ 11 erlaubt RAII und Lambdas endlich einen General zu machen:

namespace detail { //adapt to your "private" namespace
template <typename F>
struct FinalAction {
    FinalAction(F f) : clean_{f} {}
   ~FinalAction() { if(enabled_) clean_(); }
    void disable() { enabled_ = false; };
  private:
    F clean_;
    bool enabled_{true}; }; }

template <typename F>
detail::FinalAction<F> finally(F f) {
    return detail::FinalAction<F>(f); }

Anwendungsbeispiel:

#include <iostream>
int main() {
    int* a = new int;
    auto delete_a = finally([a] { delete a; std::cout << "leaving the block, deleting a!\n"; });
    std::cout << "doing something ...\n"; }

Die Ausgabe lautet:

doing something...
leaving the block, deleting a!

Persönlich habe ich dies einige Male verwendet, um sicherzustellen, dass der POSIX-Dateideskriptor in einem C ++ - Programm geschlossen wird.

Eine echte Klasse zu haben, die Ressourcen verwaltet und so jegliche Art von Lecks vermeidet, ist normalerweise besser, aber dies ist schließlich in den Fällen nützlich, in denen das Erstellen einer Klasse wie ein Overkill klingt.

Außerdem habe ich , wie es besser als andere Sprachen endlich da , wenn verwendet , natürlich Sie den Schließcode in der Nähe der Öffnungscode (in meinem Beispiel die schreiben neue und löschen ) und Zerstörung folgt Bau in LIFO - Reihenfolge wie üblich in C ++. Der einzige Nachteil ist, dass Sie eine automatische Variable erhalten, die Sie nicht wirklich verwenden, und die Lambda-Syntax macht sie etwas verrauscht (in meinem Beispiel in der vierten Zeile sind nur das Wort finally und der {} -Block rechts von Bedeutung, die Ruhe ist im Wesentlichen Lärm).

Ein anderes Beispiel:

 [...]
 auto precision = std::cout.precision();
 auto set_precision_back = finally( [precision, &std::cout]() { std::cout << std::setprecision(precision); } );
 std::cout << std::setprecision(3);

Das Deaktivierungsmitglied ist nützlich, wenn das endgültige nur im Fehlerfall aufgerufen werden muss. Wenn Sie beispielsweise ein Objekt in drei verschiedenen Containern kopieren müssen, können Sie die Option zum endgültigen Rückgängigmachen jeder Kopie einrichten und deaktivieren, nachdem alle Kopien erfolgreich waren. Wenn die Zerstörung nicht geworfen werden kann, stellen Sie die starke Garantie sicher.

Beispiel deaktivieren :

//strong guarantee
void copy_to_all(BIGobj const& a) {
    first_.push_back(a);
    auto undo_first_push = finally([first_&] { first_.pop_back(); });

    second_.push_back(a);
    auto undo_second_push = finally([second_&] { second_.pop_back(); });

    third_.push_back(a);
    //no necessary, put just to make easier to add containers in the future
    auto undo_third_push = finally([third_&] { third_.pop_back(); });

    undo_first_push.disable();
    undo_second_push.disable();
    undo_third_push.disable(); }

Wenn Sie C ++ 11 nicht verwenden können, können Sie es schließlich noch haben , aber der Code wird etwas langwieriger. Definieren Sie einfach eine Struktur mit nur einem Konstruktor und einem Destruktor, der Konstruktor nimmt Verweise auf alles, was benötigt wird, und der Destruktor führt die Aktionen aus, die Sie benötigen. Dies ist im Grunde das, was das Lambda manuell macht.

#include <iostream>
int main() {
    int* a = new int;

    struct Delete_a_t {
        Delete_a_t(int* p) : p_(p) {}
       ~Delete_a_t() { delete p_; std::cout << "leaving the block, deleting a!\n"; }
        int* p_;
    } delete_a(a);

    std::cout << "doing something ...\n"; }
Paolo.Bolzoni
quelle
Möglicherweise liegt ein Problem vor: In der Funktion 'finally (F f)' wird ein Objekt von FinalAction zurückgegeben, sodass der Dekonstruktor möglicherweise aufgerufen wird, bevor die Funktion endgültig zurückgegeben wird. Vielleicht sollten wir std :: function anstelle der Vorlage F verwenden.
user1633272
Beachten Sie, dass dies FinalActionim Grunde dasselbe ist wie die populäre ScopeGuardRedewendung, nur mit einem anderen Namen.
anderas
1
Ist diese Optimierung sicher?
Nulano
2
@ Paolo.Bolzoni Entschuldigung, dass Sie nicht früher geantwortet haben. Ich habe keine Benachrichtigung für Ihren Kommentar erhalten. Ich befürchtete, dass der finally-Block (in dem ich eine DLL-Funktion aufrufe) vor dem Ende des Bereichs aufgerufen würde (da die Variable nicht verwendet wird), habe aber seitdem eine Frage zu SO gefunden, die meine Sorgen beseitigt hat. Ich würde darauf verlinken, aber leider kann ich es nicht mehr finden.
Nulano
1
Die Funktion disable () ist eine Art Warze für Ihr ansonsten sauberes Design. Wenn Sie möchten, dass die endgültige Anweisung nur im Fehlerfall aufgerufen wird, verwenden Sie dann einfach die catch-Anweisung. Ist es nicht das, wofür es ist?
user2445507
32

RAII erleichtert nicht nur die Bereinigung mit stapelbasierten Objekten, sondern ist auch nützlich, da dieselbe automatische Bereinigung erfolgt, wenn das Objekt Mitglied einer anderen Klasse ist. Wenn die besitzende Klasse zerstört wird, wird die von der RAII-Klasse verwaltete Ressource bereinigt, da der dtor für diese Klasse als Ergebnis aufgerufen wird.

Dies bedeutet, dass Sie, wenn Sie das RAII-Nirvana erreichen und alle Mitglieder einer Klasse RAII verwenden (wie intelligente Zeiger), mit einem sehr einfachen (möglicherweise sogar standardmäßigen) Dtor für die Eigentümerklasse davonkommen können, da diese nicht manuell verwaltet werden muss Lebensdauer der Mitgliederressourcen.

Michael Burr
quelle
Das ist ein sehr guter Punkt. +1 für dich. Nicht viele andere Leute haben dich jedoch gewählt. Ich hoffe, es macht Ihnen nichts aus, dass ich meinen Beitrag so bearbeitet habe, dass er Ihre Kommentare enthält. (Ich habe dir natürlich Anerkennung gegeben.) Danke! :)
Kevin
30

Warum bieten selbst verwaltete Sprachen einen endgültigen Block, obwohl Ressourcen vom Garbage Collector ohnehin automatisch freigegeben werden?

Tatsächlich brauchen Sprachen, die auf Garbage Collectors basieren, "endlich" mehr. Ein Garbage Collector zerstört Ihre Objekte nicht rechtzeitig, sodass Sie sich nicht darauf verlassen können, dass nicht speicherbezogene Probleme korrekt behoben werden.

In Bezug auf dynamisch zugewiesene Daten würden viele argumentieren, dass Sie Smart-Pointer verwenden sollten.

Jedoch...

RAII überträgt die Verantwortung für die Ausnahmesicherheit vom Benutzer des Objekts auf den Designer

Leider ist dies sein eigener Untergang. Alte C-Programmiergewohnheiten sterben schwer. Wenn Sie eine Bibliothek verwenden, die in C oder einem sehr C-Stil geschrieben ist, wurde RAII nicht verwendet. Wenn Sie nicht das gesamte API-Front-End neu schreiben, müssen Sie genau damit arbeiten. Dann beißt der Mangel an "endlich" wirklich.

Philip Couling
quelle
13
Genau ... RAII scheint aus einer idealen Perspektive nett zu sein. Aber ich muss die ganze Zeit mit herkömmlichen C-APIs arbeiten (wie C-Funktionen in der Win32-API ...). Es ist sehr üblich, eine Ressource zu erwerben, die eine Art HANDLE zurückgibt, für deren Bereinigung eine Funktion wie CloseHandle (HANDLE) erforderlich ist. Mit try ... endlich ist eine gute Möglichkeit, mit möglichen Ausnahmen umzugehen. (Zum Glück sieht es so aus, als ob shared_ptr mit benutzerdefinierten Löschern und C ++ 11-Lambdas eine RAII-basierte Erleichterung bieten sollten, bei der nicht ganze Klassen geschrieben werden müssen, um eine API zu verpacken, die ich nur an einer Stelle verwende.)
James Johnston
7
@ JamesJohnston, es ist sehr einfach, eine Wrapper-Klasse zu schreiben, die jede Art von Handle enthält und RAII-Mechanik bietet. ATL bietet zum Beispiel eine Reihe von ihnen. Es scheint, dass Sie dies als zu viel Mühe betrachten, aber ich bin anderer Meinung, sie sind sehr klein und leicht zu schreiben.
Mark Ransom
5
Einfach ja, klein nein. Die Größe hängt von der Komplexität der Bibliothek ab, mit der Sie arbeiten.
Philip Couling
1
@MarkRansom: Gibt es einen Mechanismus, über den RAII etwas Intelligentes tun kann, wenn während der Bereinigung eine Ausnahme auftritt, während eine andere Ausnahme ansteht? In Systemen mit try / finally ist es möglich - wenn auch umständlich -, die Dinge so anzuordnen, dass sowohl die ausstehende Ausnahme als auch die Ausnahme, die während der Bereinigung aufgetreten sind, in einer neuen gespeichert werden CleanupFailedException. Gibt es einen plausiblen Weg, um mit RAII ein solches Ergebnis zu erzielen?
Supercat
3
@couling: Es gibt viele Fälle, in denen ein Programm eine SomeObject.DoSomething()Methode aufruft und wissen möchte, ob sie (1) erfolgreich war, (2) ohne Nebenwirkungen fehlgeschlagen ist, (3) mit Nebenwirkungen fehlgeschlagen ist, auf die der Aufrufer vorbereitet ist , oder (4) fehlgeschlagen mit Nebenwirkungen, die der Anrufer nicht bewältigen kann. Nur der Anrufer wird wissen, mit welchen Situationen er fertig werden kann und welche nicht. Was der Anrufer braucht, ist eine Möglichkeit zu wissen, wie die Situation ist. Es ist schade, dass es keinen Standardmechanismus für die Bereitstellung der wichtigsten Informationen zu einer Ausnahme gibt.
Supercat
9

Eine weitere "endgültige" Blockemulation mit C ++ 11 Lambda-Funktionen

template <typename TCode, typename TFinallyCode>
inline void with_finally(const TCode &code, const TFinallyCode &finally_code)
{
    try
    {
        code();
    }
    catch (...)
    {
        try
        {
            finally_code();
        }
        catch (...) // Maybe stupid check that finally_code mustn't throw.
        {
            std::terminate();
        }
        throw;
    }
    finally_code();
}

Hoffen wir, dass der Compiler den obigen Code optimiert.

Jetzt können wir Code wie folgt schreiben:

with_finally(
    [&]()
    {
        try
        {
            // Doing some stuff that may throw an exception
        }
        catch (const exception1 &)
        {
            // Handling first class of exceptions
        }
        catch (const exception2 &)
        {
            // Handling another class of exceptions
        }
        // Some classes of exceptions can be still unhandled
    },
    [&]() // finally
    {
        // This code will be executed in all three cases:
        //   1) exception was not thrown at all
        //   2) exception was handled by one of the "catch" blocks above
        //   3) exception was not handled by any of the "catch" block above
    }
);

Wenn Sie möchten, können Sie diese Redewendung in "try - finally" -Makros einbinden:

// Please never throw exception below. It is needed to avoid a compilation error
// in the case when we use "begin_try ... finally" without any "catch" block.
class never_thrown_exception {};

#define begin_try    with_finally([&](){ try
#define finally      catch(never_thrown_exception){throw;} },[&]()
#define end_try      ) // sorry for "pascalish" style :(

Jetzt ist der Block "finally" in C ++ 11 verfügbar:

begin_try
{
    // A code that may throw
}
catch (const some_exception &)
{
    // Handling some exceptions
}
finally
{
    // A code that is always executed
}
end_try; // Sorry again for this ugly thing

Persönlich mag ich die "Makro" -Version von "finally" nicht und würde es vorziehen, die reine "with_finally" -Funktion zu verwenden, obwohl eine Syntax in diesem Fall sperriger ist.

Sie können den obigen Code hier testen: http://coliru.stacked-crooked.com/a/1d88f64cb27b3813

PS

Wenn Sie einen endgültigen Block in Ihrem Code benötigen , passen Scoped Guards oder ON_FINALLY / ON_EXCEPTION- Makros wahrscheinlich besser zu Ihren Anforderungen.

Hier ist ein kurzes Anwendungsbeispiel ON_FINALLY / ON_EXCEPTION:

void function(std::vector<const char*> &vector)
{
    int *arr1 = (int*)malloc(800*sizeof(int));
    if (!arr1) { throw "cannot malloc arr1"; }
    ON_FINALLY({ free(arr1); });

    int *arr2 = (int*)malloc(900*sizeof(int));
    if (!arr2) { throw "cannot malloc arr2"; }
    ON_FINALLY({ free(arr2); });

    vector.push_back("good");
    ON_EXCEPTION({ vector.pop_back(); });

    ...
anton_rh
quelle
1
Die erste ist für mich die am besten lesbare aller auf dieser Seite vorgestellten Optionen. +1
Nikos
7

Es tut mir leid, dass ich einen so alten Thread ausgegraben habe, aber die folgenden Überlegungen enthalten einen schwerwiegenden Fehler:

RAII überträgt die Verantwortung für die Ausnahmesicherheit vom Benutzer des Objekts auf den Designer (und Implementierer) des Objekts. Ich würde argumentieren, dass dies der richtige Ort ist, da Sie dann die Ausnahmesicherheit nur einmal korrekt ausführen müssen (im Design / in der Implementierung). Wenn Sie schließlich verwenden, müssen Sie die Ausnahmesicherheit jedes Mal korrekt einstellen, wenn Sie ein Objekt verwenden.

Meistens müssen Sie sich mit dynamisch zugewiesenen Objekten, dynamischer Anzahl von Objekten usw. befassen. Innerhalb des Try-Blocks kann ein Code viele Objekte erstellen (wie viele zur Laufzeit bestimmt werden) und Zeiger darauf in einer Liste speichern. Dies ist kein exotisches Szenario, aber sehr häufig. In diesem Fall möchten Sie Dinge wie schreiben

void DoStuff(vector<string> input)
{
  list<Foo*> myList;

  try
  {    
    for (int i = 0; i < input.size(); ++i)
    {
      Foo* tmp = new Foo(input[i]);
      if (!tmp)
        throw;

      myList.push_back(tmp);
    }

    DoSomeStuff(myList);
  }
  finally
  {
    while (!myList.empty())
    {
      delete myList.back();
      myList.pop_back();
    }
  }
}

Natürlich wird die Liste selbst zerstört, wenn der Gültigkeitsbereich verlassen wird, aber das würde die von Ihnen erstellten temporären Objekte nicht bereinigen.

Stattdessen müssen Sie den hässlichen Weg gehen:

void DoStuff(vector<string> input)
{
  list<Foo*> myList;

  try
  {    
    for (int i = 0; i < input.size(); ++i)
    {
      Foo* tmp = new Foo(input[i]);
      if (!tmp)
        throw;

      myList.push_back(tmp);
    }

    DoSomeStuff(myList);
  }
  catch(...)
  {
  }

  while (!myList.empty())
  {
    delete myList.back();
    myList.pop_back();
  }
}

Außerdem: Warum bieten selbst verwaltete Sprachen einen endgültigen Block, obwohl Ressourcen vom Garbage Collector ohnehin automatisch freigegeben werden?

Hinweis: Mit "endlich" können Sie mehr tun als nur die Freigabe des Speichers.

Mephane
quelle
17
Verwaltete Sprachen müssen endgültig blockiert werden, da nur eine Art von Ressource automatisch verwaltet wird: Speicher. RAII bedeutet, dass alle Ressourcen auf die gleiche Weise behandelt werden können, sodass keine endgültige Notwendigkeit besteht. Wenn Sie in Ihrem Beispiel tatsächlich RAII verwendet haben (indem Sie intelligente Zeiger in Ihrer Liste anstelle von nackten verwenden), wäre der Code einfacher als Ihr "Endlich" -Beispiel. Und noch einfacher, wenn Sie den Rückgabewert von new nicht überprüfen - es ist ziemlich sinnlos, ihn zu überprüfen.
Myto
7
newgibt nicht NULL zurück, sondern
löst
5
Sie werfen eine wichtige Frage auf, aber es gibt zwei mögliche Antworten. Eine davon ist die von Myto - Verwenden Sie intelligente Zeiger für alle dynamischen Zuordnungen. Die andere besteht darin, Standardbehälter zu verwenden, die ihren Inhalt bei Zerstörung immer zerstören. In jedem Fall gehört jedes zugewiesene Objekt letztendlich einem statisch zugewiesenen Objekt, das es bei Zerstörung automatisch freigibt. Es ist wirklich schade, dass diese besseren Lösungen für Programmierer aufgrund der hohen Sichtbarkeit einfacher Zeiger und Arrays schwer zu finden sind.
j_random_hacker
4
C ++ 11 verbessert dies und enthält std::shared_ptrund std::unique_ptrdirekt in der stdlib.
u0b34a0f6ae
16
Der Grund, warum Ihr Beispiel so schrecklich aussieht, ist nicht, dass RAII fehlerhaft ist, sondern dass Sie es nicht verwendet haben. Raw-Zeiger sind nicht RAII.
Ben Voigt
6

FWIW, Microsoft Visual C ++ unterstützt schließlich try und wurde in der Vergangenheit in MFC-Apps verwendet, um schwerwiegende Ausnahmen abzufangen, die andernfalls zu einem Absturz führen würden. Beispielsweise;

int CMyApp::Run() 
{
    __try
    {
        int i = CWinApp::Run();
        m_Exitok = MAGIC_EXIT_NO;
        return i;
    }
    __finally
    {
        if (m_Exitok != MAGIC_EXIT_NO)
            FaultHandler();
    }
}

Ich habe dies in der Vergangenheit verwendet, um beispielsweise Sicherungen geöffneter Dateien vor dem Beenden zu speichern. Bestimmte JIT-Debugging-Einstellungen brechen diesen Mechanismus jedoch.

SmacL
quelle
4
Denken Sie daran, dass dies nicht wirklich C ++ - Ausnahmen sind, sondern SEH-Ausnahmen. Sie können beide in MS C ++ - Code verwenden. SEH ist ein OS-Ausnahmehandler, mit dem VB, .NET Ausnahmen implementiert.
Gbjbaanb
und Sie können SetUnhandledExceptionHandler verwenden, um einen 'globalen' nicht abgefangenen Ausnahmebehandler zu erstellen - für SEH-Ausnahmen.
Gbjbaanb
3
SEH ist schrecklich und verhindert auch, dass C ++ - Destruktoren aufgerufen werden
Paulm
6

Wie in den anderen Antworten ausgeführt, kann C ++ finallyähnliche Funktionen unterstützen. Die Implementierung dieser Funktionalität, die wahrscheinlich der Standardsprache am nächsten kommt, ist diejenige, die den C ++ - Kernrichtlinien beiliegt , einer Reihe von Best Practices für die Verwendung von C ++, die von Bjarne Stoustrup und Herb Sutter bearbeitet wurden. Eine Implementierung vonfinally ist Teil der Guidelines Support Library (GSL). In allen Richtlinien wird die Verwendung von finallyempfohlen, wenn mit Schnittstellen alten Stils gearbeitet wird, und es gibt auch eine eigene Richtlinie mit dem Titel Verwenden Sie ein final_action-Objekt, um die Bereinigung auszudrücken, wenn kein geeignetes Ressourcenhandle verfügbar ist .

C ++ unterstützt also nicht nur finally, es wird auch empfohlen, es in vielen gängigen Anwendungsfällen zu verwenden.

Eine beispielhafte Verwendung der GSL-Implementierung würde folgendermaßen aussehen:

#include <gsl/gsl_util.h>

void example()
{
    int handle = get_some_resource();
    auto handle_clean = gsl::finally([&handle] { clean_that_resource(handle); });

    // Do a lot of stuff, return early and throw exceptions.
    // clean_that_resource will always get called.
}

Die Implementierung und Verwendung von GSL ist der in der Antwort von Paolo.Bolzoni sehr ähnlich . Ein Unterschied besteht darin, dass dem von erstellten Objekt gsl::finally()der disable()Aufruf fehlt . Wenn Sie diese Funktionalität benötigen (z. B. um die Ressource zurückzugeben, sobald sie zusammengestellt ist und keine Ausnahmen auftreten müssen), bevorzugen Sie möglicherweise die Implementierung von Paolo. Andernfalls kommt die Verwendung von GSL der Verwendung standardisierter Funktionen so nahe wie möglich.

tobi_s
quelle
3

Nicht wirklich, aber Sie können sie in gewissem Umfang emulieren, zum Beispiel:

int * array = new int[10000000];
try {
  // Some code that can throw exceptions
  // ...
  throw std::exception();
  // ...
} catch (...) {
  // The finally-block (if an exception is thrown)
  delete[] array;
  // re-throw the exception.
  throw; 
}
// The finally-block (if no exception was thrown)
delete[] array;

Beachten Sie, dass der finally-Block möglicherweise selbst eine Ausnahme auslöst, bevor die ursprüngliche Ausnahme erneut ausgelöst wird, wodurch die ursprüngliche Ausnahme verworfen wird. Dies ist genau das gleiche Verhalten wie in einem Java-Endblock. Sie können returndie Try & Catch-Blöcke auch nicht verwenden .

bcmpinc
quelle
3
Ich bin froh, dass Sie erwähnt haben, dass der letzte Block werfen könnte. Es ist die Sache, die die meisten "Use RAII" -Antworten zu ignorieren scheinen. Um zu vermeiden, dass Sie den finally-Block zweimal schreiben müssen, können Sie so etwas wiestd::exception_ptr e; try { /*try block*/ } catch (...) { e = std::current_exception(); } /*finally block*/ if (e) std::rethrow_exception(e);
Sethobrien
1
Das ist alles was ich wissen wollte! Warum keine der anderen Antworten erklärte, dass ein Fang (...) + leerer Wurf; funktioniert fast wie ein Endblock? Manchmal braucht man es einfach.
VinGarcia
Die Lösung, die ich in meiner Antwort angegeben habe ( stackoverflow.com/a/38701485/566849 ), sollte es ermöglichen, Ausnahmen aus dem finallyBlock heraus auszulösen .
Fabio A.
3

Ich habe mir ein finallyMakro ausgedacht, das fast wie ¹ das finallySchlüsselwort in Java verwendet werden kann. es nutzt std::exception_ptrund Freunde, Lambda-Funktionen und std::promise, so erfordert es C++11oder darüber; Es verwendet auch die zusammengesetzte Anweisungsausdruck- GCC-Erweiterung, die auch von clang unterstützt wird.

WARNUNG : In einer früheren Version dieser Antwort wurde eine andere Implementierung des Konzepts mit vielen weiteren Einschränkungen verwendet.

Definieren wir zunächst eine Hilfsklasse.

#include <future>

template <typename Fun>
class FinallyHelper {
    template <typename T> struct TypeWrapper {};
    using Return = typename std::result_of<Fun()>::type;

public:    
    FinallyHelper(Fun body) {
        try {
            execute(TypeWrapper<Return>(), body);
        }
        catch(...) {
            m_promise.set_exception(std::current_exception());
        }
    }

    Return get() {
        return m_promise.get_future().get();
    }

private:
    template <typename T>
    void execute(T, Fun body) {
        m_promise.set_value(body());
    }

    void execute(TypeWrapper<void>, Fun body) {
        body();
    }

    std::promise<Return> m_promise;
};

template <typename Fun>
FinallyHelper<Fun> make_finally_helper(Fun body) {
    return FinallyHelper<Fun>(body);
}

Dann gibt es das eigentliche Makro.

#define try_with_finally for(auto __finally_helper = make_finally_helper([&] { try 
#define finally });                         \
        true;                               \
        ({return __finally_helper.get();})) \
/***/

Es kann wie folgt verwendet werden:

void test() {
    try_with_finally {
        raise_exception();
    }    

    catch(const my_exception1&) {
        /*...*/
    }

    catch(const my_exception2&) {
        /*...*/
    }

    finally {
        clean_it_all_up();
    }    
}

Die Verwendung von std::promisemacht die Implementierung sehr einfach, führt aber wahrscheinlich auch zu unnötigem Overhead, der vermieden werden könnte, indem nur die erforderlichen Funktionen von neu implementiert werden std::promise.


¹ CAVEAT: Es gibt einige Dinge, die nicht ganz so funktionieren wie die Java-Version von finally. Aus dem Kopf:

  1. Es ist nicht möglich, mit der breakAnweisung aus den Blöcken von tryund aus einer äußeren Schleife auszubrechen catch(), da sie innerhalb einer Lambda-Funktion leben.
  2. catch()Nach dem muss mindestens ein Block stehen try: Es handelt sich um eine C ++ - Anforderung.
  3. Wenn die Funktion einen anderen Rückgabewert als void hat, aber in den Blöcken tryund keine Rückgabe vorhanden ist, schlägt die catch()'sKompilierung fehl, da das finallyMakro zu Code erweitert wird, der a zurückgeben möchte void. Dies könnte eine Leere sein, die durch eine Art finally_noreturnMakro entsteht.

Alles in allem weiß ich nicht, ob ich dieses Zeug jemals selbst benutzen würde, aber es hat Spaß gemacht, damit zu spielen. :) :)

Fabio A.
quelle
Ja, es war nur ein schneller Hack, aber wenn der Programmierer weiß, was er tut, könnte es trotzdem nützlich sein.
Fabio A.
@ MarkLakata, ich habe den Beitrag mit einer besseren Implementierung aktualisiert, die das Auslösen von Ausnahmen und Rückgaben unterstützt.
Fabio A.
Sieht gut aus. Sie könnten Caveat 2 loswerden, indem Sie einfach einen unmöglichen catch(xxx) {}Block am Anfang des finallyMakros einfügen, wobei xxx ein Scheintyp ist, nur um mindestens einen Catch-Block zu haben.
Mark Lakata
@ MarkLakata, ich habe auch daran gedacht, aber das würde es unmöglich machen, es zu benutzen catch(...), nicht wahr ?
Fabio A.
Das glaube ich nicht. Bilden Sie einfach einen obskuren Typ xxxin einem privaten Namespace, der niemals verwendet wird.
Mark Lakata
2

Ich habe einen Anwendungsfall, in dem ich denke, finally dass dies ein durchaus akzeptabler Teil der C ++ 11-Sprache sein sollte, da ich denke, dass es aus Sicht des Flusses einfacher zu lesen ist. Mein Anwendungsfall ist eine Consumer / Producer-Kette von Threads, nullptrbei der am Ende des Laufs ein Sentinel gesendet wird, um alle Threads herunterzufahren.

Wenn C ++ dies unterstützt, soll Ihr Code folgendermaßen aussehen:

    extern Queue downstream, upstream;

    int Example()
    {
        try
        {
           while(!ExitRequested())
           {
             X* x = upstream.pop();
             if (!x) break;
             x->doSomething();
             downstream.push(x);
           } 
        }
        finally { 
            downstream.push(nullptr);
        }
    }

Ich denke, dies ist logischer, als Ihre endgültige Deklaration an den Anfang der Schleife zu setzen, da sie nach dem Beenden der Schleife auftritt ... aber das ist Wunschdenken, weil wir es in C ++ nicht tun können. Beachten Sie, dass die Warteschlange downstreammit einem anderen Thread verbunden ist, sodass Sie den Sentinel nicht push(nullptr)in den Destruktor von downstreameinfügen können, da er zu diesem Zeitpunkt nicht zerstört werden kann. Er muss am Leben bleiben, bis der andere Thread den empfängt nullptr.

So verwenden Sie eine RAII-Klasse mit Lambda, um dasselbe zu tun:

    class Finally
    {
    public:

        Finally(std::function<void(void)> callback) : callback_(callback)
        {
        }
        ~Finally()
        {
            callback_();
        }
        std::function<void(void)> callback_;
    };

und so benutzt du es:

    extern Queue downstream, upstream;

    int Example()
    {
        Finally atEnd([](){ 
           downstream.push(nullptr);
        });
        while(!ExitRequested())
        {
           X* x = upstream.pop();
           if (!x) break;
           x->doSomething();
           downstream.push(x);
        }
    }
Mark Lakata
quelle
Hallo, ich glaube, meine obige Antwort ( stackoverflow.com/a/38701485/566849 ) erfüllt Ihre Anforderungen vollständig.
Fabio A.
1

Wie viele Leute angegeben haben, besteht die Lösung darin, C ++ 11-Funktionen zu verwenden, um endgültige Blockierungen zu vermeiden. Eines der Merkmale ist unique_ptr.

Hier ist Mephanes Antwort, die mit RAII-Mustern geschrieben wurde.

#include <vector>
#include <memory>
#include <list>
using namespace std;

class Foo
{
 ...
};

void DoStuff(vector<string> input)
{
    list<unique_ptr<Foo> > myList;

    for (int i = 0; i < input.size(); ++i)
    {
      myList.push_back(unique_ptr<Foo>(new Foo(input[i])));
    }

    DoSomeStuff(myList);
}

Eine weitere Einführung in die Verwendung von unique_ptr mit C ++ Standard Library-Containern finden Sie hier

Mark Lakata
quelle
0

Ich möchte eine Alternative anbieten.

Wenn Sie möchten, dass finally block immer aufgerufen wird, setzen Sie es einfach nach dem letzten catch-Block (was wahrscheinlich sein sollte catch( ... ), um eine nicht bekannte Ausnahme abzufangen).

try{
   // something that might throw exception
} catch( ... ){
   // what to do with uknown exception
}

//final code to be called always,
//don't forget that it might throw some exception too
doSomeCleanUp(); 

Wenn Sie beim Auslösen einer Ausnahme als letztes eine endgültige Blockierung durchführen möchten, können Sie eine boolesche lokale Variable verwenden. Vor dem Ausführen setzen Sie sie auf false und setzen die wahre Zuweisung ganz am Ende des try-Blocks. Nach dem catch-Block wird nach der Variablen gesucht Wert:

bool generalAppState = false;
try{
   // something that might throw exception

   //the very end of try block:
   generalAppState = true;
} catch( ... ){
   // what to do with uknown exception
}

//final code to be called only when exception was thrown,
//don't forget that it might throw some exception too
if( !generalAppState ){
   doSomeCleanUpOfDirtyEnd();
}

//final code to be called only when no exception is thrown
//don't forget that it might throw some exception too
else{
   cleanEnd();
}
jave.web
quelle
Dies funktioniert nicht, da der Sinn eines finally-Blocks darin besteht, eine Bereinigung durchzuführen, selbst wenn der Code zulassen sollte, dass eine Ausnahme den Codeblock verlässt. Bedenken Sie: `versuchen Sie {// Sachen, die möglicherweise" B "werfen} catch (A & a) {} endlich {// wenn C ++ es hätte ... // Sachen, die passieren müssen, selbst wenn" B "geworfen wird. } // wird nicht ausgeführt, wenn "B" ausgelöst wird. `IMHO besteht der Punkt der Ausnahmen darin, den Fehlerbehandlungscode zu reduzieren , sodass Fangblöcke, wo immer ein Wurf auftreten könnte, kontraproduktiv sind. Aus diesem Grund hilft RAII: Bei großzügiger Anwendung sind Ausnahmen in der oberen und unteren Schicht am wichtigsten.
stämmig
1
@burlyearly obwohl Ihre Meinung nicht heilig ist, verstehe ich, aber in C ++ ist so etwas nicht so, so dass Sie dies als eine oberste Ebene betrachten müssen, die dieses Verhalten emuliert.
jave.web
DOWNVOTE = BITTE KOMMENTIEREN :)
jave.web
0

Ich denke auch, dass RIIA kein voll nützlicher Ersatz für die Ausnahmebehandlung und ein endgültiges Problem ist. Übrigens denke ich auch, dass RIIA ein schlechter Name ist. Ich nenne diese Arten von Klassen "Hausmeister" und benutze sie eine Menge. In 95% der Fälle, in denen sie weder Ressourcen initialisieren noch erwerben, wenden sie Änderungen auf einer bestimmten Basis an oder nehmen etwas, das bereits eingerichtet wurde, und stellen sicher, dass es zerstört wird. Dies ist der offizielle Mustername, der vom Internet besessen ist. Ich werde missbraucht, weil ich sogar vorschlage, mein Name könnte besser sein.

Ich halte es einfach nicht für vernünftig zu verlangen, dass für jede komplizierte Einrichtung einer Ad-hoc-Liste von Dingen eine Klasse geschrieben werden muss, um sie zu enthalten, um Komplikationen beim Reinigen zu vermeiden, obwohl mehrere gefangen werden müssen Ausnahmetypen, wenn dabei etwas schief geht. Dies würde zu vielen Ad-hoc-Kursen führen, die sonst einfach nicht notwendig wären.

Ja, es ist in Ordnung für Klassen, die für die Verwaltung einer bestimmten Ressource ausgelegt sind, oder für allgemeine Klassen, die für die Verarbeitung einer Reihe ähnlicher Ressourcen ausgelegt sind. Aber selbst wenn alle beteiligten Dinge solche Wrapper haben, kann die Koordination der Bereinigung nicht nur ein einfacher Aufruf von Destruktoren in umgekehrter Reihenfolge sein.

Ich denke, es ist absolut sinnvoll für C ++, endlich eine zu haben. Ich meine, Herrgott, in den letzten Jahrzehnten wurden so viele Kleinigkeiten darauf geklebt, dass es den Anschein hat, als würden seltsame Leute plötzlich konservativ gegenüber so etwas wie endlich, was sehr nützlich sein könnte und wahrscheinlich nicht annähernd so kompliziert wie einige andere Dinge, die es gewesen sind fügte hinzu (obwohl das nur eine Vermutung von meiner Seite ist.)

Dean Roddey
quelle
-2
try
{
  ...
  goto finally;
}
catch(...)
{
  ...
  goto finally;
}
finally:
{
  ...
}
Unbehandelte Ausnahme
quelle
35
Nette Redewendung, aber es ist nicht ganz dasselbe. Wenn Sie in den try-Block oder catch zurückkehren, wird Ihr 'finally :'-Code nicht durchlaufen.
Edward KMETT
10
Es lohnt sich, diese falsche Antwort (mit 0) beizubehalten, da Edward Kmett eine sehr wichtige Unterscheidung vorbringt.
Mark Lakata
12
Noch größerer Fehler (IMO): Dieser Code frisst alle Ausnahmen, was finallynicht der Fall ist.
Ben Voigt