Si dovrebbe verificare ogni piccolo errore in C?


60

Come buon programmatore si dovrebbero scrivere codici robusti che gestiranno ogni singolo risultato del suo programma. Tuttavia, quasi tutte le funzioni della libreria C restituiranno 0 o -1 o NULL in caso di errore.

A volte è ovvio che è necessario il controllo degli errori, ad esempio quando si tenta di aprire un file. Ma spesso ignoro il controllo degli errori nelle funzioni come printfo anche mallocperché non mi sento necessario.

if(fprintf(stderr, "%s", errMsg) < 0){
    perror("An error occurred while displaying the previous error.");
    exit(1);
}

È una buona pratica ignorare solo alcuni errori o esiste un modo migliore per gestire tutti gli errori?


13
Dipende dal livello di robustezza richiesto per il progetto a cui stai lavorando. I sistemi che hanno la possibilità di ricevere input da parti non attendibili (ad esempio server rivolti al pubblico) o che operano in ambienti non completamente fidati, devono essere codificati con molta cautela, per evitare che il codice diventi una bomba a orologeria (o che il collegamento più debole venga violato ). Ovviamente, i progetti di hobby e di apprendimento non hanno bisogno di tanta robustezza.
rwong

1
Alcune lingue forniscono eccezioni. Se non si rilevano eccezioni, il thread verrà terminato, il che è meglio che lasciarlo continuare con un cattivo stato. Se si sceglie di rilevare eccezioni, è possibile coprire molti errori in numerose righe di codice, tra cui funzioni e metodi invocati con un'unica tryistruzione, quindi non è necessario controllare ogni singola chiamata o operazione. (Si noti inoltre che alcune lingue sono migliori di altre nel rilevare errori semplici come la null nullità o l'indice di array fuori limite.)
Erik Eidt

1
Il problema è principalmente metodologico: non si scrive il controllo degli errori in genere quando si sta ancora cercando di capire cosa si dovrebbe implementare e non si desidera aggiungere il controllo degli errori in questo momento perché si desidera ottenere il "percorso felice" giusto prima. Ma dovresti ancora controllare per malloc and co. Invece di saltare la fase di verifica degli errori, devi dare la priorità alle tue attività di codifica e lasciare che la verifica degli errori sia una fase implicita di refactoring permanente nell'elenco TODO, applicata ogni volta che sei soddisfatto del tuo codice. L'aggiunta di commenti "/ * CHECK * /" non validi può essere d'aiuto.
coredump,

7
Non riesco a credere che nessuno sia menzionato errno! Nel caso in cui non si abbia familiarità, mentre è vero che "quasi tutte le funzioni della libreria C restituiranno 0 o −1 o NULLquando si verifica un errore", impostano anche la errnovariabile globale , a cui è possibile accedere utilizzando #include <errno.h>e quindi semplicemente leggendo il valore di errno. Quindi, ad esempio, se open(2) ritorna -1, potresti voler verificare se errno == EACCES, che indicherebbe un errore di autorizzazione, o ENOENT, che indicherebbe che il file richiesto non esiste.
wchargin,

1
@ErikEidt C non supporta try/ catch, anche se potresti simularlo con i salti.
Albero

Risposte:


29

In generale, il codice dovrebbe trattare condizioni eccezionali ovunque sia appropriato. Sì, questa è una dichiarazione vaga.

Nei linguaggi di livello superiore con gestione delle eccezioni del software, questo viene spesso indicato come "intercettare l'eccezione nel metodo in cui è possibile fare effettivamente qualcosa al riguardo". Se si è verificato un errore del file, forse lo lasci fare il bubble up dello stack al codice dell'interfaccia utente che può effettivamente dire all'utente "il tuo file non è stato salvato sul disco". Il meccanismo delle eccezioni ingoia efficacemente "ogni piccolo errore" e lo gestisce implicitamente nel posto appropriato.

In C, non hai quel lusso. Esistono alcuni modi per gestire gli errori, alcuni dei quali sono funzionalità di lingua / libreria, alcuni dei quali sono pratiche di codifica.

È una buona pratica ignorare solo alcuni errori o esiste un modo migliore per gestire tutti gli errori?

Ignorare alcuni errori? Può essere. Ad esempio, è ragionevole supporre che la scrittura nell'output standard non fallisca. Se fallisce, come diresti all'utente, comunque? Sì, è una buona idea ignorare alcuni errori o codificare in modo difensivo per prevenirli. Ad esempio, controllare lo zero prima di dividere.

