Perché non esiste un costrutto "finalmente" in C ++?


57

La gestione delle eccezioni in C ++ è limitata a provare / lanciare / catturare. A differenza di Object Pascal, Java, C # e Python, anche in C ++ 11, il finallycostrutto non è stato implementato.

Ho visto un sacco di letteratura C ++ che parlava di "codice sicuro di eccezione". Lippman scrive che il codice sicuro di eccezione è un argomento importante ma avanzato e difficile, al di là dell'ambito 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 riscontrati durante il tentativo di scrivere "codice sicuro di eccezione" potrebbero essere risolti abbastanza bene se il finallycostrutto fosse implementato, consentendo al programmatore di garantire che anche in caso di eccezione, il programma potesse essere ripristinato a uno stato sicuro, stabile, privo di perdite, vicino al punto di allocazione delle risorse e al codice potenzialmente problematico. Come programmatore Delphi e C # molto esperto, uso try .. finalmente si blocca abbastanza ampiamente nel mio codice, così come la maggior parte dei programmatori in queste lingue.

Considerando tutte le "campane e fischietti" implementate in C ++ 11, sono rimasto sorpreso di scoprire che "finalmente" non c'era ancora.

Quindi, perché il finallycostrutto non è mai stato implementato in C ++? Non è davvero un concetto molto difficile o avanzato da comprendere e aiuta molto il programmatore a scrivere "codice sicuro eccezioni".


25
Perché no finalmente? Perché rilasci cose nel distruttore che si attivano automaticamente quando l'oggetto (o il puntatore intelligente) lascia l'ambito. I distruttori sono finalmente superiori a {} poiché separano il flusso di lavoro dalla logica di pulizia. Proprio come non vorrai che le chiamate a free () ingombrassero il flusso di lavoro in un linguaggio garbage collection.
mike30,


8
Porre la domanda "Perché non esiste finallyC ++ e quali tecniche di gestione delle eccezioni vengono utilizzate al suo posto?" è valido e in argomento per questo sito. Le risposte esistenti lo coprono bene, credo. Trasformandolo in una discussione su "I motivi dei progettisti del C ++ per non includerne la finallypena?" e "Dovrebbe finallyessere aggiunto al C ++?" e portare avanti la discussione attraverso i commenti sulla domanda e ogni risposta non corrisponde al modello di questo sito di domande e risposte.
Josh Kelley,

2
Se hai finalmente, hai già la separazione delle preoccupazioni: il blocco di codice principale è qui, e la preoccupazione di pulizia viene risolta qui.
Kaz,

2
@Kaz. La differenza è implicita rispetto alla pulizia esplicita. Un distruttore ti dà una ripulitura automatica simile a come una vecchia primitiva semplice viene ripulita mentre si stacca dalla pila. Non è necessario effettuare chiamate esplicite di pulizia e concentrarsi sulla logica di base. Immagina quanto sarebbe complicato se dovessi ripulire le primitive allocate in pila in un tentativo / finalmente. La pulizia implicita è superiore. Il confronto tra la sintassi della classe e le funzioni anonime non è rilevante. Sebbene passando funzioni di prima classe a una funzione che rilascia un handle potrebbe centralizzare la pulizia manuale.
mike30,

Risposte:


57

Come alcuni commenti aggiuntivi sulla risposta di @ Nemanja (che, dal momento che cita Stroustrup, è davvero una risposta tanto buona quanto puoi ottenere):

È davvero solo una questione di comprensione della filosofia e dei modi di dire del C ++. Prendi l'esempio di un'operazione che apre una connessione al database su una classe persistente e deve assicurarsi che chiuda tale connessione se viene generata un'eccezione. Questa è una questione di sicurezza delle eccezioni e si applica a qualsiasi linguaggio con eccezioni (C ++, C #, Delphi ...).

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

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

Semplice e diretto Vi sono, tuttavia, alcuni svantaggi:

  • Se la lingua non ha distruttori deterministici, devo sempre scrivere il finallyblocco, altrimenti perdo risorse.
  • Se DoRiskyOperationè più di una singola chiamata di metodo - se ho qualche elaborazione da fare nel tryblocco - l' Closeoperazione può finire per essere un po 'decente Opendall'operazione. Non riesco a scrivere la pulizia proprio accanto alla mia acquisizione.
  • Se ho diverse risorse che devono essere acquisite e poi liberate in modo sicuro dalle eccezioni, posso finire con diversi strati profondi di try/ finallyblocchi.

