Come posso creare e applicare contratti per eccezioni?


33

Sto cercando di convincere il mio team a consentire l'utilizzo delle eccezioni in C ++ invece di restituire un bool isSuccessfulo un enum con il codice di errore. Tuttavia, non posso contrastare questa sua critica.

Considera questa libreria:

class OpenFileException() : public std::runtime_error {
}

void B();
void C();

/** Does blah and blah. */
void B() {
    // The developer of B() either forgot to handle C()'s exception or
    // chooses not to handle it and let it go up the stack.
    C();
};

/** Does blah blah.
 *
 * @raise OpenFileException When we failed to open the file. */
void C() {
    throw new OpenFileException();
};
  1. Considera uno sviluppatore che chiama la B()funzione. Controlla la sua documentazione e vede che non restituisce eccezioni, quindi non cerca di catturare nulla. Questo codice potrebbe causare l'arresto anomalo del programma in produzione.

  2. Considera uno sviluppatore che chiama la C()funzione. Non controlla la documentazione, quindi non rileva alcuna eccezione. La chiamata non è sicura e potrebbe causare l'arresto anomalo del programma in produzione.

Ma se controlliamo la presenza di errori in questo modo:

void old_C(myenum &return_code);

Uno sviluppatore che utilizza quella funzione verrà avvisato dal compilatore se non fornisce tale argomento, e direbbe "Ah, questo restituisce un codice di errore che devo verificare."

Come posso utilizzare le eccezioni in modo sicuro, in modo che ci sia una sorta di contratto?


4
@Sjoerd Il vantaggio principale è che uno sviluppatore è costretto dal compilatore a fornire una variabile alla funzione per memorizzare il codice di ritorno; è consapevole che può esserci un errore e dovrebbe gestirlo. È infinitamente migliore delle eccezioni, che non hanno alcun tempo di compilazione per il controllo.
DBedrenko,

4
"dovrebbe gestirlo" - non è possibile verificare se ciò accada.
Sjoerd,

4
@Sjoerd Non è necessario. È abbastanza buono per rendere lo sviluppatore consapevole che può verificarsi un errore e che dovrebbero verificarlo. È evidente anche ai revisori, se hanno verificato l'errore o meno.
DBedrenko,

5
Potresti avere maggiori possibilità di usare una Either/ Resultmonade per restituire l'errore in modo componibile sicuro per il tipo
Daenyth,

5
@gardenhead Perché troppi sviluppatori non lo usano correttamente, finiscono con un brutto codice e procedono a incolpare la funzione invece di se stessi. La funzionalità di eccezione verificata in Java è una cosa bellissima, ma ottiene un brutto rap perché alcune persone semplicemente non lo capiscono.
AxiomaticNexus,

Risposte:


47

Questa è una legittima critica delle eccezioni. Spesso sono meno visibili della semplice gestione degli errori come la restituzione di un codice. E non esiste un modo semplice per far rispettare un "contratto". Parte del punto è consentirti di far intercettare le eccezioni a un livello superiore (se devi intercettare tutte le eccezioni ad ogni livello, in che modo è diverso dalla restituzione di un codice di errore?). E questo significa che il tuo codice potrebbe essere chiamato da qualche altro codice che non lo gestisce in modo appropriato.

Le eccezioni hanno degli aspetti negativi; devi presentare un caso basato sul rapporto costi-benefici.
Ho trovato utili questi due articoli: La necessità di eccezioni e Tutto sbagliato con le eccezioni. Inoltre, questo post sul blog offre opinioni di molti esperti sulle eccezioni, con particolare attenzione al C ++ . Mentre l'opinione degli esperti sembra appoggiarsi a eccezioni, è ben lungi dall'essere un chiaro consenso.

Per quanto riguarda convincere il vantaggio della tua squadra, questa potrebbe non essere la battaglia giusta da scegliere. Soprattutto non con il codice legacy. Come notato nel secondo link sopra:

Le eccezioni non possono essere propagate attraverso alcun codice che non sia sicuro. L'uso di eccezioni implica quindi che tutto il codice nel progetto deve essere sicuro per le eccezioni.

L'aggiunta di un po 'di codice che utilizza eccezioni a un progetto che principalmente non lo farà probabilmente non sarà un miglioramento. Non usare eccezioni in codice altrimenti ben scritto è tutt'altro che un problema catastrofico; potrebbe non essere affatto un problema, a seconda dell'applicazione e dell'esperto che chiedi. Devi scegliere le tue battaglie.