Esistono modi per gestire tutti o almeno la maggior parte degli errori:

  1. È possibile utilizzare i salti, simili a goto, per la gestione degli errori . Sebbene sia un problema controverso tra i professionisti del software, ci sono usi validi per loro soprattutto nel codice incorporato e critico per le prestazioni (ad esempio kernel Linux).
  1. Cascading ifs:

    if (!<something>) {
      printf("oh no 1!");
      return;
    }
    if (!<something else>) {
      printf("oh no 2!");
      return;
    }
    
  2. Testare la prima condizione, ad esempio l'apertura o la creazione di un file, quindi presupporre che le operazioni successive abbiano esito positivo.

Il codice robusto è buono e si dovrebbe verificare e gestire gli errori. Quale metodo è meglio per il tuo codice dipende da cosa fa il codice, da quanto è critico un errore, ecc. E solo tu puoi veramente rispondere. Tuttavia, questi metodi sono testati in battaglia e utilizzati in vari progetti open source in cui puoi dare un'occhiata per vedere come il codice reale controlla gli errori.


21
Siamo spiacenti, un pò da scegliere qui: "scrivere sull'output standard non fallirà. Se fallisce, come diresti all'utente, comunque?" - scrivendo all'errore standard? Non c'è garanzia che un'incapacità di scrivere su uno implichi l'impossibilità di scrivere all'altro.
Damien_The_Unbeliever,

4
@Damien_The_Unbeliever - soprattutto perché stdoutpuò essere reindirizzato a un file o addirittura non esistere a seconda del sistema. stdoutnon è un flusso di "scrittura su console", di solito è quello, ma non deve esserlo.
Assapora Ždralo il

3
@JAB Invia un messaggio a stdout... ovvio :-)
TripeHound,

7
@JAB: potresti uscire con EX_IOERR o giù di lì, se fosse appropriato.
John Marshall,

6
Qualunque cosa tu faccia, non continuare a correre come se non fosse successo niente! Se il comando dump_database > backups/db_dump.txtnon riesce a scrivere nell'output standard in un dato punto, non vorrei che proseguisse e uscisse con successo. (Non è stato eseguito il backup dei database in questo modo, ma il punto è ancora valido)
Ben S,

15

Tuttavia, quasi tutte le funzioni della libreria C restituiranno 0 o -1 o NULL in caso di errore.

Sì, ma sai quale funzione hai chiamato, vero?

In realtà hai molte informazioni che potresti inserire in un messaggio di errore. Sai quale funzione è stata chiamata, il nome della funzione che l'ha chiamata, quali parametri sono stati passati e il valore restituito della funzione. Sono molte informazioni per un messaggio di errore molto informativo.

Non è necessario farlo per ogni chiamata di funzione. Ma la prima volta che vedi il messaggio di errore "Si è verificato un errore durante la visualizzazione dell'errore precedente", quando ciò di cui avevi veramente bisogno erano informazioni utili, sarà l'ultima volta che vedrai quel messaggio di errore lì, perché cambierai immediatamente il messaggio di errore a qualcosa di informativo che ti aiuterà a risolvere il problema.


5
Questa risposta mi ha fatto sorridere, perché è vero, ma non risponde alla domanda.
RubberDuck,

Come non risponde alla domanda?
Robert Harvey,

2
un motivo penso che C (e altre lingue) abbiano mischiato il booleano 1 e 0 è lo stesso motivo per cui qualsiasi funzione decente che restituisce un codice di errore restituisce "0" per nessun errore. ma poi devi dire if (!myfunc()) {/*proceed*/}. 0 non era un errore e qualsiasi cosa diversa da zero era un qualche errore di tipo e ogni errore di tipo aveva il proprio codice diverso da zero. come diceva un mio amico era "c'è solo una Verità, ma molte falsità". "0" dovrebbe essere "vero" nel if()test e qualsiasi cosa diversa da zero dovrebbe essere "falso".
robert bristow-johnson,

1
no, facendo a modo tuo, c'è solo un possibile codice di errore negativo. e molti possibili codici no_error.
robert bristow-johnson,