L'approccio C ++ sarebbe simile al seguente:

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

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

  • Ci sono buone probabilità che tu abbia bisogno di scrivere ScopedDatabaseConnectiontu stesso la lezione. Tuttavia, è un'implementazione molto semplice: solo 4 o 5 righe di codice.
  • Implica la creazione di una variabile locale aggiuntiva - di cui apparentemente non sei un fan, in base al tuo commento su "creare e distruggere costantemente le classi per fare affidamento sui loro distruttori per ripulire il tuo casino è molto scarso" - ma un buon compilatore ottimizzerà fuori dal lavoro extra che comporta una variabile locale extra. Un buon design C ++ si basa molto su questo tipo di ottimizzazioni.

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

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


8
C # ha l' usingistruzione, che pulisce automaticamente qualsiasi oggetto che implementa l' IDisposableinterfaccia. Quindi, mentre è possibile sbagliare, è abbastanza facile farlo bene.
Robert Harvey,

18
Dover scrivere una classe completamente nuova per occuparsi dell'inversione temporanea del cambio di stato, usando un linguaggio di progettazione implementato dal compilatore con un try/finallycostrutto perché il compilatore non espone un try/finallycostrutto e l'unico modo per accedervi è attraverso la classe design idioma, non è un "vantaggio"; è la definizione stessa di inversione di astrazione.
Mason Wheeler,

15
@MasonWheeler - Umm, non ho detto che dover scrivere una nuova classe sia un vantaggio. Ho detto che è uno svantaggio. A conti fatti, però, preferisco RAII a dover usare finally. Come ho detto, il tuo chilometraggio può variare.
Josh Kelley,

7
@JoshKelley: "Il buon design C ++ si basa molto su questo tipo di ottimizzazioni". Scrivere gobs di codice estraneo e quindi fare affidamento sull'ottimizzazione del compilatore è Good Design ?! IMO è l'antitesi del buon design. Tra i fondamenti di un buon design c'è un codice conciso e facilmente leggibile. Meno debug, meno da mantenere, ecc. Ecc. Ecc. NON dovresti scrivere codice di codice e fare affidamento sul compilatore per far sparire tutto - IMO che non ha alcun senso!
Vector

14
@Mikey: Quindi duplicare il codice di pulizia (o il fatto che la pulizia debba avvenire) in tutto il codice è "conciso" e "facilmente leggibile"? Con RAII, scrivi tale codice una volta e viene automaticamente applicato ovunque.
Mankarse,

55

Da Perché il C ++ non fornisce un costrutto "finalmente"? nelle FAQ su stile e tecnica C ++ di Bjarne Stroustrup :

Perché C ++ supporta un'alternativa che è quasi sempre migliore: la tecnica "acquisizione delle risorse è inizializzazione" (TC ++ PL3 sezione 14.4). L'idea di base è quella di rappresentare una risorsa da un oggetto locale, in modo che il distruttore dell'oggetto locale rilasci la risorsa. In questo modo, il programmatore non può dimenticare di rilasciare la risorsa.


5
Ma non c'è nulla in quella tecnica specifica del C ++, vero? Puoi fare RAII in qualsiasi linguaggio con oggetti, costruttori e distruttori. È un'ottima tecnica, ma la semplice RAII esistente non implica che un finallycostrutto sia sempre inutile nei secoli dei secoli, nonostante ciò che dice Strousup. Il semplice fatto che la scrittura di "codice sicuro di eccezione" sia un grosso problema in C ++ ne è la prova. Diamine, C # ha entrambi i distruttori finallye si abituano entrambi .
Tacroy,

28
@Tacroy: C ++ è uno dei pochissimi linguaggi tradizionali con distruttori deterministici . I "distruttori" C # sono inutili per questo scopo e devi avere manualmente i blocchi "usando" per avere RAII.
Nemanja Trifunovic,

15
@ Mike hai la risposta di "Perché il C ++ non fornisce un costrutto" finalmente "?" direttamente dallo stesso Stroustrup. Cosa si potrebbe chiedere di più? Questo è il motivo.

