Come posso creare e far rispettare i contratti per le eccezioni?

31

Sto cercando di convincere il mio team a consentire l'uso di eccezioni in C ++ invece di restituire un bool isSuccessful o 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 funzione B() . Controlla la sua documentazione e vede che non restituisce eccezioni, quindi non cerca di catturare nulla. Questo codice potrebbe mandare in crash il programma in produzione.

  2. Considera uno sviluppatore che chiama la funzione C() . Non controlla la documentazione, quindi non rileva eccezioni. La chiamata non è sicura e potrebbe causare il crash del programma in produzione.

Ma se controlliamo gli errori in questo modo:

void old_C(myenum &return_code);

Uno sviluppatore che usa quella funzione sarà avvisato dal compilatore se non fornisce quell'argomento, e dirà "Aha, questo restituisce un codice di errore che devo controllare."

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

    
posta DBedrenko 28.10.2016 - 11:15
fonte

5 risposte

48

Questa è una critica legittima alle eccezioni. Spesso sono meno visibili di una semplice gestione degli errori come la restituzione di un codice. E non esiste un modo semplice per far rispettare un "contratto". Parte del punto è di consentire di far sì che le eccezioni vengano catturate a un livello più alto (se si deve catturare ogni eccezione ad ogni livello, quanto è diverso dal restituire un codice di errore, comunque?). Questo significa che il tuo codice potrebbe essere chiamato da un altro codice che non lo gestisce in modo appropriato.

Le eccezioni hanno aspetti negativi; devi creare 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 inclinarsi a favore delle eccezioni, è lontano da un chiaro consenso.

Per convincere la tua squadra a comandare, questa potrebbe non essere la battaglia giusta da scegliere. Soprattutto non con il codice legacy. Come indicato nel secondo link sopra:

Exceptions cannot be propagated through any code which is not exception safe. The use of exceptions thus implies that all code in the project must be exception safe.

Aggiungere un po 'di codice che usa eccezioni a un progetto che principalmente non è non probabilmente sarà un miglioramento. Non usare eccezioni in codice altrimenti ben scritto è lontano da un problema catastrofico; potrebbe non essere affatto un problema, a seconda dell'applicazione e di quale esperto chiedi. Devi scegliere le tue battaglie.

Questo probabilmente non è un argomento su cui vorrei dedicarmi - almeno non fino a quando non verrà avviato un nuovo progetto. E anche se hai un nuovo progetto, verrà utilizzato o utilizzato da qualsiasi codice legacy?

    
risposta data 28.10.2016 - 11:35
fonte
26

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

Le eccezioni sono destinate a NON essere catturate ovunque.

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 di cattura, se non del tutto, al di fuori del ciclo principale. E questo sembra essere l'approccio comune nel moderno C ++.

Le eccezioni e i codici di ritorno non si escludono a vicenda.

Non dovresti renderlo un approccio all-or-nothing. 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 verificare se un nome file fornito dall'utente sia valido, non è un caso d'uso per eccezioni; Utilizza invece un valore di ritorno in questi casi.

Come puoi vedere 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 refuso".

Quindi non esiste una regola assoluta. Una linea guida approssimativa è: se può essere gestito localmente, rendilo 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 essere gestite localmente comunque, di solito non è importante quali eccezioni possano essere lanciate. L'unica informazione utile è se può essere lanciata qualche eccezione.

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

Per questi motivi, C ++ ha deprecato 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 altrimenti.

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

Direi piuttosto che scrivere codice eccezionalmente sicuro è tutto su come evitare di scrivere gestori di eccezioni!

La maggior parte delle best practice intende ridurre il numero di gestori di eccezioni. Scrivi una volta il codice e invocalo automaticamente, ad es. attraverso RAII - si traduce in meno bug rispetto al copia-incolla dello stesso codice dappertutto.

    
risposta data 28.10.2016 - 12:38
fonte
8

I programmatori C ++ non cercano le specifiche delle eccezioni. Cercano garanzie di eccezione.

Supponiamo che una parte di codice abbia generato un'eccezione. Quali ipotesi può fare il programmatore che sarà ancora valido? Dal modo in cui è scritto il codice, cosa garantisce il codice all'indomani di un'eccezione?

O è possibile che un certo pezzo di codice possa garantire di non gettare mai (cioè niente a meno che il processo del sistema operativo venga terminato)?

La parola "rollback" si verifica frequentemente nelle discussioni sulle eccezioni. Essere in grado di ripristinare uno stato valido (che è esplicitamente documentato) è un esempio di garanzia di eccezione. Se non ci sono garanzie di eccezione, un programma dovrebbe terminare sul posto perché non è nemmeno garantito che qualsiasi codice che eseguirà successivamente funzionerà come previsto - diciamo, se la memoria è danneggiata, qualsiasi ulteriore operazione è tecnicamente un comportamento indefinito.

