Usare assert () in C ++ è una cattiva pratica?


92

Tendo ad aggiungere molte asserzioni al mio codice C ++ per semplificare il debug senza influire sulle prestazioni delle build di rilascio. Ora, assertè una macro C pura progettata senza i meccanismi C ++ in mente.

Il C ++ d'altra parte definisce std::logic_error, che è pensato per essere lanciato nei casi in cui c'è un errore nella logica del programma (da cui il nome). Lanciare un'istanza potrebbe essere solo l'alternativa perfetta e più C ++ a assert.

Il problema è che asserte abortsia terminare il programma immediatamente senza chiamare distruttori, quindi saltare la pulizia, mentre un'eccezione aggiunge manualmente i costi di esecuzione non necessari. Un modo per aggirare questo sarebbe creare una propria macro di asserzione SAFE_ASSERT, che funziona proprio come la controparte C, ma genera un'eccezione in caso di errore.

Posso pensare a tre opinioni su questo problema:

  • Attenersi all'asserzione di C. Poiché il programma viene terminato immediatamente, non importa se le modifiche vengono srotolate correttamente. Inoltre, usare #defines in C ++ è altrettanto dannoso.
  • Lancia un'eccezione e catturala in main () . Consentire al codice di saltare i distruttori in qualsiasi stato del programma è una cattiva pratica e deve essere evitato a tutti i costi, così come le chiamate a terminate (). Se vengono generate eccezioni, devono essere rilevate.
  • Lancia un'eccezione e lascia che termini il programma. Un'eccezione che interrompe un programma va bene e, per NDEBUGquesto motivo, ciò non accadrà mai in una build di rilascio. La cattura non è necessaria ed espone i dettagli di implementazione del codice interno a main().

C'è una risposta definitiva a questo problema? Qualche riferimento professionale?

Modificato: saltare i distruttori, ovviamente, non è un comportamento indefinito.


22
No, davvero, logic_errorè l'errore logico. Un errore nella logica del programma è chiamato bug. Non risolvi i bug generando eccezioni.
R. Martinho Fernandes

4
Asserzioni, eccezioni, codici di errore. Ognuno ha un caso d'uso completamente distinto e non dovresti usarne uno dove è necessario un altro.
Kerrek SB

5
Assicurati di usare static_assertdove è appropriato se lo hai a disposizione.
Flexo

4
@trion Non vedo come questo aiuti. Lanciaeresti std::bug?
R. Martinho Fernandes

3
@trion: non farlo. Le eccezioni non riguardano il debug. Qualcuno potrebbe rilevare l'eccezione. Non è necessario preoccuparsi di UB durante la chiamata std::abort(); solleverà solo un segnale che fa terminare il processo.
Kerrek SB

Risposte:


73

Le asserzioni sono del tutto appropriate nel codice C ++. Le eccezioni e altri meccanismi di gestione degli errori non sono realmente intesi per la stessa cosa delle asserzioni.

La gestione degli errori è per quando esiste la possibilità di ripristinare o segnalare un errore correttamente all'utente. Ad esempio, se si verifica un errore nel tentativo di leggere un file di input, potresti voler fare qualcosa al riguardo. Gli errori potrebbero derivare da bug, ma potrebbero anche essere semplicemente l'output appropriato per un dato input.

Le asserzioni sono per cose come il controllo che i requisiti di un'API siano soddisfatti quando l'API non sarebbe normalmente controllata, o per controllare cose che lo sviluppatore ritiene di essere garantite dalla costruzione. Ad esempio, se un algoritmo richiede un input ordinato, normalmente non lo verifichi, ma potresti avere un'asserzione per controllarlo in modo che le build di debug contrassegnino quel tipo di bug. Un'asserzione dovrebbe sempre indicare un programma che funziona in modo errato.


Se stai scrivendo un programma in cui un arresto impuro potrebbe causare un problema, potresti voler evitare asserzioni. Un comportamento indefinito strettamente in termini di linguaggio C ++ non si qualifica come un problema del genere qui, poiché colpire un'asserzione è probabilmente già il risultato di un comportamento indefinito, o la violazione di qualche altro requisito che potrebbe impedire il corretto funzionamento di una pulizia.

Inoltre, se si implementano le asserzioni in termini di eccezione, queste potrebbero essere potenzialmente catturate e "gestite" anche se ciò contraddice lo scopo stesso dell'asserzione.


1
Non sono del tutto sicuro che questo sia stato dichiarato specificamente nella risposta, quindi lo dichiaro qui: non dovresti usare un'asserzione per qualcosa che coinvolge l'input dell'utente che non può essere determinato al momento della scrittura del codice. Se un utente passa 3invece che 1al tuo codice, in generale non dovrebbe attivare un'asserzione. Le asserzioni sono solo un errore del programmatore, non un errore dell'utente della libreria o dell'applicazione.
SS Anne

101
  • Le asserzioni servono per il debug . L'utente del codice spedito non dovrebbe mai vederli. Se viene soddisfatta un'asserzione, il codice deve essere corretto.

    CWE-617: asserzione raggiungibile

Il prodotto contiene un'istruzione assert () o simile che può essere attivata da un utente malintenzionato, che porta all'uscita dall'applicazione o ad altri comportamenti più gravi del necessario.

Sebbene l'asserzione sia utile per rilevare errori logici e ridurre le possibilità di raggiungere condizioni di vulnerabilità più gravi, può comunque portare a una negazione del servizio.

