Is `catch (...) {lancio; } `una cattiva pratica?


74

Anche se concordo sul fatto che catturare ... senza ricrescere sia effettivamente sbagliato, tuttavia credo che usare costrutti come questo:

try
{
  // Stuff
}
catch (...)
{
  // Some cleanup
  throw;
}

È accettabile nei casi in cui RAII non è applicabile . (Per favore, non chiedere ... non tutti nella mia azienda amano la programmazione orientata agli oggetti e RAII è spesso vista come "materiale scolastico inutile" ...)

I miei colleghi dicono che dovresti sempre sapere quali eccezioni devono essere lanciate e che puoi sempre usare costrutti come:

try
{
  // Stuff
}
catch (exception_type1&)
{
  // Some cleanup
  throw;
}
catch (exception_type2&)
{
  // Some cleanup
  throw;
}
catch (exception_type3&)
{
  // Some cleanup
  throw;
}

Esiste una buona pratica ben ammessa riguardo a queste situazioni?


3
@Pubby: non sono sicuro che questa sia esattamente la stessa domanda. La domanda collegata riguarda di più "Dovrei catturare ..." mentre la mia domanda si concentra su "Dovrei catturare meglio ...o <specific exception>prima di ridisegnare"
ereOn

53
Mi dispiace dirlo, ma C ++ senza RAII non è C ++.
Fredoverflow,

46
Quindi i tuoi cow-worker respingono la tecnica che è stata inventata per affrontare un certo problema e poi discutono su quale delle alternative inferiori dovrebbe essere usata? Mi dispiace dirlo, ma sembra stupido , non importa in che modo lo guardo.
sabato

11
"Catturare ... senza ricrescere è davvero sbagliato" - ti sbagli. In main, catch(...) { return EXIT_FAILURE; }potrebbe essere proprio nel codice che non è in esecuzione in un debugger. Se non prendi, la pila potrebbe non essere srotolata. È solo quando il tuo debugger rileva eccezioni non rilevate che vuoi che vengano lasciate main.
Steve Jessop,

3
... quindi anche se si tratta di un "errore di programmazione", non ne consegue necessariamente che non vuoi saperlo. Ad ogni modo, i tuoi colleghi non sono buoni professionisti del software, quindi come dice sbi è molto difficile parlare del modo migliore per affrontare una situazione che è cronicamente debole all'inizio.
Steve Jessop,

Risposte:


196

I miei colleghi dicono che dovresti sempre sapere quali eccezioni devono essere lanciate [...]

Il tuo collega, odio dirlo, ovviamente non ha mai lavorato su biblioteche di uso generale.

Come in tutto il mondo può una classe come std::vectoranche fingere di sapere quali sono i costruttori di copia getterà, pur garantendo la sicurezza eccezione?

Se sapessi sempre cosa farebbe la chiamata in fase di compilazione, il polimorfismo sarebbe inutile! A volte l' intero obiettivo è quello di sottrarre ciò che accade a un livello inferiore, quindi in particolare non vuoi sapere cosa sta succedendo!


32
In realtà, anche se sapevano che devono essere lanciate eccezioni. Qual è lo scopo di questa duplicazione del codice? A meno che la gestione non differisca, non vedo alcun senso enumerare le eccezioni per mostrare le tue conoscenze.
Michael Krelin - hacker il

3
@ MichaelKrelin-hacker: anche quello. Inoltre, aggiungi ad esso il fatto che hanno deprecato le specifiche delle eccezioni perché elencare tutte le possibili eccezioni nel codice tendeva a causare bug in seguito ... è l'idea peggiore di sempre.
Mehrdad,

4
E ciò che mi dà fastidio è quella che potrebbe essere l'origine di un simile atteggiamento se associato alla visione di una tecnica utile e conveniente come "materiale scolastico inutile". Ma bene ...
Michael Krelin - hacker il

1
+1, l'enumerazione di tutte le possibili opzioni è una ricetta eccellente per un fallimento futuro, perché mai qualcuno dovrebbe scegliere di farlo ...?
littleadv,

2
Bella risposta. Potrebbe forse trarre vantaggio dal dire che se un compilatore che uno deve supportare ha un bug nell'area X, l'utilizzo della funzionalità dall'area X non è intelligente, almeno non usarlo direttamente. Ad esempio, date le informazioni sull'azienda, non sarei sorpreso se usassero Visual C ++ 6.0, che aveva alcuni stupidi in quest'area (come i distruttori di oggetti di eccezione chiamati due volte) - alcuni discendenti più piccoli di quei primi bug sono sopravvissuti a questo giorno, ma richiedono un'attenta organizzazione per manifestare.
Alf P. Steinbach,

44

Quello che sembra essere catturato è l'inferno specifico di qualcuno che cerca di avere la sua torta e mangiarla anche lei.