5
@Mikey Se ti preoccupi per il codice si comporta bene, in particolare le risorse che non perda, quando le eccezioni sono gettati in esso, si sono preoccuparsi di sicurezza rispetto alle eccezioni / cercando di eccezione scrittura di codice sicuro. Non lo chiami così e, a causa della disponibilità di diversi strumenti, lo implementi in modo diverso. Ma è esattamente ciò di cui parlano le persone C ++ quando discutono della sicurezza delle eccezioni.

19
@Kaz: Devo solo ricordare di fare la pulizia nel distruttore una volta, e da allora uso l'oggetto. Devo ricordare di fare la pulizia nel blocco finally ogni volta che utilizzo l'operazione che alloca.
Deworde

19

Il motivo che C ++ non ha finallyè perché non è necessario in C ++. finallyviene utilizzato per eseguire del codice indipendentemente dal fatto che si sia verificata o meno un'eccezione, che è quasi sempre una sorta di codice di pulizia. In C ++, questo codice di pulizia dovrebbe trovarsi nel distruttore della classe pertinente e il distruttore verrà sempre chiamato, proprio come un finallyblocco. Il linguaggio dell'uso del distruttore per la tua pulizia si chiama RAII .

All'interno della comunità C ++ si potrebbe parlare più del codice "eccezioni sicure", ma è quasi altrettanto importante in altre lingue che hanno eccezioni. L'intero punto del codice "eccezioni sicure" è che pensi a quale stato viene lasciato il tuo codice se si verifica un'eccezione in una qualsiasi delle funzioni / metodi che chiami.
In C ++, il codice "eccezioni sicure" è leggermente più importante, poiché C ++ non ha garbage collection automatica che si occupa degli oggetti lasciati orfani a causa di un'eccezione.

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


2
Nota: non sostenere che C ++ abbia distruttori deterministici. Anche Object Pascal / Delphi ha distruttori deterministici ma supporta anche "finalmente", per le ottime ragioni che ho spiegato nei miei primi commenti qui sotto.
Vector

13
@Mikey: dato che non c'è mai stata una proposta da aggiungere finallyallo standard C ++, penso che sia sicuro concludere che la comunità C ++ non ritiene the absence of finallyun problema. La maggior parte dei linguaggi che finallymancano della coerente distruzione deterministica del C ++. Vedo che Delphi li possiede entrambi, ma non conosco abbastanza bene la sua storia per sapere quale fosse la prima volta.
Bart van Ingen Schenau,

3
Dephi non supporta oggetti basati su stack - solo basato su heap e riferimenti a oggetti sullo stack. Pertanto, "finalmente" è necessario per invocare esplicitamente i distruttori ecc., Se del caso.
Vettore

2
C'è un sacco di cruft in C ++ che probabilmente non è necessario, quindi questa non può essere la risposta giusta.
Kaz,

15
Negli ultimi due decenni ho usato la lingua e ho lavorato con altre persone che la usavano, non ho mai incontrato un programmatore C ++ che dicesse "Vorrei davvero che la lingua avesse un finally". Non riesco mai a ricordare nessun compito che avrebbe reso più semplice se avessi avuto accesso ad esso.
Gort il robot,

12

Altri hanno discusso di RAII come soluzione. È una soluzione perfettamente valida. Ma questo non affronta davvero il motivo per cui non hanno aggiunto finallyaltrettanto poiché è una cosa ampiamente desiderata. La risposta a questo è più fondamentale per la progettazione e lo sviluppo del C ++: durante lo sviluppo del C ++, le persone coinvolte hanno resistito fortemente all'introduzione di funzionalità di progettazione che possono essere raggiunte usando altre funzionalità senza un grande sforzo e soprattutto dove ciò richiede l'introduzione di nuove parole chiave che potrebbero rendere incompatibile il codice precedente. Dal momento che RAII offre un'alternativa altamente funzionale a finallye puoi effettivamente creare il tuo finallyin C ++ 11, ci sono state poche richieste.

Tutto quello che devi fare è creare una classe Finallyche chiama la funzione passata al suo costruttore nel suo distruttore. Quindi puoi farlo:

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

    database.doRisky();
}

La maggior parte dei programmatori C ++ nativi preferirà, in generale, oggetti RAII dal design pulito.


