Il C ++ supporta i blocchi "finalmente"? (E che cos'è questa "RAII" di cui continuo a sentire?)


Risposte:


273

No, C ++ non supporta i blocchi "finalmente". Il motivo è che C ++ invece supporta RAII: "L'acquisizione delle risorse è inizializzazione" - un nome scadente per un concetto davvero utile.

L'idea è che il distruttore di un oggetto sia responsabile della liberazione delle risorse. Quando l'oggetto ha una durata di memorizzazione automatica, il distruttore dell'oggetto verrà chiamato quando il blocco in cui è stato creato esce - anche quando quel blocco viene chiuso in presenza di un'eccezione. Ecco la spiegazione di Bjarne Stroustrup sull'argomento.

Un uso comune per RAII è il blocco di un 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 semplifica anche l'uso di oggetti come membri di altre classi. Quando la classe proprietaria "viene distrutta, la risorsa gestita dalla classe RAII viene rilasciata perché di conseguenza viene chiamato il distruttore per la classe gestita RAII. Ciò significa che quando si utilizza RAII per tutti i membri di una classe che gestiscono risorse, è possibile cavarsela utilizzando un distruttore molto semplice, forse anche predefinito, per la classe proprietaria poiché non è necessario gestire manualmente la durata delle risorse dei membri . (Grazie a Mike B per averlo segnalato.)

Per coloro che hanno familiarità con C # o VB.NET, potresti riconoscere che RAII è simile alla distruzione deterministica di .NET usando le istruzioni IDisposable e "using" . In effetti, i due metodi sono molto simili. La differenza principale è che RAII rilascerà in modo deterministico qualsiasi tipo di risorsa, inclusa la memoria. Quando si implementa IDisposable in .NET (anche il linguaggio .NET C ++ / CLI), le risorse verranno rilasciate in modo deterministico ad eccezione della memoria. In .NET, la memoria non viene rilasciata in modo deterministico; la memoria viene rilasciata solo durante i cicli di garbage collection.

 

† Alcune persone credono che "La distruzione sia rinuncia alle risorse" è un nome più accurato per il linguaggio RAII.


18
"La distruzione è rinuncia alla risorsa" - DIRR ... No, non funziona per me. = P
Erik Forbes,

14
RAII è bloccato: non è possibile cambiarlo. Cercare di farlo sarebbe sciocco. Tuttavia, devi ammettere che "L'acquisizione delle risorse è inizializzazione" è ancora un nome piuttosto scadente.
Kevin,

162
SBRM == Scope Bound Resource Management
Johannes Schaub - litb

10
Chiunque abbia l'abilità di progettare non solo il software in generale, per non parlare delle tecniche migliorate, non può fornire una valida scusa per un acronimo così orrendo.
Hardryv,

54
Questo ti lascia bloccato quando hai qualcosa da ripulire che non corrisponde alla durata di nessun oggetto C ++. Immagino che tu finisca con Lifetime Equals Classe C ++ Liftime O altrimenti diventa brutto (LECCLEOEIGU?).
Warren P,

79

In C ++, infine, NON è richiesto a causa di RAII.

RAII sposta la responsabilità della sicurezza delle eccezioni dall'utente dell'oggetto al progettista (e implementatore) dell'oggetto. Direi che questo è il posto giusto in quanto è necessario solo una volta corretta la sicurezza delle eccezioni (nella progettazione / implementazione). Usando finalmente è necessario ottenere la sicurezza delle eccezioni corretta ogni volta che si utilizza un oggetto.

Anche IMO il codice sembra più ordinato (vedi sotto).

Esempio:

Un oggetto di database. Per assicurarsi che la connessione DB sia utilizzata, deve essere aperta e chiusa. Usando RAII questo può essere fatto nel costruttore / distruttore.

C ++ Come 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.

L'uso di RAII rende molto semplice l'utilizzo corretto di un oggetto DB. L'oggetto DB si chiuderà correttamente mediante l'uso di un distruttore, indipendentemente da come lo proviamo e lo abusiamo.

Java piace finalmente

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.
        }
    }
}

