Perché non esiste un costrutto 'finally' in C ++?

56

La gestione delle eccezioni in C ++ è limitata a try / throw / catch. A differenza di Object Pascal, Java, C # e Python, anche in C ++ 11, il costrutto finally non è stato implementato.

Ho visto un sacco di pubblicazioni in C ++ che parlano di "codice di sicurezza eccezionale". Lippman scrive che il codice eccezionalmente sicuro è un argomento importante ma avanzato, oltre lo scopo del suo Primer - il che sembra implicare che il codice sicuro non sia fondamentale per C ++. Herb Sutter dedica 10 capitoli all'argomento nel suo Eccezionale C ++!

Tuttavia, mi sembra che molti dei problemi incontrati durante il tentativo di scrivere "codice eccezionalmente protetto" potrebbero essere risolti abbastanza se il costrutto finally è stato implementato, consentendo al programmatore di garantire che anche in caso di un'eccezione , il programma può essere ripristinato in uno stato sicuro, stabile, privo di perdite, vicino al punto di allocazione delle risorse e codice potenzialmente problematico. Come programmatore Delphi e C # molto esperto, io uso try .. finalmente blocca abbastanza estesamente nel mio codice, come fanno la maggior parte dei programmatori in queste lingue.

Considerando tutti i "campanelli e fischietti" implementati in C ++ 11, sono rimasto stupito nello scoprire che "finalmente" non era ancora lì.

Quindi, perché il costrutto finally non è mai stato implementato in C ++? Non è davvero un concetto molto difficile o avanzato da cogliere e serve molto per aiutare il programmatore a scrivere "codice di sicurezza".

    
posta Vector 09.05.2013 - 19:08
fonte

7 risposte

54

Come commento aggiuntivo sulla risposta di @ Nemanja (che, dato che cita Stroustrup, è davvero buono di una risposta che puoi ottenere):

In realtà è solo questione di comprendere la filosofia e gli idiomi di C ++. Prendi il tuo esempio di un'operazione che apre una connessione al database su una classe persistente e devi assicurarti che chiuda la connessione se viene lanciata un'eccezione. Questa è una questione di sicurezza delle eccezioni e si applica a qualsiasi lingua con eccezioni (C ++, C #, Delphi ...).

In una lingua che utilizza try / finally , il codice potrebbe essere simile a questo:

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

Semplice e diretto. Vi sono, tuttavia, alcuni svantaggi:

  • Se il linguaggio non ha distruttori deterministici, I ha sempre per scrivere il blocco finally , altrimenti ho perdite di risorse.
  • Se DoRiskyOperation è più di una singola chiamata di metodo - se ho qualche elaborazione da fare nel blocco try - allora l'operazione Close può finire per essere un po 'decente rispetto all'operazione Open . Non posso scrivere la mia pulizia subito dopo la mia acquisizione.
  • Se ho diverse risorse che devono essere acquisite e quindi liberate in modo eccezionalmente sicuro, posso finire con diversi livelli in profondità di try / finally blocchi.

L'approccio C ++ dovrebbe essere simile a questo:

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

Questo risolve completamente tutti gli svantaggi dell'approccio finally . Ha un paio di svantaggi, ma sono relativamente minori:

  • C'è una buona probabilità che tu abbia bisogno di scrivere tu stesso la classe ScopedDatabaseConnection . Tuttavia, è un'implementazione molto semplice: solo 4 o 5 righe di codice.
  • Si tratta di creare una variabile locale extra - di cui apparentemente non sei un fan, basandoti sul tuo commento su "creare e distruggere costantemente le classi per fare affidamento sui loro distruttori per ripulire il tuo casino è molto povero" - ma una buona il compilatore ottimizzerà tutto il lavoro extra che comporta una variabile locale extra. Il buon design C ++ si basa molto su questi tipi di ottimizzazioni.

Personalmente, considerando questi vantaggi e svantaggi, trovo RAII una tecnica molto preferibile a finally . Il tuo chilometraggio può variare.

Infine, poiché RAII è un idioma così consolidato in C ++ e per alleviare gli sviluppatori di alcune delle difficoltà di scrivere numerose classi Scoped... , ci sono librerie come ScopeGuard e Boost.ScopeExit che facilitano questo tipo di pulizia deterministica.

    
risposta data 09.05.2013 - 20:05
fonte
54

Da Perché C ++ non fornisce un costrutto "finale"? in Bjarne Domande frequenti su stile e tecnica C ++ di Stroustrup :

Because C++ supports an alternative that is almost always better: The "resource acquisition is initialization" technique (TC++PL3 section 14.4). The basic idea is to represent a resource by a local object, so that the local object's destructor will release the resource. That way, the programmer cannot forget to release the resource.

    
risposta data 09.05.2013 - 19:14
fonte
19

Il motivo per cui C ++ non ha finally è perché non è necessario in C ++. finally è usato per eseguire del codice indipendentemente dal fatto che si sia verificata un'eccezione, che è quasi sempre una sorta di codice di pulizia. In C ++, questo codice di pulizia dovrebbe essere nel distruttore della classe rilevante e il distruttore verrà sempre chiamato, proprio come un blocco finally . L'idioma di usare il distruttore per la tua pulizia è chiamato RAII .

All'interno della comunità C ++ potrebbero esserci più discussioni riguardo al codice 'eccezionalmente sicuro', ma è quasi ugualmente importante in altre lingue che hanno eccezioni. L'intero punto del codice 'eccezionalmente sicuro' è che si pensi in quale stato viene lasciato il codice se si verifica un'eccezione in una delle funzioni / metodi che si chiama.
In C ++, il codice 'eccezionalmente sicuro' è leggermente più importante, perché C ++ non ha una garbage collection automatica che si occupa di oggetti che sono rimasti orfani a causa di un'eccezione.

Il motivo per cui la sicurezza delle eccezioni è discussa di più nella comunità C ++ deriva probabilmente dal fatto che in C ++ devi essere più consapevole di cosa può andare storto, perché ci sono meno reti di sicurezza predefinite nella lingua.

    
risposta data 09.05.2013 - 19:24
fonte
11

Altri hanno discusso di RAII come soluzione. È una soluzione perfettamente buona. Ma questo in realtà non si rivolge a perché non hanno aggiunto finally poiché è una cosa ampiamente desiderata. La risposta a questo è più fondamentale per la progettazione e lo sviluppo di C ++: durante lo sviluppo di C ++ quelli coinvolti hanno strongmente resistito all'introduzione di caratteristiche progettuali che possono essere ottenute utilizzando altre funzionalità senza troppi problemi e soprattutto dove ciò richiede l'introduzione di nuove parole chiave che potrebbero rendere il codice precedente incompatibile. Dal momento che RAII fornisce un'alternativa altamente funzionale a finally e puoi effettivamente eseguire il tuo finally in C ++ 11 in ogni caso, c'era poca richiesta.

Tutto ciò che devi fare è creare una classe Finally che chiami la funzione passata al suo costruttore nel suo distruttore. Quindi puoi fare questo:

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

    database.doRisky();
}

