Perché non dovrei avvolgere ogni blocco in “try” – “catch”?

Sono sempre stato convinto che se un metodo può generare un’eccezione, è imprudente non proteggere questa chiamata con un blocco try significativo.

Ho appena pubblicato ‘ Dovresti SEMPRE avvolgere le chiamate che possono lanciare in prova, prendere blocchi. ‘a questa domanda e mi è stato detto che si trattava di’ consigli notevolmente negativi ‘- mi piacerebbe capire perché.

Un metodo dovrebbe rilevare un’eccezione solo quando può gestirlo in modo ragionevole.

Altrimenti, passalo in su, nella speranza che un metodo più in alto nello stack delle chiamate possa dare un senso.

Come altri hanno notato, è buona norma avere un gestore di eccezioni non gestito (con registrazione) al più alto livello dello stack di chiamate per assicurarsi che vengano registrati eventuali errori irreversibili.

Come hanno affermato Mitch e altri , non dovresti notare un’eccezione che non intendi gestire in qualche modo. Dovresti considerare in che modo l’applicazione gestirà sistematicamente le eccezioni durante la progettazione. Questo di solito porta ad avere livelli di gestione degli errori basati sulle astrazioni – ad esempio, gestisci tutti gli errori relativi al codice SQL nel tuo codice di accesso ai dati in modo che la parte dell’applicazione che interagisce con gli oggetti di dominio non sia esposta al fatto che è un DB sotto il cofano da qualche parte.

Ci sono alcuni odori di codice correlati che si desidera evitare in aggiunta all’odore “cattura tutto ovunque” .

  1. “catch, log, rethrow” : se si desidera la registrazione basata sull’ambito , quindi scrivere una class che emette un’istruzione di registro nel suo distruttore quando lo stack viene srotolato a causa di un’eccezione (ala std::uncaught_exception() ). Tutto quello che devi fare è dichiarare un’istanza di logging nell’ambito che ti interessa e, voilà, hai logging e nessuna logica try / catch non necessaria.

  2. “catch, throw translated” : questo di solito indica un problema di astrazione. A meno che non si stia implementando una soluzione federata in cui si stanno traducendo diverse eccezioni specifiche in una più generica, probabilmente si dispone di un livello non necessario di astrazione … e non si dice “Potrei averne bisogno domani” .

  3. “catch, cleanup, rethrow” : questo è uno dei miei pet-peeves. Se viene visualizzato molto, è necessario applicare l’ Acquisizione risorse è tecniche di inizializzazione e posizionare la parte di pulitura nel distruttore di un’istanza dell’object bidello .

Considero il codice che è pieno di blocchi try / catch per essere un buon bersaglio per la revisione e il refactoring del codice. Indica che la gestione delle eccezioni non è ben compresa o che il codice è diventato un amœba e ha un serio bisogno di refactoring.

Perché la prossima domanda è “Ho preso un’eccezione, cosa faccio dopo?” Cosa farai? Se non fai niente – questo è un errore che si nasconde e il programma potrebbe “non funzionare” senza alcuna possibilità di scoprire cosa è successo. Devi capire cosa farai esattamente una volta che avrai colto l’eccezione e catturerai solo se lo sai.

Herb Sutter ha scritto su questo problema qui . Di sicuro vale la pena leggerlo.
Un teaser:

“Scrivere codice eccezionalmente sicuro è fondamentalmente sulla scrittura di ‘provare’ e ‘catturare’ nei posti corretti.” Discutere.

In parole povere, questa affermazione riflette un fondamentale fraintendimento della sicurezza delle eccezioni. Le eccezioni sono solo un’altra forma di segnalazione degli errori, e sappiamo certamente che scrivere codice sicuro da errori non riguarda solo dove controllare i codici di ritorno e gestire le condizioni di errore.

In realtà, risulta che raramente la sicurezza delle eccezioni riguarda la scrittura di “try” e “catch” – e più raramente è il migliore. Inoltre, non dimenticare mai che la sicurezza delle eccezioni influisce su un pezzo di design del codice; non è mai solo un ripensamento che può essere riadattato con alcune dichiarazioni di cattura in più come per condire.

Non è necessario coprire ogni blocco con try-catches perché un try-catch può comunque rilevare eccezioni non gestite generate in funzioni più in basso nello stack di chiamate. Quindi, piuttosto che ogni funzione ha un try-catch, puoi averne uno nella logica di livello superiore della tua applicazione. Ad esempio, potrebbe esserci una SaveDocument() primo livello SaveDocument() , che chiama molti metodi che chiamano altri metodi, ecc. Questi sottoprogrammi non hanno bisogno dei propri try-catches, perché se vengono SaveDocument() comunque catturati da SaveDocument() cattura.

Questo è utile per tre motivi: è utile perché hai un unico posto in cui segnalare un errore: i SaveDocument() cattura SaveDocument() ). Non è necessario ripeterlo in tutti i sottoprogrammi, ed è ciò che si vuole comunque: un solo posto per fornire all’utente una diagnostica utile su qualcosa che è andato storto.