Quando si utilizza infine l'uso corretto dell'oggetto viene delegato all'utente dell'oggetto. vale a dire l'utente dell'oggetto deve chiudere correttamente la connessione DB in modo corretto. Ora potresti sostenere che ciò può essere fatto nel finalizzatore, ma le risorse possono avere disponibilità limitata o altri vincoli e quindi in genere vuoi controllare il rilascio dell'oggetto e non fare affidamento sul comportamento non deterministico del garbage collector.

Anche questo è un semplice esempio.
Quando hai più risorse che devono essere rilasciate, il codice può diventare complicato.

Un'analisi più dettagliata può essere trovata qui: http://accu.org/index.php/journals/236


16
// Make sure not to throw exception if one is already propagating.È importante che i distruttori C ++ non generino eccezioni anche per questo motivo.
Cema per

10
@Cemafor: il motivo per cui C ++ non getta eccezioni al distruttore è diverso da Java. In Java funzionerà (hai appena perso l'eccezione originale). In C ++ è davvero male. Ma il punto in C ++ è che devi farlo una sola volta (dal progettista della classe) quando scrive il distruttore. In Java devi farlo nel punto di utilizzo. Quindi è responsabilità dell'utente della classe scrivere la stessa piastra della caldaia molto tempo.
Martin York,

1
Se è una questione di "bisogno", non hai nemmeno bisogno di RAII. Liberiamoci di esso! :-) Scherzi a parte, RAII va bene per molti casi. Ciò che RAII rende più ingombranti sono i casi in cui si desidera eseguire un codice (non relativo alle risorse) anche se il codice sopra riportato è stato restituito in anticipo. Per quello o usi goto o lo separi in due metodi.
Trinidad,

1
@Trinidad: Non è semplice come pensi (dato che tutti i tuoi suggerimenti sembrano scegliere le peggiori opzioni possibili). Ecco perché una domanda potrebbe essere un posto migliore per esplorarlo rispetto ai commenti.
Martin York,

1
Criticare il "NON è necessario a causa di RAII": ci sono molti casi in cui l'aggiunta di RAII ad hoc sarebbe troppo codice da aggiungere, e provare-finalmente sarebbe estremamente appropriato.
Ceztko,

63

RAII di solito è meglio, ma puoi avere facilmente la semantica di fine in C ++. Utilizzando una piccola quantità di codice.

Inoltre, le Linee guida di base C ++ danno finalmente.

Ecco un collegamento all'implementazione Microsoft GSL e un collegamento all'implementazione Martin Moene

Bjarne Stroustrup più volte ha detto che tutto ciò che è nella GSL significava andare allo standard alla fine. Quindi dovrebbe essere un modo a prova di futuro da usare finalmente .

Puoi facilmente implementare te stesso se vuoi, continua a leggere.

In C ++ 11 RAII e lambdas consentono finalmente di fare un generale:

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); }

esempio di utilizzo:

#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"; }

l'output sarà:

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

Personalmente l'ho usato alcune volte per assicurarmi di chiudere il descrittore di file POSIX in un programma C ++.

Avere una vera classe che gestisce le risorse e quindi evita qualsiasi tipo di perdita è di solito meglio, ma questo alla fine è utile nei casi in cui fare una classe suona come un eccesso.

Inoltre, mi piace meglio di altri linguaggi , infine, perché se usato, naturalmente, si scrive il codice di chiusura vicina il codice di apertura (nel mio esempio la nuova e di eliminazione ) costruzione e distruzione seguito in ordine LIFO come al solito in C ++. L'unico aspetto negativo è che ottieni una variabile automatica che non usi davvero e la sintassi lambda la rende un po 'rumorosa (nel mio esempio nella quarta riga solo la parola finalmente e il blocco {} a destra sono significativi, il il resto è essenzialmente rumore).

Un altro esempio:

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

Il membro di disabilitazione è utile se deve essere chiamato infine solo in caso di errore. Ad esempio, devi copiare un oggetto in tre contenitori diversi, puoi infine impostare il comando per annullare ogni copia e disabilitarla dopo che tutte le copie hanno esito positivo. In questo modo, se la distruzione non può essere lanciata, si garantisce una forte garanzia.

disabilita esempio:

//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(); }