Questo probabilmente non è un argomento su cui mi impegnerei - almeno non fino all'avvio di un nuovo progetto. E anche se hai un nuovo progetto, verrà utilizzato o utilizzato da qualsiasi codice legacy?


1
Grazie per il consiglio e i collegamenti. Questo è un nuovo progetto, non precedente. Mi chiedo perché le eccezioni siano così diffuse quando hanno un così grande svantaggio che rende il loro uso non sicuro. Se rilevi un'eccezione n livelli più alti, successivamente modifichi il codice e lo sposti, non esiste alcun controllo statico per garantire che l'eccezione sia ancora rilevata e gestita (ed è troppo chiedere ai revisori di controllare in profondità tutte le funzioni n-livelli ).
DBedrenko,

3
@Vedi i link sopra per alcuni argomenti a favore delle eccezioni (ho aggiunto un link aggiuntivo con un parere più esperto).

6
Questa risposta è perfetta!
Doc Brown,

1
Per esperienza, posso affermare che cercare di aggiungere eccezioni a una base di codice preesistente è davvero una cattiva idea. Ho lavorato con un tale codebase ed è stato DAVVERO, DAVVERO difficile lavorare perché non hai mai saputo cosa ti sarebbe saltato in faccia. Le eccezioni dovrebbero essere utilizzate ESCLUSIVAMENTE nei progetti in cui si pianificano in anticipo.
Michael Kohne,

26

Ci sono letteralmente interi libri scritti su questo argomento, quindi ogni risposta sarà al massimo una sintesi. Ecco alcuni dei punti importanti che penso valga la pena fare, in base alla tua domanda. Non è un elenco esaustivo.


Le eccezioni NON sono intese per essere catturate dappertutto.

Finché esiste un gestore di eccezioni generale nel ciclo principale - a seconda del tipo di applicazione (server Web, servizio locale, utilità della riga di comando ...) - di solito hai tutti i gestori di eccezioni di cui hai bisogno.

Nel mio codice, ci sono solo alcune dichiarazioni catch - se non del tutto - al di fuori del ciclo principale. E quello sembra essere l'approccio comune nel C ++ moderno.


Eccezioni e codici di ritorno non si escludono a vicenda.

Non dovresti renderlo un approccio tutto o niente. Le eccezioni dovrebbero essere utilizzate per situazioni eccezionali. Cose come "File di configurazione non trovato", "Disco pieno" o qualsiasi altra cosa che non può essere gestita localmente.

Errori comuni, come la verifica della validità di un nome file fornito dall'utente, non rappresentano un caso d'uso per le eccezioni; Utilizzare invece un valore di ritorno in questi casi.

Come si vede dagli esempi precedenti, "file non trovato" può essere un'eccezione o un codice di ritorno, a seconda del caso d'uso: "fa parte dell'installazione" contro "l'utente può fare un errore di battitura".

Quindi non esiste una regola assoluta. Una linea guida approssimativa è: se può essere gestita localmente, renderla un valore di ritorno; se non riesci a gestirlo localmente, genera un'eccezione.


Il controllo statico delle eccezioni non è utile.

Poiché le eccezioni non devono comunque essere gestite localmente, di solito non è importante quali eccezioni possano essere generate. Le uniche informazioni utili sono se è possibile generare un'eccezione.

Java ha un controllo statico, ma di solito è considerato un esperimento fallito e la maggior parte delle lingue poiché - in particolare C # - non hanno quel tipo di controllo statico. Questa è una buona lettura dei motivi per cui C # non ce l'ha.

Per questi motivi, C ++ ha disapprovato il suo throw(exceptionA, exceptionB)a favore di noexcept(true). L'impostazione predefinita è che una funzione può essere lanciata, quindi i programmatori dovrebbero aspettarselo, a meno che la documentazione non prometta esplicitamente il contrario.


La scrittura di codice sicuro di eccezione non ha nulla a che fare con la scrittura di gestori di eccezioni.

Preferirei dire che scrivere il codice sicuro delle eccezioni è tutto su come evitare di scrivere gestori di eccezioni!

La maggior parte delle migliori pratiche intende ridurre il numero di gestori di eccezioni. Scrivere una volta il codice e invocarlo automaticamente, ad esempio tramite RAII, si traduce in un minor numero di bug rispetto al copia e incolla dello stesso codice ovunque.


3
" Finché esiste un gestore di eccezioni generale ... di solito stai già bene. " Ciò contraddice la maggior parte delle fonti, il che sottolinea che è molto difficile scrivere un codice sicuro per le eccezioni.