Due, il salvataggio viene annullato ogni volta che viene lanciata un’eccezione. Con ogni metodo secondario che tenta di catturare, se viene lanciata un’eccezione, si entra nel blocco catch di quel metodo, l’esecuzione lascia la funzione e continua con SaveDocument() . Se qualcosa è già andato storto probabilmente vorresti fermarti proprio lì.

Tre, tutti i tuoi sottoprogrammi possono supporre che ogni chiamata abbia esito positivo . Se una chiamata fallisce, l’esecuzione salterà sul blocco catch e il codice successivo non verrà mai eseguito. Questo può rendere il tuo codice molto più pulito. Ad esempio, ecco i codici di errore:

 int ret = SaveFirstSection(); if (ret == FAILED) { /* some diagnostic */ return; } ret = SaveSecondSection(); if (ret == FAILED) { /* some diagnostic */ return; } ret = SaveThirdSection(); if (ret == FAILED) { /* some diagnostic */ return; } 

Ecco come potrebbe essere scritto con eccezioni:

 // these throw if failed, caught in SaveDocument's catch SaveFirstSection(); SaveSecondSection(); SaveThirdSection(); 

Ora è molto più chiaro cosa sta succedendo.

Nota: il codice di eccezione delle eccezioni può essere più complicato scrivere in altri modi: non si vuole perdere alcuna memoria se viene lanciata un’eccezione. Assicurati di sapere su RAII , contenitori STL, puntatori intelligenti e altri oggetti che liberano le loro risorse nei distruttori, poiché gli oggetti vengono sempre distrutti prima delle eccezioni.

Come affermato in altre risposte, dovresti solo rilevare un’eccezione se puoi fare una sorta di ragionevole gestione degli errori per questo.

Ad esempio, nella domanda che ha generato la domanda, l’interrogante chiede se è sicuro ignorare le eccezioni per un lexical_cast da un intero a una stringa. Un cast del genere non dovrebbe mai fallire. Se ha avuto esito negativo, qualcosa è andato terribilmente male nel programma. Cosa potresti fare per recuperare in quella situazione? Probabilmente è meglio lasciare che il programma muoia, poiché è in uno stato che non può essere considerato attendibile. Quindi non gestire l’eccezione potrebbe essere la cosa più sicura da fare.

Se gestisci sempre le eccezioni immediatamente nel chiamante di un metodo che può generare un’eccezione, le eccezioni diventano inutili e è preferibile utilizzare i codici di errore.

L’intero punto delle eccezioni è che non devono essere gestite in tutti i metodi della catena di chiamate.

Il miglior consiglio che ho sentito è che dovresti sempre prendere delle eccezioni in punti in cui puoi ragionevolmente fare qualcosa per la condizione eccezionale, e che “catch, log and release” non è una buona strategia (se occasionalmente inevitabile nelle librerie).

Sono d’accordo con la direzione di base della tua domanda per gestire il maggior numero ansible di eccezioni al livello più basso.

Alcune delle risposte esistenti vanno come “Non è necessario gestire l’eccezione. Qualcun altro lo farà in cima allo stack”. Per la mia esperienza è una ctriggers scusa per non pensare alla gestione delle eccezioni nella parte di codice attualmente sviluppata, facendo in modo che l’eccezione gestisca il problema di qualcun altro o successivo.

Questo problema cresce in modo drammatico nello sviluppo distribuito, dove potrebbe essere necessario chiamare un metodo implementato da un collega. E poi devi ispezionare una catena annidata di chiamate al metodo per scoprire perché lui / lei sta generando qualche eccezione, che avrebbe potuto essere gestita molto più facilmente con il metodo nidificato più profondo.

Il consiglio che il mio professore di informatica mi ha dato una volta era: “Usa i blocchi Try and Catch solo quando non è ansible gestire l’errore usando mezzi standard”.