Se non è possibile utilizzare C ++ 11 è ancora possibile avere finalmente , ma il codice diventa un po 'più lungo senza fiato. Basta definire una struttura con solo un costruttore e un distruttore, il costruttore prende riferimenti a tutto ciò che è necessario e il distruttore fa le azioni necessarie. Questo è fondamentalmente ciò che fa la lambda, fatto manualmente.

#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"; }

Potrebbe esserci un possibile problema: nella funzione 'finally (F f)' restituisce un oggetto di FinalAction, quindi il decostruttore potrebbe essere chiamato prima di restituire finalmente la funzione. Forse dovremmo usare la funzione std :: invece del modello F.
user1633272

Nota che FinalActionè sostanzialmente lo stesso del ScopeGuardlinguaggio popolare , solo con un nome diverso.
anderas,

1
Questa ottimizzazione è sicura?
Nulano,

2
@ Paolo.Bolzoni Ci scusiamo per non aver risposto prima, non ho ricevuto una notifica per il tuo commento. Ero preoccupato che il blocco finally (in cui chiamo una funzione DLL) sarebbe stato chiamato prima della fine dell'ambito (perché la variabile non è utilizzata), ma da allora ho trovato una domanda su SO che ha chiarito le mie preoccupazioni. Vorrei collegarmi ad esso, ma purtroppo non riesco più a trovarlo.
Nulano,

1
La funzione disable () è una specie di verruca sul tuo design altrimenti pulito. Se vuoi che il fine sia chiamato solo in caso di fallimento, allora perché non usare semplicemente l'istruzione catch? Non è quello che serve?
user2445507

32

Oltre a semplificare la pulizia con oggetti basati su stack, RAII è utile anche perché la stessa pulizia "automatica" si verifica quando l'oggetto fa parte di un'altra classe. Quando la classe proprietaria viene distrutta, la risorsa gestita dalla classe RAII viene ripulita perché il dtor per quella classe viene chiamato di conseguenza.

Ciò significa che quando raggiungi il nirvana di RAII e tutti i membri di una classe usano RAII (come i puntatori intelligenti), puoi cavartela con un dtor molto semplice (forse anche predefinito) per la classe del proprietario poiché non è necessario gestirne manualmente durata delle risorse dei membri.


Questo è un ottimo punto. +1 a te. Tuttavia, non molte altre persone ti hanno votato. Spero non ti dispiaccia che ho modificato il mio post per includere i tuoi commenti. (Ti ho dato credito ovviamente.) Grazie! :)
Kevin,

30

perché anche le lingue gestite forniscono un blocco finale nonostante le risorse vengano automaticamente dislocate dal garbage collector?

In realtà, le lingue basate sui Garbage Collector necessitano "finalmente" di più. Un garbage collector non distrugge i tuoi oggetti in modo tempestivo, quindi non è possibile fare affidamento su come risolvere correttamente i problemi relativi alla non memoria.

In termini di dati allocati dinamicamente, molti sosterrebbero che dovresti usare gli smart-pointer.

Però...

RAII sposta la responsabilità della sicurezza delle eccezioni dall'utilizzatore dell'oggetto al progettista

Purtroppo questa è la sua rovina. Le vecchie abitudini di programmazione C sono dure a morire. Quando stai usando una libreria scritta in stile C o molto C, RAII non sarà usato. A meno di riscrivere l'intero front-end API, è proprio quello con cui devi lavorare. Quindi la mancanza di "finalmente" morde davvero.