12
@ dan1111 "Scrivere un codice sicuro di eccezione" ha poco a che fare con la scrittura di gestori di eccezioni. Scrivere un codice sicuro per le eccezioni riguarda l'utilizzo di RAII per incapsulare le risorse, usando "prima fai tutto il lavoro localmente, poi usa swap / move" e altre best practice. Sono difficili quando non ci sei abituato. Ma "scrivere gestori di eccezioni" non fa parte di queste migliori pratiche.
Sjoerd,

4
@AxiomaticNexus Ad un certo livello, puoi spiegare ogni utilizzo non riuscito di una funzione come "cattivo sviluppatore". Le idee migliori sono quelle che riducono il cattivo utilizzo perché rendono semplice fare la cosa giusta. Le eccezioni controllate staticamente non rientrano in quella categoria. Creano un lavoro sproporzionato rispetto al loro valore, spesso in luoghi in cui il codice da scrivere semplicemente non ha nemmeno bisogno di preoccuparsene. Gli utenti sono tentati di silenziarli in modo errato per impedire al compilatore di disturbarli. Anche se fatto bene, portano a un sacco di disordine.
jpmc26,

4
@AxiomaticNexus Il motivo per cui è sproporzionato è che questo può propagarsi facilmente a quasi tutte le funzioni dell'intera base di codice . Quando tutte le funzioni della metà delle mie classi accedono al database, non tutte devono dichiarare esplicitamente che può lanciare una SQLException; né tutti i metodi controller che li chiamano. Quel rumore è in realtà un valore negativo, indipendentemente da quanto sia piccola la quantità di lavoro. Sjoerd colpisce l'unghia sulla testa: la maggior parte del tuo codice non ha bisogno di preoccuparsi delle eccezioni perché non può fare nulla al riguardo .
jpmc26,

3
@AxiomaticNexus Sì, perché i migliori sviluppatori sono così perfetti che il loro codice gestirà sempre perfettamente ogni singolo input utente possibile e non vedrai mai e poi mai queste eccezioni in prod. E se lo fai, significa che sei un fallimento nella vita. Seriamente, non darmi questa spazzatura. Nessuno è perfetto, nemmeno il migliore. E la tua app dovrebbe comportarsi in modo sano anche di fronte a quegli errori, anche se "in modo sicuro" è "fermati e restituisci una pagina / messaggio di errore decente". E indovina un po ': è la stessa cosa che fai anche con il 99% di IOExceptions . La divisione controllata è arbitraria e inutile.
jpmc26,

8

I programmatori C ++ non cercano specifiche di eccezione. Cercano garanzie eccezionali.

Supponiamo che un pezzo di codice abbia generato un'eccezione. Quali ipotesi può fare il programmatore che saranno ancora valide? Dal modo in cui è scritto il codice, cosa garantisce il codice a seguito di un'eccezione?

O è possibile che un certo codice possa garantire di non lanciare mai (ovvero a meno che il processo del sistema operativo sia terminato)?

La parola "rollback" compare frequentemente nelle discussioni sulle eccezioni. Essere in grado di ripristinare uno stato valido (che è esplicitamente documentato) è un esempio di garanzia di eccezione. Se non vi è alcuna garanzia di eccezione, un programma dovrebbe essere terminato sul posto perché non è nemmeno garantito che qualsiasi codice che eseguirà successivamente funzionerà come previsto - diciamo, se la memoria ha corrotto, ogni ulteriore operazione è un comportamento tecnicamente indefinito.