1
@ robertbristow-johnson: quindi leggilo come "Non si sono verificati errori". if(function()) { // do something }si legge come "se la funzione viene eseguita senza errori, quindi fare qualcosa". o, ancora meglio, "se la funzione viene eseguita correttamente, fai qualcosa". Seriamente, coloro che comprendono la convenzione non ne sono confusi. Il ritorno false(o 0) quando si verifica un errore non consentirebbe l'utilizzo dei codici di errore. Zero significa falso in C perché è così che funziona la matematica. Vedi programmers.stackexchange.com/q/198284
Robert Harvey il

11

TLDR; non dovresti quasi mai ignorare gli errori.

Nel linguaggio C manca una buona funzionalità di gestione degli errori che consente a ogni sviluppatore di librerie di implementare le proprie soluzioni. Le lingue più moderne hanno eccezioni integrate che rendono questo problema particolare molto più facile da gestire.

Ma quando sei bloccato con C non hai tali vantaggi. Sfortunatamente dovrai semplicemente pagare il prezzo ogni volta che chiami una funzione che presenta una remota possibilità di fallimento. Altrimenti subirai conseguenze molto peggiori come la sovrascrittura involontaria di dati in memoria. Quindi, come regola generale, devi sempre verificare la presenza di errori.

Se non si controlla il ritorno, fprintfè molto probabile che si lasci alle spalle un bug che, nel migliore dei casi, non farà ciò che l'utente si aspetta e nel peggiore dei casi esploderà durante il volo. Non ci sono scuse per indebolirti in quel modo.

Tuttavia, in qualità di sviluppatore C, è anche tuo compito semplificare la manutenzione del codice. Quindi a volte puoi tranquillamente ignorare gli errori per motivi di chiarezza se (e solo se) non rappresentano una minaccia per il comportamento generale dell'applicazione. .

È lo stesso problema di fare questo:

try
{
    run();
} catch (Exception) {
    // comments expected here!!!
}

Se vedi che senza buoni commenti all'interno del blocco catch vuoto questo è sicuramente un problema. Non c'è motivo di pensare che una chiamata malloc()verrà eseguita correttamente il 100% delle volte.


Concordo sul fatto che la manutenibilità sia un aspetto molto importante e che il paragrafo abbia risposto alla mia domanda. Grazie per la risposta dettagliata
Derek 功夫 會 功夫

Penso che i gazillions di programmi che non si preoccupano mai di controllare le loro chiamate malloc e che tuttavia non si arrestano mai, sono un buon motivo per essere pigri quando un programma desktop utilizza solo pochi MB di memoria.
whatsisname

5
@whatsisname: Solo perché non si bloccano non significa che non stanno corrompendo silenziosamente i dati. malloc()è una funzione su cui vuoi sicuramente controllare i valori di ritorno!
TMN,

1
@TMN: Se malloc fallisse, il programma sarebbe immediatamente segfault e terminato, non avrebbe continuato a fare cose. Si tratta di rischio e ciò che è appropriato, non "quasi mai".
whatsisname

2
@Alex C non manca di una buona gestione degli errori. Se vuoi eccezioni, puoi usarle. La libreria standard che non utilizza le eccezioni è una scelta deliberata (le eccezioni ti costringono ad avere una gestione automatizzata della memoria e spesso non la vuoi per codice di basso livello).
martinkunev,

9

La domanda non è in realtà specifica della lingua, ma piuttosto specifica dell'utente. Pensa alla domanda dal punto di vista dell'utente. L'utente fa qualcosa, come digitare il nome del programma su una riga di comando e premere invio. Cosa si aspetta l'utente? Come possono sapere se qualcosa è andato storto? Possono permettersi di intercedere se si verifica un errore?

In molti tipi di codice, tali controlli sono eccessivi. Tuttavia, nel codice critico di sicurezza ad alta affidabilità, come quelli per i reattori nucleari, il controllo degli errori patologici e i percorsi di recupero pianificati fanno parte della natura quotidiana del lavoro. Si ritiene che valga la pena spendere del tempo per chiedere "Cosa succede se X fallisce? Come posso tornare a uno stato sicuro?" In un codice meno affidabile, come quelli per i videogiochi, puoi cavartela con un controllo degli errori molto minore.

Un altro approccio simile è quanto puoi effettivamente migliorare lo stato rilevando l'errore? Non riesco a contare il numero di programmi C ++ che catturano con orgoglio le eccezioni, solo per ricominciarli perché non sapevano davvero cosa farne ... ma sapevano che avrebbero dovuto fare la gestione delle eccezioni. Tali programmi non hanno guadagnato nulla dallo sforzo extra. Aggiungi solo il codice di controllo degli errori che ritieni possa effettivamente gestire la situazione meglio del semplice non controllo del codice di errore. Detto questo, C ++ ha regole specifiche per la gestione delle eccezioni che si verificano durante la gestione delle eccezioni al fine di catturarle in un modo più significativo (e con questo intendo chiamare terminate () per assicurarsi che la pira funeraria che hai costruito per te si illumini nella sua gloria propria)

Quanto patologico puoi ottenere? Ho lavorato su un programma che ha definito un "SafetyBool" i cui valori veri e falsi sono stati scelti con cura per avere una distribuzione uniforme di 1 e 0, e sono stati scelti in modo tale che uno qualsiasi dei vari guasti hardware (capovolgimenti a bit singolo, bus dati le tracce che si rompono, ecc.) non hanno causato un'interpretazione errata di un booleano. Inutile dire che non direi che si tratta di una pratica di programmazione per scopi generali da utilizzare in qualsiasi vecchio programma.


6
  • Diversi requisiti di sicurezza richiedono livelli diversi di correttezza. Nel software di controllo aeronautico o automobilistico verranno controllati tutti i valori di ritorno, cfr. MISRA.FUNC.UNUSEDRET . In una rapida dimostrazione del concetto che non lascia mai la tua macchina, probabilmente no.
  • La codifica costa tempo. Troppi controlli irrilevanti nei software non critici per la sicurezza sono sforzi meglio spesi altrove. Ma dov'è il punto debole tra qualità e costo? Dipende dagli strumenti di debug e dalla complessità del software.
  • La gestione degli errori può oscurare il flusso di controllo e introdurre nuovi errori. Mi piacciono abbastanza le funzioni wrapper di Richard "network" Stevens che almeno riportano errori.
  • La gestione degli errori, raramente, può essere un problema di prestazioni. Ma la maggior parte delle chiamate in libreria C impiegherà così tanto tempo che il costo della verifica di un valore di ritorno è incredibilmente piccolo.

3

Un po 'di una visione astratta della domanda. E non è necessariamente per il linguaggio C.

Per programmi più grandi avresti uno strato di astrazione; forse una parte del motore, una libreria o all'interno di un framework. Questo strato non importa di tempo si ottiene un dato valido o l'uscita sarebbe un valore di default: 0, -1, nulletc.

Poi c'è un livello che sarebbe la tua interfaccia con il livello astratto, che farebbe molta gestione degli errori e forse altre cose come iniezioni di dipendenza, ascolto di eventi ecc.

E in seguito avresti il ​​tuo livello di implementazione concreto in cui imposti le regole e gestisci l'output.

Quindi la mia opinione su questo è che a volte è meglio escludere completamente la gestione degli errori da una parte del codice perché quella parte semplicemente non fa quel lavoro. E poi avere un processore che valuti l'output e indichi un errore.

Questo viene fatto principalmente per separare le responsabilità che portano alla leggibilità del codice e una migliore scalabilità.


0

In generale, a meno che tu non abbia una buona ragione per non controllare quella condizione di errore, dovresti. L'unico buon motivo che mi viene in mente per non verificare la presenza di una condizione di errore è quando non è possibile fare qualcosa di significativo se fallisce. Questa è una barra molto difficile da incontrare, perché c'è sempre un'opzione praticabile: uscire con garbo.


In generale non sarei d'accordo con te in quanto gli errori sono situazioni di cui non hai tenuto conto. È difficile sapere come potrebbe manifestarsi l'errore se non si conosce a quale condizione si è verificato l'errore. E quindi la gestione degli errori prima dell'effettiva elaborazione dei dati.
Creative Magic,

Ad esempio, ho detto, se qualcosa restituisce o genera un errore, anche se non sai come correggere l'errore e continuare, puoi sempre fallire con garbo, registrare l'errore, dire all'utente che non ha funzionato come previsto, ecc. Il punto è che l'errore non dovrebbe passare inosservato, non bloccato, "fallire silenziosamente" o arrestare in modo anomalo l'applicazione. Ecco cosa significa "fallimento con grazia" o "uscita con grazia".
Neologismo intelligente,
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.