13
Esatto ... RAII sembra bello da una prospettiva ideale. Ma devo sempre lavorare con le API C convenzionali (come le funzioni in stile C nell'API Win32 ...). È molto comune acquisire una risorsa che restituisce una sorta di HANDLE, che quindi richiede alcune funzioni come CloseHandle (HANDLE) per ripulire. Usare try ... finalmente è un bel modo di gestire possibili eccezioni. (Fortunatamente, sembra che shared_ptr con deleter personalizzati e C ++ 11 lambdas dovrebbe fornire un sollievo basato su RAII che non richiede la scrittura di intere classi per avvolgere alcune API che uso solo in un posto.).
James Johnston,

7
@JamesJohnston, è molto facile scrivere una classe wrapper che contiene qualsiasi tipo di handle e fornisce meccanica RAII. ATL ne fornisce un sacco, ad esempio. Sembra che lo consideri un problema eccessivo, ma non sono d'accordo, sono molto piccoli e facili da scrivere.
Mark Ransom il

5
Semplice sì, piccolo no. Le dimensioni dipendono dalla complessità della libreria con cui stai lavorando.
Philip Couling,

1
@MarkRansom: esiste qualche meccanismo attraverso il quale RAII può fare qualcosa di intelligente se si verifica un'eccezione durante la pulizia mentre un'altra è in sospeso? Nei sistemi con try / finally, è possibile - anche se scomodo - organizzare le cose in modo che l'eccezione in sospeso e l'eccezione verificatesi durante la pulizia vengano entrambe archiviate in una nuova CleanupFailedException. Esiste un modo plausibile per ottenere questo risultato usando RAII?
supercat

3
@couling: ci sono molti casi in cui un programma chiamerà un SomeObject.DoSomething()metodo e vorrà sapere se (1) è riuscito, (2) ha fallito senza effetti collaterali , (3) ha fallito con effetti collaterali che il chiamante è pronto a far fronte o (4) non riuscito con effetti collaterali che il chiamante non può far fronte. Solo il chiamante saprà quali situazioni può e non può affrontare; ciò di cui il chiamante ha bisogno è un modo per sapere qual è la situazione. È un peccato che non esista un meccanismo standard per fornire le informazioni più importanti su un'eccezione.
supercat

9

Un'altra emulazione di blocco "finalmente" che utilizza le funzioni lambda C ++ 11

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();
}

Speriamo che il compilatore ottimizzi il codice sopra.

Ora possiamo scrivere codice in questo modo:

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
    }
);

Se lo desideri, puoi avvolgere questo idioma in macro "prova - finalmente":

// 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 :(

Ora il blocco "finalmente" è disponibile in C ++ 11:

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

Personalmente non mi piace la versione "macro" di "finalmente" idioma e preferirei usare la funzione "with_finally" pura anche se in questo caso una sintassi è più voluminosa.

Puoi testare il codice qui sopra: http://coliru.stacked-crooked.com/a/1d88f64cb27b3813

PS

Se hai bisogno di un blocco finalmente nel tuo codice, le protezioni con ambito o le macro ON_FINALLY / ON_EXCEPTION probabilmente risponderanno meglio alle tue esigenze.

Ecco un breve esempio di utilizzo 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(); });

    ...

1
Il primo è per me il più leggibile tra tutte le opzioni presentate in questa pagina. +1
Nikos,

7

Ci scusiamo per aver scavato un thread così vecchio, ma c'è un errore grave nel seguente ragionamento:

RAII sposta la responsabilità della sicurezza delle eccezioni dall'utente dell'oggetto al progettista (e implementatore) dell'oggetto. Direi che questo è il posto giusto in quanto è necessario solo una volta corretta la sicurezza delle eccezioni (nella progettazione / implementazione). Usando finalmente è necessario ottenere la sicurezza delle eccezioni corretta ogni volta che si utilizza un oggetto.

Più spesso, devi occuparti di oggetti allocati dinamicamente, numeri dinamici di oggetti ecc. All'interno del blocco try, alcuni codici potrebbero creare molti oggetti (quanti sono determinati in fase di esecuzione) e memorizzare i puntatori su di essi in un elenco. Ora, questo non è uno scenario esotico, ma molto comune. In questo caso, vorresti scrivere cose come

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();
    }
  }
}

Ovviamente l'elenco stesso verrà distrutto quando si esce dall'ambito, ma ciò non ripulirebbe gli oggetti temporanei che hai creato.

Invece, devi seguire la brutta strada:

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();
  }
}

Inoltre: perché anche le lingue gestite forniscono un blocco finalmente nonostante le risorse vengano automaticamente dislocate dal garbage collector?

Suggerimento: c'è di più che puoi fare con "finalmente" oltre alla semplice deallocazione della memoria.


17
Le lingue gestite hanno bisogno di blocchi finalmente proprio perché viene gestito automaticamente solo un tipo di risorsa: la memoria. RAII significa che tutte le risorse possono essere gestite allo stesso modo, quindi alla fine non è necessario. Se in realtà hai usato RAII nel tuo esempio (usando i puntatori intelligenti nella tua lista invece di quelli nudi), il codice sarebbe più semplice del tuo esempio "finalmente". E ancora più semplice se non controlli il valore di ritorno di new - controllarlo è praticamente inutile.
Myto,