3
Manca la cattura di riferimento nella tua lambda. Dovrei Finally atEnd([&] () { database.close(); });anche immaginare che sia meglio: { Finally atEnd(...); try {...} catch(e) {...} }(ho tolto il finalizzatore dal blocco di prova in modo che venga eseguito dopo i blocchi di cattura.)
Thomas Eding,

2

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

Inserire un oggetto semplice nell'ambito richiesto. Nel distruttore di questo oggetto metti la tua logica "definitiva". Indipendentemente da ciò, quando la pila viene srotolata, verrà chiamato il distruttore dell'oggetto e otterrai le tue caramelle.


1
Questo non risponde alla domanda e dimostra semplicemente che alla fine non è poi una cattiva idea dopo tutto ...
Vector

2

Bene, potresti finallyusare il roll-your-own , usando Lambdas, che farebbe compilare bene quanto segue (usando ovviamente un esempio senza RAII, non il codice più carino):

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

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

Vedere questo articolo .


-2

Non sono sicuro di essere d'accordo con le affermazioni qui che RAII è un superset di finally. Il tallone d'Achille di RAII è semplice: eccezioni. RAII è implementato con i distruttori, ed è sempre sbagliato in C ++ eliminare un distruttore. Ciò significa che non è possibile utilizzare RAII quando è necessario lanciare il codice di pulizia. Se finallyfossero stati implementati, d'altra parte, non c'è motivo di credere che non sarebbe legale lanciare da un finallyblocco.

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 finallypotuto 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 riesco a trovare il modo di ottenere un comportamento equivalente usando RAII.

Se qualcuno sa come farlo in C ++, sono molto interessato alla risposta. Sarei anche felice di qualcosa su cui fare affidamento, ad esempio, imponendo che tutte le eccezioni ereditate da una singola classe con alcune capacità speciali o qualcosa del genere.


1
Nel tuo secondo esempio, se complex_cleanuppuò lanciare, allora potresti avere un caso in cui due eccezioni non rilevate sono in volo contemporaneamente, proprio come faresti con RAII / destructors, e C ++ rifiuta di permetterlo. Se vuoi vedere l'eccezione originale, allora complex_cleanupdovresti prevenire qualsiasi eccezione, proprio come farebbe con RAII / destructors. Se vuoi vedere complex_cleanupl'eccezione, allora penso che puoi usare blocchi di prova / cattura nidificati - anche se questo è tangente e difficile da inserire in un commento, quindi vale una domanda separata.
Josh Kelley,

Voglio usare RAII per ottenere un comportamento identico al primo esempio, in modo più sicuro. Un lancio in un finallyblocco putativo avrebbe chiaramente lo stesso effetto di un lancio in un catchblocco WRT eccezioni in volo - non chiamata std::terminate. La domanda è "perché no finallyin C ++?" e tutte le risposte dicono "non ne hai bisogno ... RAII FTW!" Il mio punto è che sì, RAII va bene per casi semplici come la gestione della memoria, ma fino a quando il problema delle eccezioni non viene risolto richiede troppo pensiero / sovraccarico / preoccupazione / riprogettazione per essere una soluzione di uso generale.
MadScientist,

3
Capisco il tuo punto - ci sono alcuni problemi legittimi con i distruttori che potrebbero essere lanciati - ma quelli sono rari. Dire che le eccezioni di RAII + ha problemi irrisolti o che RAII non è una soluzione per scopi generici semplicemente non corrisponde all'esperienza della maggior parte degli sviluppatori C ++.
Josh Kelley,

1
Se ti trovi con la necessità di sollevare eccezioni nei distruttori, stai facendo qualcosa di sbagliato, probabilmente usando i puntatori in altri luoghi quando non sono necessari.
Vettore

1
Questo è troppo coinvolto per i commenti. Pubblica una domanda a riguardo: come gestiresti questo scenario in C ++ usando il modello RAII ... non sembra funzionare ... Ancora una volta, dovresti indirizzare i tuoi commenti : digitare @ e il nome del membro di cui stai parlando all'inizio del tuo commento. Quando i commenti sono sul tuo post, riceverai una notifica di tutto, ma altri no a meno che tu non gli invii un commento.
Vettore
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.