RAII ed eccezioni sono progettate per andare di pari passo. RAII è il mezzo con cui non è necessario scrivere molte catch(...)istruzioni per eseguire la pulizia. Accadrà automaticamente, ovviamente. E le eccezioni sono l'unico modo per lavorare con oggetti RAII, perché i costruttori possono solo riuscire o lanciare (o mettere l'oggetto in uno stato di errore, ma chi lo vuole?).

Una catchdichiarazione può fare una delle due cose: gestire un errore o di circostanza eccezionale, o fare il lavoro di pulizia. A volte fa entrambe le cose, ma ogni catchaffermazione esiste per fare almeno una di queste.

catch(...)non è in grado di gestire correttamente le eccezioni. Non sai quale sia l'eccezione; non è possibile ottenere informazioni sull'eccezione. Non hai assolutamente altre informazioni oltre al fatto che un'eccezione è stata generata da qualcosa all'interno di un determinato blocco di codice. L'unica cosa legittima che puoi fare in un blocco del genere è fare pulizia. E questo significa ripetere l'eccezione alla fine della pulizia.

Ciò che RAII ti offre in merito alla gestione delle eccezioni è la pulizia gratuita. Se tutto è incapsulato correttamente in RAII, allora tutto verrà adeguatamente ripulito. Non è più necessario che le catchistruzioni eseguano la pulizia. In tal caso, non c'è motivo di scrivere una catch(...)dichiarazione.

Quindi concordo sul fatto che catch(...)è per lo più malvagio ... provvisoriamente .

Tale disposizione è l'uso corretto di RAII. Perché senza di essa, è necessario essere in grado di fare certa pulizia. Non c'è modo di aggirarlo; devi essere in grado di fare un lavoro di pulizia. Devi essere in grado di assicurarti che il lancio di un'eccezione lascerà il codice in uno stato ragionevole. Ed catch(...)è uno strumento vitale per farlo.

Non puoi averne uno senza l'altro. Non si può dire che sia RAII e catch(...) sono cattivi. Hai bisogno di almeno uno di questi; in caso contrario, non sei sicuro per le eccezioni.

Ovviamente, c'è un uso valido, anche se raro, catch(...)che nemmeno la RAII può bandire: ottenere un exception_ptrinoltro a qualcun altro. In genere tramite promise/futureun'interfaccia simile o simile.

I miei colleghi dicono che dovresti sempre sapere quali eccezioni devono essere lanciate e che puoi sempre usare costrutti come:

Il tuo collega è un idiota (o semplicemente terribilmente ignorante). Questo dovrebbe essere immediatamente ovvio a causa della quantità di codice copia e incolla che sta suggerendo di scrivere. La pulizia per ciascuna di queste dichiarazioni di cattura sarà esattamente la stessa . È un incubo per la manutenzione, per non parlare della leggibilità.

In breve: questo è il problema che RAII è stato creato per risolvere (non che non risolve altri problemi).

Ciò che mi confonde di questa nozione è che è generalmente all'indietro rispetto al modo in cui la maggior parte delle persone sostiene che la RAII è cattiva. In generale, l'argomento dice "RAII è un male perché devi usare le eccezioni per segnalare un errore del costruttore. Ma non puoi lanciare eccezioni, perché non è sicuro e dovrai avere un sacco di catchistruzioni per ripulire tutto". Il che è un argomento rotto perché RAII risolve il problema creato dalla mancanza di RAII.

Molto probabilmente, è contro la RAII perché nasconde i dettagli. Le chiamate ai distruttori non sono immediatamente visibili sulle variabili automatiche. Quindi ottieni codice che viene chiamato in modo implicito. Alcuni programmatori lo odiano davvero. Apparentemente, al punto in cui pensano di avere 3 catchistruzioni, tutte che fanno la stessa cosa con il codice copia e incolla è un'idea migliore.


2
Sembra che tu non scriva codice che fornisce una forte garanzia di sicurezza delle eccezioni. RAII aiuta a fornire una garanzia di base . Ma al fine di fornire una forte garanzia, è necessario annullare alcune azioni per ripristinare il sistema allo stato che aveva prima che la funzione fosse chiamata. La garanzia di base è la pulizia , la garanzia forte è il rollback . Il rollback è specifico della funzione. Quindi non puoi metterlo in "RAII". E questo è quando il blocco catch-all diventa utile. Se si scrive codice con una forte garanzia, si utilizza molto catch-all .
anton_rh,

@anton_rh: forse, ma anche in quei casi, le dichiarazioni generali sono lo strumento di ultima istanza . Lo strumento preferito è fare tutto ciò che genera prima di modificare qualsiasi stato che si dovrebbe ripristinare in caso di eccezione. Ovviamente non è possibile implementare tutto in questo modo in tutti i casi, ma questo è il modo ideale per ottenere la forte garanzia di eccezione.
Nicol Bolas,

14