7
newnon restituisce NULL, genera invece un'eccezione
Hasturkun,

5
Sollevi una domanda importante, ma ha 2 possibili risposte. Uno è quello dato da Myto: utilizzare i puntatori intelligenti per tutte le allocazioni dinamiche. L'altro è usare contenitori standard, che distruggono sempre il loro contenuto dopo la distruzione. Ad ogni modo, ogni oggetto allocato è in ultima analisi di proprietà di un oggetto allocato staticamente che lo libera automaticamente alla distruzione. È un vero peccato che queste soluzioni migliori siano difficili da scoprire per i programmatori a causa dell'alta visibilità di puntatori e matrici semplici.
j_random_hacker il

4
C ++ 11 migliora questo e include std::shared_ptre std::unique_ptrdirettamente nello stdlib.
u0b34a0f6ae,

16
Il motivo per cui il tuo esempio è così orribile non è perché RAII è difettoso, piuttosto perché non sei riuscito a usarlo. I puntatori non elaborati non sono RAII.
Ben Voigt,

6

FWIW, Microsoft Visual C ++ supporta try, finalmente ed è stato storicamente utilizzato nelle app MFC come metodo per rilevare gravi eccezioni che altrimenti comporterebbero un arresto anomalo. Per esempio;

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

Ho usato questo in passato per fare cose come salvare i backup di file aperti prima di uscire. Tuttavia, alcune impostazioni di debug JIT interromperanno questo meccanismo.


4
tenere presente che non si tratta in realtà di eccezioni C ++, ma di SEH. È possibile utilizzare entrambi nel codice MS C ++. SEH è un gestore di eccezioni del sistema operativo che è il modo in cui VB, .NET implementa le eccezioni.
gbjbaanb,

e puoi usare SetUnhandledExceptionHandler per creare un gestore di eccezioni non intercettato "globale" - per le eccezioni SEH.
gbjbaanb,

3
SEH è orribile e impedisce anche di chiamare i distruttori C ++
paulm

6

Come sottolineato nelle altre risposte, C ++ può supportare finallyfunzionalità simili. L'implementazione di questa funzionalità che è probabilmente più vicina a far parte del linguaggio standard è quella che accompagna le Linee guida di base C ++ , un insieme di migliori pratiche per l'uso del C ++ modificato da Bjarne Stoustrup e Herb Sutter. Un attuazionefinally è parte della libreria di supporto Linee guida (GSL). In tutte le Linee guida, finallysi consiglia l' uso di quando si ha a che fare con interfacce vecchio stile e ha anche una linea guida propria, intitolata Usa un oggetto final_action per esprimere la pulizia se non è disponibile un handle di risorse adatto .

Quindi, non solo supporta il C ++ finally, in realtà si consiglia di usarlo in molti casi d'uso comuni.

Un esempio di utilizzo dell'implementazione GSL sarebbe simile a:

#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.
}

L'implementazione e l'utilizzo di GSL è molto simile a quello nella risposta di Paolo.Bolzoni . Una differenza è che l'oggetto creato dagsl::finally() manca della disable()chiamata. Se hai bisogno di quella funzionalità (ad esempio, per restituire la risorsa una volta assemblata e non sono previste eccezioni), potresti preferire l'implementazione di Paolo. Altrimenti, l'utilizzo di GSL è simile all'utilizzo di funzionalità standardizzate.


3

Non proprio, ma puoi emularli in qualche misura, ad esempio:

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;

Si noti che il blocco finally potrebbe a sua volta generare un'eccezione prima che venga generata nuovamente l'eccezione originale, eliminando così l'eccezione originale. Questo è esattamente lo stesso comportamento di un blocco Java finalmente. Inoltre, non è possibile utilizzare returnall'interno dei blocchi try & catch.


3
Sono contento che tu abbia menzionato il blocco infine potrebbe lanciare; è la cosa che la maggior parte delle risposte "usa RAII" sembrano ignorare. Per evitare di dover scrivere il blocco finalmente due volte, potresti fare qualcosa del generestd::exception_ptr e; try { /*try block*/ } catch (...) { e = std::current_exception(); } /*finally block*/ if (e) std::rethrow_exception(e);
sethobrien,