Varie tecniche di programmazione C ++ promuovono le garanzie di eccezione. RAII (gestione delle risorse basata su ambito) fornisce un meccanismo per eseguire il codice di pulizia e assicurare 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 tale oggetto se l'operazione non riesce. E così via.

Le risposte a questa domanda StackOverflow dà un'occhiata alla grande durata programmatori C ++ vanno a capire tutte le possibili modalità di errore che potrebbero accadere al loro codice e cercare di salvaguardare la validità dello stato del programma, nonostante i fallimenti. L'analisi riga per riga del codice C ++ diventa un'abitudine.

Quando si sviluppa in C ++ (per uso produttivo), non ci si può permettere di soffermarsi sui dettagli. Inoltre, blob blob (non-open-source) è la rovina dei programmatori C ++. Se devo chiamare un blob binario e il blob fallisce, il reverse engineering è ciò che un programmatore C ++ farebbe dopo.

Riferimento: link - vedi in Sicurezza delle eccezioni.

C ++ ha avuto un tentativo fallito di implementare le specifiche delle eccezioni. Analisi successive in altre lingue dicono che le specifiche delle eccezioni semplicemente non sono pratici.

Perché è un tentativo fallito: applicarlo rigorosamente, deve essere parte del sistema di tipi. Ma non lo è. Il compilatore non controlla le specifiche delle eccezioni.

Perché C ++ l'ha scelto, e perché esperienze da altri linguaggi (Java) dimostra che le specifiche delle eccezioni sono discutibili: come si modifica l'implementazione di una funzione (per esempio, è necessario effettuare una chiamata a una funzione diversa che può lanciare un nuovo tipo di eccezione), una rigorosa applicazione delle specifiche di eccezione significa che devi aggiornare anche quella specifica. Questo si propaga - potresti finire per dover aggiornare le specifiche delle eccezioni per dozzine o centinaia di funzioni per quella che è una semplice modifica. Le cose peggiorano per le classi base astratte (l'equivalente C ++ delle interfacce). Se le specifiche delle eccezioni vengono applicate sulle interfacce, le implementazioni delle interfacce non saranno autorizzate a richiamare funzioni che generano tipi diversi di eccezioni.

Riferimento: link

A partire da C ++ 17, l'attributo [[nodiscard]] può essere utilizzato sui valori di ritorno della funzione (vedi: link ).

Quindi, se faccio un cambio di codice e questo introduce un nuovo tipo di condizione di errore (cioè un nuovo tipo di eccezione), si tratta di un cambio di rottura? Dovrebbe aver costretto il chiamante ad aggiornare il codice, o almeno essere avvisato della modifica?

Se accetti gli argomenti secondo i quali i programmatori C ++ cercano garanzie di eccezione invece di specifiche di eccezione, la risposta è che se il nuovo tipo di condizione di errore non interrompe alcuna eccezione garantisce che il codice promette in precedenza, non è un rompere il cambiamento.

    
risposta data 28.10.2016 - 15:42
fonte
4

Consider a developer calling the C() function. He doesn't check the documentation so doesn't catch any exceptions. The call is unsafe

È totalmente sicuro. Non ha bisogno di catturare eccezioni in ogni posto, può semplicemente lanciare un tentativo / catturare in un posto dove può effettivamente fare qualcosa di utile a riguardo. Non è sicuro se gli permette di filtrare da una discussione, ma in genere non è difficile impedire che ciò accada.

    
risposta data 29.10.2016 - 22:15
fonte
3

Se stai costruendo un sistema critico, considera di seguire i consigli del tuo team lead e di non usare eccezioni. Questa è la regola AV 208 in gli standard di codifica C ++ di Joint Strike Fighter Air Vehicle di Lockheed Martin . D'altra parte, le MISRA linee guida C ++ hanno regole molto specifiche riguardo a quando le eccezioni possono e non possono essere utilizzate se si sta costruendo un MISRA- sistema software compatibile.

Se si stanno costruendo sistemi critici, è probabile che si stiano eseguendo anche strumenti di analisi statica. Molti strumenti di analisi statica avviseranno se non si controlla il valore di ritorno di un metodo, rendendo facilmente evidenti i casi di mancata gestione degli errori. Per quanto ne so, il supporto di strumenti simili per il rilevamento della corretta gestione delle eccezioni non è così strong.

In definitiva, direi che design per contratto e la programmazione difensiva, insieme all'analisi statica, è più sicuro per i sistemi software critici di eccezioni.

    
risposta data 28.10.2016 - 16:46
fonte

Leggi altre domande sui tag