Due commenti, davvero. Il primo è che mentre sei in un mondo ideale, dovresti sempre sapere quali eccezioni potrebbero essere lanciate, in pratica, se hai a che fare con librerie di terze parti o compilando con un compilatore Microsoft, non lo fai. Più al punto, tuttavia; anche se conosci esattamente tutte le possibili eccezioni, è rilevante qui? catch (...)esprime l'intento molto meglio di catch ( std::exception const& ), anche supponendo che derivino tutte le possibili eccezioni std::exception(che sarebbe il caso in un mondo ideale). Per quanto riguarda l'utilizzo di diversi blocchi di cattura, se non esiste una base comune per tutte le eccezioni: questa è chiaramente offuscamento e un incubo di manutenzione. Come riconosci che tutti i comportamenti sono identici? E quello era l'intento? E cosa succede se devi modificare il comportamento (correzione di bug, ad esempio)? È fin troppo facile perderne uno.


3
In realtà, il mio collega ha progettato la sua classe di eccezioni, che non deriva std::exceptione cerca ogni giorno di farne uso nel nostro codice. La mia ipotesi è che prova a punirmi per aver usato codice e librerie esterne che non ha scritto da solo.
Il

17
@ereOn Mi sembra che il tuo collega abbia un disperato bisogno di formazione. In ogni caso, probabilmente eviterei di usare le librerie scritte da lui.

2
Modelli e sapere quali eccezioni verranno lanciate vanno insieme come burro di arachidi e gechi morti. Qualcosa di così semplice come std::vector<>può generare qualsiasi tipo di eccezione per praticamente qualsiasi motivo.
David Thornley,

3
Per favore, dicci esattamente come fai a sapere quale eccezione verrà generata dalla correzione di bug di domani più in basso nella struttura delle chiamate?
Mattnz,

11

Penso che il tuo collega abbia confuso alcuni buoni consigli: dovresti gestire le eccezioni note in un catchblocco solo quando non le rilasci.

Questo significa:

try
{
  // Stuff
}
catch (...)
{
  // General stuff
}

È male perché nasconderà silenziosamente qualsiasi errore.

Però:

try
{
  // Stuff
}
catch (exception_type_we_can_handle&)
{
  // Deal with the known exception
}

Va bene - sappiamo con cosa abbiamo a che fare e non abbiamo bisogno di esporlo al codice chiamante.

Allo stesso modo:

try
{
  // Stuff
}
catch (...)
{
  // Rollback transactions, log errors, etc
  throw;
}

Va bene, anche la migliore pratica, il codice per gestire gli errori generali dovrebbe essere con il codice che li causa. È meglio che fare affidamento sul chiamato per sapere che una transazione deve essere ripristinata o altro.


9

Qualsiasi risposta o no dovrebbe essere accompagnata da una logica del perché sia ​​così.

Dire che è sbagliato semplicemente perché mi è stato insegnato in quel modo è solo un fanatismo cieco.

Scrivere lo stesso //Some cleanup; throwpiù volte, come nel tuo esempio, è sbagliato perché è la duplicazione del codice e questo è un onere di manutenzione. Scriverlo solo una volta è meglio.

Scrivere a catch(...)per mettere a tacere tutte le eccezioni è sbagliato perché dovresti gestire solo le eccezioni che sai come gestire, e con quel jolly puoi catturare più di quanto ti aspetti, e così facendo puoi mettere a tacere errori importanti.

Ma se ripeti la ricerca dopo un catch(...), allora quest'ultima logica non si applica più, poiché in realtà non stai gestendo l'eccezione, quindi non c'è motivo per cui questo dovrebbe essere scoraggiato.

In realtà l'ho fatto per accedere a funzioni sensibili senza alcun problema:

void DoSomethingImportant()
{
    try
    {
        Log("Going to do something important");
        DoIt();
    }
    catch (std::exception &e)
    {
        Log("Error doing something important: %s", e.what());
        throw;
    }
    catch (...)
    {
        Log("Unexpected error doing something important");
        throw;
    }
    Log("Success doing something important");
}

2
Speriamo Log(...)non possiamo lanciare.
Deduplicatore

2

In genere sono d'accordo con l'umore dei post qui, non mi piace molto il modello di cattura di specifiche eccezioni: penso che la sintassi di questo sia ancora agli inizi e non sia ancora in grado di far fronte al codice ridondante.

Ma dal momento che tutti lo dicono, farò il punto sul fatto che, anche se li uso con parsimonia, ho spesso guardato una delle mie dichiarazioni "catch (Exception e)" e detto "Accidenti, vorrei aver chiamato le eccezioni specifiche di quel tempo "perché quando arrivi più tardi è spesso bello sapere quale fosse l'intenzione e ciò che il cliente potrebbe gettare a colpo d'occhio.

Non sto giustificando l'atteggiamento di "Usa sempre x", sto solo dicendo che occasionalmente è bello vederli elencati e sono sicuro che alcune persone pensano che sia la strada "giusta".

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.