1
Questo è tutto ciò che volevo sapere! Perché nessuna delle altre risposte ha spiegato che una presa (...) + tiro vuoto; funziona quasi come un blocco finalmente? A volte ne hai solo bisogno.
VinGarcia,

La soluzione che ho fornito nella mia risposta ( stackoverflow.com/a/38701485/566849 ) dovrebbe consentire di generare eccezioni dall'interno del finallyblocco.
Fabio A.

3

Mi è venuta in mente una finallymacro che può essere usata quasi come ¹ la finallyparola chiave in Java; fa uso di std::exception_ptre amici, funzioni lambda e std::promise, quindi richiede C++11o sopra; utilizza anche l' estensione GCC di espressione dell'istruzione composta , che è supportata anche da clang.

ATTENZIONE : una versione precedente di questa risposta utilizzava un'implementazione diversa del concetto con molte altre limitazioni.

Innanzitutto, definiamo una classe di supporto.

#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);
}

Quindi c'è la macro attuale.

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

Può essere usato in questo modo:

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

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

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

    finally {
        clean_it_all_up();
    }    
}

L'uso di std::promisesemplifica l'implementazione, ma probabilmente introduce anche un po 'di spese generali non necessarie che potrebbero essere evitate reimplementando solo le funzionalità necessarie std::promise.


¹ CAVEAT: ci sono alcune cose che non funzionano come la versione java di finally. In cima alla mia testa:

  1. non è possibile rompere da un ciclo esterno con la breakdichiarazione all'interno del trye catch()'s blocchi, poiché vivono all'interno di una funzione lambda;
  2. ci deve essere almeno un catch()blocco dopo try: è un requisito C ++;
  3. se la funzione ha un valore di ritorno diverso da void ma non c'è ritorno all'interno dei blocchi trye catch()'s, la compilazione fallirà perché la finallymacro si espanderà nel codice che vorrà restituire a void. Questo potrebbe essere, a volte, un vuoto avendo una finally_noreturnspecie di macro.

Tutto sommato, non so se avrei mai usato queste cose da solo, ma è stato divertente giocarci. :)


Sì, è stato solo un trucco veloce, ma se il programmatore sa cosa sta facendo potrebbe essere comunque utile.
Fabio A.

@MarkLakata, ho aggiornato il post con una migliore implementazione che supporta il lancio di eccezioni e resi.
Fabio A.

Sembra buono. Puoi sbarazzarti di Caveat 2 inserendo un catch(xxx) {}blocco impossibile all'inizio della finallymacro, dove xxx è un tipo falso solo allo scopo di avere almeno un blocco catch.
Mark Lakata,

@MarkLakata, ci ho pensato anche io, ma sarebbe impossibile usarlo catch(...), no?
Fabio A.

Io non la penso così. Basta creare un tipo oscuro xxxin uno spazio dei nomi privato che non verrà mai utilizzato.
Mark Lakata,

2

Ho un caso d'uso in cui penso che finally dovrebbe essere una parte perfettamente accettabile del linguaggio C ++ 11, poiché penso che sia più facile da leggere dal punto di vista del flusso. Il mio caso d'uso è una catena di thread consumatore / produttore, in cui una sentinella nullptrviene inviata alla fine della corsa per chiudere tutti i thread.

Se C ++ lo supportasse, il tuo codice dovrebbe apparire così:

    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);
        }
    }

Penso che questo sia più logico che mettere la tua dichiarazione finalmente all'inizio del ciclo, dal momento che si verifica dopo che il ciclo è uscito ... ma questo è un pio desiderio perché non possiamo farlo in C ++. Nota che la coda downstreamè connessa a un altro thread, quindi non puoi inserire la sentinella push(nullptr)nel distruttore downstreamperché non può essere distrutta a questo punto ... deve rimanere in vita fino a quando l'altro thread non riceve il nullptr.

Ecco quindi come utilizzare una classe RAII con lambda per fare lo stesso:

    class Finally
    {
    public:

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

ed ecco come lo usi:

    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);
        }
    }

Ciao, credo che la mia risposta sopra ( stackoverflow.com/a/38701485/566849 ) soddisfi pienamente le tue esigenze.
Fabio A.