Varie tecniche di programmazione C ++ promuovono garanzie di eccezione. RAII (gestione delle risorse basata sull'ambito) fornisce un meccanismo per eseguire il codice di pulizia e garantire che le risorse vengano rilasciate sia in casi normali che in casi eccezionali. Effettuare una copia dei dati prima di eseguire modifiche sugli oggetti consente di ripristinare lo stato di quell'oggetto se l'operazione non riesce. E così via.

Le risposte a questa domanda StackOverflow danno uno sguardo alle grandi lunghezze che i programmatori C ++ cercano di comprendere tutte le possibili modalità di errore che potrebbero verificarsi nel loro codice e provano a proteggere la validità dello stato del programma nonostante gli errori. L'analisi riga per riga del codice C ++ diventa un'abitudine.

Quando si sviluppa in C ++ (per uso in produzione), non ci si può permettere di sorvolare i dettagli. Inoltre, il BLOB binario (non open-source) è la rovina dei programmatori C ++. Se devo chiamare un BLOB binario e il BLOB non riesce, il reverse engineering è quello che farebbe un programmatore C ++.

Riferimento: http://en.cppreference.com/w/cpp/language/exceptions#Exception_safety - consultare la sezione relativa alla sicurezza delle eccezioni.

C ++ ha fallito nel tentativo di implementare le specifiche delle eccezioni. Un'analisi successiva in altre lingue afferma che le specifiche delle eccezioni semplicemente non sono pratiche.

Perché è un tentativo fallito: per applicarlo rigorosamente, deve far parte del sistema dei tipi. Ma non lo è. Il compilatore non verifica la specifica dell'eccezione.

Perché C ++ lo ha scelto e perché le esperienze da altri linguaggi (Java) dimostrano che la specifica dell'eccezione è controversa: quando si modifica l'implementazione di una funzione (ad esempio, è necessario effettuare una chiamata a una funzione diversa che potrebbe generare un nuovo tipo di eccezione), un'applicazione rigorosa delle specifiche delle eccezioni significa che è necessario aggiornare anche tali specifiche. Questo si propaga: potresti dover aggiornare le specifiche delle eccezioni per dozzine o centinaia di funzioni per una semplice modifica. Le cose peggiorano per le classi base astratte (l'equivalente C ++ delle interfacce). Se la specifica dell'eccezione viene applicata alle interfacce, le implementazioni delle interfacce non potranno chiamare funzioni che generano diversi tipi di eccezioni.

Riferimento: http://www.gotw.ca/publications/mill22.htm

A partire da C ++ 17, l' [[nodiscard]]attributo può essere utilizzato sui valori di ritorno della funzione (vedere: https://stackoverflow.com/questions/39327028/can-ac-function-be-declared-such-that-the-return-value -cannot-be-ignored ).

Quindi, se apporto una modifica del codice e questo introduce un nuovo tipo di condizione di errore (ovvero un nuovo tipo di eccezione), si tratta di una modifica di rottura? Avrebbe dovuto costringere il chiamante ad aggiornare il codice o almeno essere avvisato della modifica?

Se si accettano gli argomenti che i programmatori C ++ cercano garanzie di eccezione invece di specifiche di eccezione, la risposta è che se il nuovo tipo di condizione di errore non rompe nessuna delle eccezioni garantisce che il codice precedentemente promesso, non si tratta di una modifica di rottura.


"Quando si modifica l'implementazione di una funzione (ad esempio, è necessario effettuare una chiamata a un'altra funzione che potrebbe generare un nuovo tipo di eccezione), un'applicazione rigorosa delle specifiche delle eccezioni significa che è necessario aggiornare anche quella specifica" - Buono , come dovresti. Quale sarebbe l'alternativa? Ignorando l'eccezione appena introdotta?
AxiomaticNexus,

@AxiomaticNexus Anche questo è garantito da una garanzia eccezionale. In effetti, sostengo che le specifiche non possono mai essere complete; la garanzia è ciò che cerchiamo. Spesso, confrontiamo il tipo di eccezione nel tentativo di scoprire quale garanzia era rotta - al fine di indovinare quale garanzia era ancora valida. La specifica dell'eccezione elenca alcuni dei tipi che è necessario verificare, ma ricordare che in qualsiasi sistema pratico, l'elenco non può essere completo. Il più delle volte, costringe le persone a prendere la scorciatoia di "mangiare" il tipo di eccezione, avvolgendolo in un'altra eccezione, il che rende difficile il controllo del tipo di eccezione
rwong

@AxiomaticNexus Non sto discutendo a favore o contro l'uso dell'eccezione. L'utilizzo tipico delle eccezioni incoraggia ad avere una classe di eccezione di base e alcune classi di eccezioni derivate. Nel frattempo, bisogna ricordare che sono possibili altri tipi di eccezione, ad esempio quelli lanciati da C ++ STL o altre librerie. Si potrebbe sostenere che in questo caso si potrebbe far funzionare la specifica dell'eccezione: specificare che std::exceptionverranno generate le eccezioni standard ( ) e la propria eccezione di base. Ma quando applicato a un intero progetto, significa semplicemente che ogni singola funzione avrà la stessa specifica: il rumore.
rwong,