La maggior parte dei programmatori C ++ nativi, in generale, preferiscono comunque oggetti RAII chiaramente progettati.

    
risposta data 09.05.2013 - 23:51
fonte
2

È possibile utilizzare un modello "trap", anche se non si desidera utilizzare il blocco try / catch.

Metti un oggetto semplice nello scope richiesto. Nel distruttore di questo oggetto metti la tua logica "definitiva". Qualunque cosa accada, quando la pila viene srotolata, verrà chiamato il distruttore dell'oggetto e otterrai le tue caramelle.

    
risposta data 09.05.2013 - 22:45
fonte
2

Beh, potresti fare un po 'di roll-your-own finally , usando Lambdas, che otterrebbe quanto segue per compilare bene (usando un esempio senza RAII, ovviamente, non il più bel pezzo di codice):

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

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

Vedi questo articolo .

    
risposta data 25.04.2015 - 13:12
fonte
-2

Non sono sicuro di essere d'accordo con le affermazioni secondo cui RAII è un superset di finally . Il tallone d'Achille di RAII è semplice: eccezioni. RAII è implementato con i distruttori, ed è sempre sbagliato in C ++ buttarlo fuori da un distruttore. Ciò significa che non puoi utilizzare RAII quando hai bisogno del tuo codice di pulizia da buttare. Se finally sono stati implementati, d'altra parte, non c'è motivo di credere che non sarebbe legale lanciare da un blocco finally .

Considera un percorso di codice come questo:

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

Se avessimo finally potremmo scrivere:

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

Ma non c'è modo, che io possa trovare, di ottenere il comportamento equivalente usando RAII.

Se qualcuno sa come farlo in C ++, sono molto interessato alla risposta. Sarei anche contento di qualcosa che si basava, ad esempio, sull'applicazione di tutte le eccezioni ereditate da una singola classe con alcune capacità speciali o qualcosa del genere.

    
risposta data 31.08.2015 - 00:13
fonte

Leggi altre domande sui tag