1

Come molte persone hanno affermato, la soluzione è utilizzare le funzionalità C ++ 11 per evitare finalmente i blocchi. Una delle caratteristiche è unique_ptr.

Ecco la risposta di Mephane scritta usando modelli RAII.

#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);
}

Ancora un po 'introduzione all'uso unique_ptr con i contenitori C ++ Standard Library è qui


0

Vorrei fornire un'alternativa.

Se vuoi che il blocco finalmente sia chiamato sempre, basta inserirlo dopo l'ultimo blocco catch (che probabilmente dovrebbe essere catch( ... )quello di catturare un'eccezione non nota)

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(); 

Se vuoi infine bloccare come ultima cosa da fare quando viene generata un'eccezione, puoi utilizzare la variabile locale booleana: prima di eseguirla, imposta su false e imposta l'assegnazione vera alla fine del blocco try, quindi dopo il blocco catch verifica la variabile valore:

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();
}

Questo non funziona, perché l'intero punto di un blocco finally è quello di eseguire la pulizia anche quando il codice dovrebbe consentire a un'eccezione di lasciare il blocco di codice. Considera: `prova {// cose che probabilmente lanciano" B "} cattura (A & a) {} infine {// se C ++ avesse ... // cose che devono accadere, anche se viene lanciata" B ". } // non verrà eseguito se viene generata "B". `IMHO, il punto delle eccezioni è ridurre il codice di gestione degli errori, quindi catturare i blocchi, ovunque si verifichi un lancio, è controproducente. Questo è il motivo per cui RAII aiuta: se applicate liberamente, le eccezioni contano di più agli strati superiore e inferiore.
burlyearly

1
@burlyearly anche se la tua opinione non è santa, capisco il punto, ma in C ++ non è una cosa del genere, quindi devi considerare questo come uno strato superiore che emula questo comportamento.
jave.web,

DOWNVOTE = PER FAVORE COMMENTO :)
jave.web

0

Penso anche che RIIA non sia un sostituto del tutto utile per la gestione delle eccezioni e per avere finalmente. A proposito, penso anche che RIIA sia un brutto nome dappertutto. Chiamo questi tipi di classi "bidelli" e li uso MOLTO. Il 95% delle volte non sta inizializzando né acquisendo risorse, sta applicando alcune modifiche su una base mirata o prendendo qualcosa di già impostato e assicurandosi che sia distrutto. Essendo questo il nome ufficiale del modello ossessionato da Internet, mi viene abusato anche solo del fatto che il mio nome potrebbe essere migliore.

Non penso sia ragionevole richiedere che ogni complicata configurazione di un elenco di cose ad hoc debba avere una classe scritta per contenerla al fine di evitare complicazioni quando si ripulisce il tutto di fronte alla necessità di catturare più tipi di eccezione se qualcosa va storto nel processo. Ciò porterebbe a molte classi ad hoc che altrimenti non sarebbero necessarie.

Sì, va bene per le classi progettate per gestire una particolare risorsa o per quelle generiche progettate per gestire un insieme di risorse simili. Ma, anche se tutte le cose coinvolte hanno tali involucri, il coordinamento della pulizia potrebbe non essere solo una semplice invocazione in ordine inverso dei distruttori.

Penso che abbia perfettamente senso avere finalmente un C ++. Voglio dire, accidenti, così tanti pezzi e bob sono stati incollati su di esso negli ultimi decenni che sembra che strane persone diventino improvvisamente conservatrici su qualcosa come finalmente che potrebbe essere abbastanza utile e probabilmente niente di così complicato come alcune altre cose che sono state aggiunto (anche se è solo una supposizione da parte mia).


-2
try
{
  ...
  goto finally;
}
catch(...)
{
  ...
  goto finally;
}
finally:
{
  ...
}

35
Simpatico idioma, ma non è proprio lo stesso. ritornando nel blocco try o catch non passerà attraverso il tuo codice 'finally:'.
Edward KMETT,

10
Vale la pena mantenere questa risposta sbagliata (con 0 voti), poiché Edward Kmett fa una distinzione molto importante.
Mark Lakata il

12
Ancora più grande difetto (IMO): questo codice mangia tutte le eccezioni, il che finallynon funziona.
Ben Voigt,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.