Vorrei sottolineare che quando si tratta di eccezioni, C ++ e Java / C # sono lingue diverse. In primo luogo C ++ non è sicuro per i tipi nel senso di consentire l'esecuzione di codice arbitrario o la sovrascrittura dei dati, in secondo luogo C ++ non stampa una bella traccia dello stack. Queste differenze hanno costretto i programmatori C ++ a fare diverse scelte in termini di gestione degli errori e delle eccezioni. Significa anche che alcuni progetti potrebbero legittimamente affermare che il C ++ non è un linguaggio adatto a loro. (Ad esempio, ritengo personalmente che la decodifica delle immagini TIFF non debba essere implementata in C o C ++.)
rwong,

1
@rwong: Un grosso problema con la gestione delle eccezioni nei linguaggi che seguono il modello C ++ è che catchsi basa sul tipo di oggetto eccezione, quando in molti casi ciò che conta non è tanto la causa diretta dell'eccezione ma piuttosto ciò che implica lo stato del sistema. Sfortunatamente, il tipo di un'eccezione generalmente non dice nulla sul fatto che abbia causato l'uscita del codice in modo dirompente da un pezzo di codice in un modo che ha lasciato un oggetto in uno stato parzialmente aggiornato (e quindi non valido).
supercat,

4

Considera uno sviluppatore che chiama la funzione C (). Non controlla la documentazione, quindi non rileva alcuna eccezione. La chiamata non è sicura

È totalmente sicuro. Non ha bisogno di catturare eccezioni in ogni luogo, può semplicemente fare un tentativo in un posto dove può effettivamente fare qualcosa di utile al riguardo. Non è sicuro solo se gli consente di uscire da un thread, ma in genere non è difficile impedire che ciò accada.


Non è sicuro poiché non si aspetta che venga emessa un'eccezione C()e, di conseguenza, non protegge dal codice dopo la chiamata che viene ignorata. Se quel codice è richiesto per garantire che una certa invarianza rimanga vera, quella garanzia diventa falsa alla prima eccezione che viene fuori C(). Non esiste un software perfettamente sicuro dalle eccezioni, e certamente non dove non si sarebbero mai aspettate eccezioni.
cmaster

1
@cmaster Non aspettarsi un'eccezioneC() è un grosso errore. L'impostazione predefinita in C ++ è che qualsiasi funzione può essere lanciata - richiede noexcept(true)altrimenti un esplicito per dire al compilatore. In assenza di questa promessa, un programmatore deve presumere che una funzione possa essere lanciata.
Sjoerd,

1
@cmaster Questa è la sua stupida colpa e niente a che fare con l'autore di C (). La sicurezza delle eccezioni è una cosa, dovrebbe conoscerla e seguirla. Se chiama una funzione che non sa di essere assolutamente esclusa, dovrebbe dannatamente gestire bene cosa succede se viene generata. Se ha bisogno di noexcept, dovrebbe trovare un'implementazione che sia.
DeadMG

@Sjoerd e DeadMG: sebbene lo standard consenta a qualsiasi funzione di generare un'eccezione, ciò non significa che i progetti debbano consentire eccezioni nel loro codice. Se si vietano le eccezioni dal proprio codice, proprio come sembra fare il comando del team dell'OP, non è necessario eseguire una programmazione sicura delle eccezioni. E se provi a convincere un team leader a consentire eccezioni in una base di codice che in precedenza era libera dalle eccezioni, ci sarebbero un sacco di C()funzioni che si rompono in presenza di eccezioni. Questa è davvero una cosa poco saggia da fare, è destinata a portare a un sacco di problemi.
cmaster

@cmaster Si tratta di un nuovo progetto: vedi il primo commento sulla risposta accettata. Quindi non ci sono funzioni esistenti che si interrompono, in quanto non esistono funzioni esistenti.
Sjoerd,

3

Se stai costruendo un sistema critico, considera di seguire i consigli del tuo team leader e non usare eccezioni. Questa è la regola AV 208 negli standard di codifica C ++ per veicoli aerei da combattimento congiunti Lockheed Martin . D'altra parte, le linee guida MISRA C ++ hanno regole molto specifiche su quando le eccezioni possono e non possono essere utilizzate se si sta costruendo un sistema software compatibile MISRA.

Se si creano sistemi critici, è probabile che si stiano eseguendo anche strumenti di analisi statica. Molti strumenti di analisi statica avvisano se non si controlla il valore di ritorno di un metodo, rendendo immediatamente evidenti i casi di gestione degli errori mancanti. Per quanto ne so, il supporto di strumenti simili per rilevare una corretta gestione delle eccezioni non è altrettanto efficace.

In definitiva, direi che la progettazione per contratto e la programmazione difensiva, insieme all'analisi statica, sono più sicure per i sistemi software critici rispetto alle eccezioni.

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.