Ad esempio, se un server gestisce più connessioni simultanee e una assert () si verifica in una singola connessione che causa la caduta di tutte le altre connessioni, questa è un'asserzione raggiungibile che porta a una negazione del servizio.

  • Le eccezioni sono per circostanze eccezionali . Se ne incontra uno, l'utente non sarà in grado di fare ciò che vuole, ma potrebbe essere in grado di riprendere da qualche altra parte.

  • La gestione degli errori è per il normale flusso del programma. Ad esempio, se richiedi un numero all'utente e ottieni qualcosa di non analizzabile, è normale , perché l'input dell'utente non è sotto il tuo controllo e devi sempre gestire tutte le possibili situazioni come una cosa ovvia . (Es. Ciclo finché non si dispone di un input valido, dicendo "Mi dispiace, riprova" nel mezzo.)


1
è venuto a cercare questa affermazione; qualsiasi forma di asserzione che passa attraverso il codice di produzione indica una progettazione e un controllo di qualità scadenti. Il punto in cui viene chiamata un'asserzione è dove dovrebbe essere la gestione corretta di una condizione di errore. (Non uso mai assert). Per quanto riguarda le eccezioni, l' unico caso d'uso che conosco è quando ctor potrebbe fallire, tutti gli altri sono per la normale gestione degli errori.
slashmais

5
@slashmais: Il sentimento è lodevole, ma a meno che tu non stia inviando un codice perfetto e privo di bug, trovo un'affermazione (anche quella che blocca l'utente) preferibile a un comportamento indefinito. I bug si verificano in sistemi complessi e con un'affermazione si ha un modo di vederli e diagnosticare dove si verificano.
Kerrek SB

@ KerrekSB preferirei usare un'eccezione su un'asserzione. Almeno il codice ha la possibilità di scartare il ramo guasto e fare qualcos'altro di utile. Per lo meno, se stai usando RAII, tutti i tuoi buffer per aprire i file verranno svuotati correttamente.
daemonspring

14

Le asserzioni possono essere utilizzate per verificare invarianti di implementazione interna, come lo stato interno prima o dopo l'esecuzione di un metodo, ecc. Se l'asserzione fallisce, significa che la logica del programma è rotta e non è possibile ripristinarla. In questo caso il meglio che puoi fare è interrompere il prima possibile senza passare eccezioni all'utente. Ciò che è veramente bello delle asserzioni (almeno su Linux) è che il core dump viene generato come risultato della terminazione del processo e quindi puoi facilmente esaminare la traccia dello stack e le variabili. Questo è molto più utile per comprendere l'errore logico rispetto al messaggio di eccezione.


Ho un approccio simile. Uso asserzioni per la logica che dovrebbero essere probabilmente corrette localmente (ad esempio invarianti di ciclo). Le eccezioni riguardano il caso in cui un errore logico è stato imposto al codice da una situazione non locale (esterna).
spruzzo

Se un'asserzione fallisce, significa che la logica di una parte del programma è rotta. Un'asserzione fallita non implica necessariamente che nulla possa essere realizzato. Un plugin rotto probabilmente non dovrebbe interrompere un intero word processor.
daemonspring

13

Non eseguire i distruttori a causa dell'alling abort () non è un comportamento indefinito!

Se lo fosse, allora sarebbe anche un comportamento indefinito da chiamare std::terminate(), e quindi che senso avrebbe fornirlo?

assert() è altrettanto utile in C ++ come in C. Le asserzioni non servono per la gestione degli errori, ma per interrompere immediatamente il programma.


1
Direi che abort()è per interrompere immediatamente il programma. Hai ragione sul fatto che le asserzioni non servono per la gestione degli errori, tuttavia assert cerca di gestire l'errore interrompendo. Non dovresti invece lanciare un'eccezione e lasciare che il chiamante gestisca l'errore se può? Dopo tutto, il chiamante è in una posizione migliore per determinare se il fallimento di una funzione non fa valere la pena fare nient'altro. Forse il chiamante sta tentando di eseguire tre operazioni non correlate e potrebbe comunque completare gli altri due lavori e scartare semplicemente questo.
daemonspring

Ed assertè definito per chiamare abort(quando la condizione è falsa). Per quanto riguarda il lancio di eccezioni, no, non è sempre appropriato. Alcune cose non possono essere gestite dal chiamante. Il chiamante non può determinare se un bug logico in una funzione di libreria di terze parti è recuperabile o se i dati danneggiati possono essere corretti.
Jonathan Wakely

6

IMHO, le affermazioni servono per verificare le condizioni che, se violate, rendono tutto il resto senza senso. E quindi non puoi riprenderti da loro o meglio, il recupero è irrilevante.

Li raggrupperei in 2 categorie:

  • Peccati dello sviluppatore (ad esempio una funzione di probabilità che restituisce valori negativi):

probabilità in virgola mobile () {ritorno -1,0; }

asserire (probabilità ()> = 0,0)

  • La macchina è guasta (ad esempio la macchina che esegue il programma è molto sbagliata):

int x = 1;

asserire (x> 0);

Questi sono entrambi esempi banali ma non troppo lontani dalla realtà. Ad esempio, pensa agli algoritmi ingenui che restituiscono indici negativi per l'utilizzo con i vettori. O programmi integrati in hardware personalizzato. O meglio perché succede una merda .

E se si verificano tali errori di sviluppo, non dovresti essere sicuro di alcun meccanismo di ripristino o di gestione degli errori implementato. Lo stesso vale per gli errori hardware.


1
assert (probabilità ()> = 0,0)
Elliott
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.