Ad esempio, ci ha detto che se un programma si è imbattuto in un problema serio in un posto dove non è ansible fare qualcosa del tipo:

 int f() { // Do stuff if (condition == false) return -1; return 0; } int condition = f(); if (f != 0) { // handle error } 

Allora dovresti usare try, prendere blocchi. Sebbene sia ansible utilizzare le eccezioni per gestirle, in genere non è consigliabile in quanto le eccezioni sono costose in termini di prestazioni.

Se si desidera verificare l’esito di ogni funzione, utilizzare i codici di ritorno.

Lo scopo di Eccezioni è che puoi testare i risultati MENO spesso. L’idea è di separare condizioni eccezionali (inusuali, più rari) dal tuo codice più comune. Ciò mantiene il codice ordinario più pulito e semplice, ma comunque in grado di gestire tali condizioni eccezionali.

In un codice ben progettato, le funzioni più profonde potrebbero essere lanciate e potrebbero essere rilevate funzioni più elevate. Ma la chiave è che molte funzioni “intermedie” saranno libere dall’onere di gestire condizioni eccezionali. Devono essere solo “eccezionalmente sicuri”, il che non significa che debbano intercettarli.

Oltre al consiglio di cui sopra, personalmente uso qualche tentativo + catch + throw; per il seguente motivo:

  1. Al limite di un altro programmatore, uso try + catch + throw nel codice scritto da me stesso, prima che l’eccezione sia lanciata al chiamante che è scritta da altri, questo mi dà la possibilità di conoscere qualche condizione di errore nel mio codice, e questo posto è molto più vicino al codice che inizialmente lancia l’eccezione, più vicino è, più facile trovare la ragione.
  2. Al confine dei moduli, sebbene un modulo diverso possa essere scritto la mia stessa persona.
  3. Learning + scopo di debug, in questo caso uso catch (…) in C ++ e catch (Exception ex) in C #, per C ++, la libreria standard non genera troppe eccezioni, quindi questo caso è raro in C ++. Ma il posto comune in C #, C # ha un’enorme libreria e una gerarchia di eccezioni mature, il codice della libreria C # genera tonnellate di eccezioni, in teoria io (e tu) dovremmo conoscere ogni eccezione dalla funzione che hai chiamato e sapere il motivo / caso perché queste eccezioni vengono lanciate e sanno come gestirle (passare o afferrarle e gestirle sul posto) con garbo. Sfortunatamente in realtà è molto difficile sapere tutto sulle potenziali eccezioni prima di scrivere una riga di codice. Quindi prendo tutto e lascia che il mio codice parli ad alta voce loggando (in ambiente di prodotto) / assert dialog (in ambiente di sviluppo) quando si verifica veramente un’eccezione. In questo modo aggiungo progressivamente il codice di gestione delle eccezioni. So che si fondono con buoni consigli ma in realtà funziona per me e non conosco un modo migliore per questo problema.

Se si desidera risolvere facilmente problemi di produzione intermittenti, è necessario racchiudere ogni blocco di codice in un blocco try..catch. Fondamentalmente questo strumento costituisce il codice con l’objective di fornire ampie informazioni di debug che consentono di eseguire il debug senza un debugger in produzione. Gli utenti non hanno bisogno di e-mail o chat con supporto e tutte le informazioni necessarie per risolvere il problema sono proprio lì. Non è necessario riprodurre i problemi.

Per funzionare correttamente, deve essere combinato con una registrazione estesa in grado di catturare lo spazio dei nomi / modulo, il nome della class, il metodo, gli input e il messaggio di errore e archiviarli in un database in modo che possa essere aggregato per evidenziare quale metodo fallisce di più in modo che possa essere risolto prima.

Le eccezioni sono da 100 a 1000 volte più lente del codice normale e non dovrebbero mai essere ricacciate. Inoltre, non creare un’eccezione e lanciarla. Questo è molto distpruuttivo. Le eccezioni vengono catturate in modo che possano essere corrette con un codice normale.

Questa tecnica è stata utilizzata per stabilizzare rapidamente un’app buggy in un’azienda Fortune 500 sviluppata da 12 sviluppatori in 2 anni. Usando questo ho identificato, risolto, costruito test e distribuito 3000 correzioni in 4 mesi, nel qual caso il sistema non riportava più eccezioni come tutte le altre sono state gestite. Questa media è di una correzione ogni 15 minuti in media per 4 mesi.

Vorrei aggiungere a questa discussione che, dal C ++ 11 , ha molto senso, purché ogni blocco di catch rethrow l’eccezione fino al punto in cui può / deve essere gestita. In questo modo può essere generato un backtrace . Pertanto ritengo che le precedenti opinioni siano in parte obsolete.

Usa std::nested_exception e std::throw_with_nested

È descritto su StackOverflow qui e qui come ottenere ciò.

Dato che puoi farlo con qualsiasi class di derivazione derivata, puoi aggiungere molte informazioni a tale backtrace! Puoi anche dare un’occhiata al mio MWE su GitHub , dove un backtrace sarebbe simile a questo:

 Library API: Exception caught in function 'api_function' Backtrace: ~/Git/mwe-cpp-exception/src/detail/Library.cpp:17 : library_function failed ~/Git/mwe-cpp-exception/src/detail/Library.cpp:13 : could not open file "nonexistent.txt" 

Non hai bisogno di hide ogni parte del tuo codice all’interno di try-catch . L’uso principale del blocco try-catch è la gestione degli errori e ha ottenuto bug / eccezioni nel programma. Alcuni usi di try-catch

  1. Puoi usare questo blocco dove vuoi gestire un’eccezione o semplicemente puoi dire che il blocco del codice scritto può generare un’eccezione.
  2. Se si desidera disporre gli oggetti immediatamente dopo il loro utilizzo, è ansible utilizzare try-catch blocco try-catch .