La tua domanda afferma che "Scrivere un codice sicuro per le eccezioni è molto difficile". Prima risponderò alle tue domande e poi risponderò alla domanda nascosta dietro di loro.
Rispondendo alle domande
Scrivi davvero un codice sicuro?
Certo che lo so.
Questo è il motivo per cui Java ha perso molto del suo fascino per me come programmatore C ++ (mancanza di semantica RAII), ma sto divagando: questa è una domanda C ++.
In effetti, è necessario quando devi lavorare con il codice STL o Boost. Ad esempio, i thread C ++ ( boost::thread
o std::thread
) genereranno un'eccezione per uscire con grazia.
Sei sicuro che il tuo ultimo codice "pronto per la produzione" sia sicuro per le eccezioni?
Puoi essere sicuro che lo sia?
Scrivere un codice sicuro per le eccezioni è come scrivere un codice privo di bug.
Non puoi essere sicuro al 100% che il tuo codice sia sicuro per le eccezioni. Ma poi, ti sforzi per farlo, usando schemi ben noti ed evitando noti anti-schemi.
Conosci e / o effettivamente usi delle alternative che funzionano?
Non ci sono non valide alternative in C ++ (cioè è necessario ritornare alla C ed evitare librerie C ++, così come le sorprese esterni come Windows SEH).
Scrittura del codice sicuro di eccezione
Per scrivere il codice di sicurezza dell'eccezione, devi prima sapere quale livello di sicurezza dell'eccezione è ogni istruzione che scrivi.
Ad esempio, a new
può generare un'eccezione, ma l'assegnazione di un built-in (ad esempio un int o un puntatore) non fallirà. Uno scambio non fallirà mai (non scrivere mai uno scambio di lancio), un std::list::push_back
lancio può ...
Garanzia d'eccezione
La prima cosa da capire è che devi essere in grado di valutare la garanzia di eccezione offerta da tutte le tue funzioni:
- nessuno : il tuo codice non dovrebbe mai offrirlo. Questo codice perderà tutto e si interromperà alla prima eccezione generata.
- di base : questa è la garanzia che devi offrire per lo meno, cioè se viene generata un'eccezione, nessuna risorsa viene trapelata e tutti gli oggetti sono ancora interi
- strong : l'elaborazione avrà esito positivo o genererà un'eccezione, ma se viene generata, i dati saranno nello stesso stato come se l'elaborazione non fosse stata avviata (ciò conferisce una potenza transazionale a C ++)
- nothrow / nofail : Il trattamento avrà successo.
Esempio di codice
Il seguente codice sembra corretto C ++, ma in verità offre la garanzia "none" e quindi non è corretto:
void doSomething(T & t)
{
if(std::numeric_limits<int>::max() > t.integer) // 1. nothrow/nofail
t.integer += 1 ; // 1'. nothrow/nofail
X * x = new X() ; // 2. basic : can throw with new and X constructor
t.list.push_back(x) ; // 3. strong : can throw
x->doSomethingThatCanThrow() ; // 4. basic : can throw
}
Scrivo tutto il mio codice pensando a questo tipo di analisi.
La garanzia più bassa offerta è di base, ma poi, l'ordinamento di ciascuna istruzione rende l'intera funzione "nessuna", perché se 3. lancia, x perderà.
La prima cosa da fare sarebbe rendere la funzione "di base", ovvero mettere x in un puntatore intelligente fino a quando non è di proprietà dell'elenco in modo sicuro:
void doSomething(T & t)
{
if(std::numeric_limits<int>::max() > t.integer) // 1. nothrow/nofail
t.integer += 1 ; // 1'. nothrow/nofail
std::auto_ptr<X> x(new X()) ; // 2. basic : can throw with new and X constructor
X * px = x.get() ; // 2'. nothrow/nofail
t.list.push_back(px) ; // 3. strong : can throw
x.release() ; // 3'. nothrow/nofail
px->doSomethingThatCanThrow() ; // 4. basic : can throw
}
Ora, il nostro codice offre una garanzia "base". Nulla perderà e tutti gli oggetti saranno nello stato corretto. Ma potremmo offrire di più, cioè la forte garanzia. È qui che può diventare costoso, ed è per questo che non tutto il codice C ++ è forte. Proviamolo:
void doSomething(T & t)
{
// we create "x"
std::auto_ptr<X> x(new X()) ; // 1. basic : can throw with new and X constructor
X * px = x.get() ; // 2. nothrow/nofail
px->doSomethingThatCanThrow() ; // 3. basic : can throw
// we copy the original container to avoid changing it
T t2(t) ; // 4. strong : can throw with T copy-constructor
// we put "x" in the copied container
t2.list.push_back(px) ; // 5. strong : can throw
x.release() ; // 6. nothrow/nofail
if(std::numeric_limits<int>::max() > t2.integer) // 7. nothrow/nofail
t2.integer += 1 ; // 7'. nothrow/nofail
// we swap both containers
t.swap(t2) ; // 8. nothrow/nofail
}
Abbiamo riordinato le operazioni, prima creando e impostando X
il valore corretto. Se qualsiasi operazione fallisce, t
non viene modificata, quindi le operazioni da 1 a 3 possono essere considerate "forti": se qualcosa viene lanciato, t
non viene modificato e X
non perde perché è di proprietà del puntatore intelligente.
Poi, creiamo una copia t2
di t
, e il lavoro su questa copia dal funzionamento da 4 a 7. Se qualcosa getta, t2
viene modificato, ma poi, t
è ancora l'originale. Offriamo ancora la forte garanzia.
Quindi, scambiamo t
e t2
. Le operazioni di swap dovrebbero essere nothrow in C ++, quindi speriamo che lo swap per cui hai scritto non T
sia nothrow (in caso contrario, riscriverlo in modo che non sia nothrow).
Quindi, se raggiungiamo la fine della funzione, tutto è riuscito (non è necessario un tipo restituito) e t
ha il suo valore escluso. Se fallisce, t
ha ancora il suo valore originale.
Ora, offrire la garanzia forte potrebbe essere piuttosto costoso, quindi non sforzarti di offrire la garanzia forte a tutto il tuo codice, ma se riesci a farlo senza un costo (e l'integrazione del C ++ e altre ottimizzazioni potrebbero rendere tutto il codice sopra senza costi) , allora fallo. L'utente della funzione ti ringrazierà per questo.
Conclusione
Ci vuole una certa abitudine per scrivere un codice sicuro per le eccezioni. Dovrai valutare la garanzia offerta da ciascuna istruzione che utilizzerai, quindi dovrai valutare la garanzia offerta da un elenco di istruzioni.
Naturalmente, il compilatore C ++ non eseguirà il backup della garanzia (nel mio codice, offro la garanzia come tag doxygen @warning), il che è un po 'triste, ma non dovrebbe impedirti di provare a scrivere un codice sicuro per le eccezioni.
Errore normale vs. errore
Come può un programmatore garantire che una funzione no-fail abbia sempre successo? Dopotutto, la funzione potrebbe avere un bug.
Questo è vero. Le garanzie di eccezione dovrebbero essere offerte da un codice privo di bug. Ma poi, in qualsiasi lingua, chiamare una funzione suppone che la funzione sia priva di bug. Nessun codice sano si protegge dalla possibilità che abbia un bug. Scrivi il codice meglio che puoi e poi offri la garanzia supponendo che sia privo di bug. E se c'è un bug, correggilo.
Eccezioni sono per errori di elaborazione eccezionali, non per errori di codice.
Ultime parole
Ora, la domanda è "Ne vale la pena?".
Ovviamente è. Avere una funzione "nothrow / no-fail" sapendo che la funzione non fallirà è un grande vantaggio. Lo stesso si può dire per una funzione "forte", che consente di scrivere codice con semantica transazionale, come database, con funzioni di commit / rollback, il commit è la normale esecuzione del codice, generando eccezioni come rollback.
Quindi, la "base" è la minima garanzia che dovresti offrire. Il C ++ è un linguaggio molto forte lì, con i suoi ambiti, che consente di evitare perdite di risorse (qualcosa che un garbage collector troverebbe difficile offrire per il database, la connessione o gli handle di file).
Quindi, per quanto vedo io, si è valsa la pena.
Modifica 29/01/2010: informazioni sullo scambio senza lancio
Nobar ha fatto un commento che credo sia abbastanza pertinente, perché fa parte di "Come si scrive il codice sicuro di eccezione":
- [me] Uno scambio non fallirà mai (non scrivere nemmeno uno scambio di lancio)
- [nobar] Questa è una buona raccomandazione per le
swap()
funzioni scritte su misura . Va notato, tuttavia, che std::swap()
può fallire in base alle operazioni che utilizza internamente
il default std::swap
eseguirà copie e assegnazioni che, per alcuni oggetti, possono essere lanciate. Pertanto, lo swap predefinito potrebbe essere utilizzato, utilizzato per le classi o anche per le classi STL. Per quanto riguarda lo standard C ++, l'operazione di scambio per vector
, deque
e list
non verrà lanciata, mentre potrebbe map
accadere se il funzione di confronto può lanciare sulla costruzione di copie (Vedi The C ++ Programming Language, Special Edition, appendice E, E.4.3 .Swap ).
Guardando l'implementazione di Visual C ++ 2008 dello swap del vettore, lo swap del vettore non verrà lanciato se i due vettori hanno lo stesso allocatore (cioè il caso normale), ma ne faranno delle copie se hanno allocatori diversi. E quindi, suppongo che potrebbe gettare in quest'ultimo caso.
Quindi, il testo originale è ancora valido: non scrivere mai uno scambio di lancio, ma il commento di nobar deve essere ricordato: assicurati che gli oggetti che stai scambiando abbiano uno scambio di non lancio.
Modifica 2011-11-06: articolo interessante
Dave Abrahams , che ci ha fornito le garanzie di base / forti / nothrow , ha descritto in un articolo la sua esperienza sulla sicurezza dell'eccezione STL:
http://www.boost.org/community/exception_safety.html
Guarda il 7 ° punto (Test automatizzati per la sicurezza delle eccezioni), dove si affida ai test delle unità automatizzate per assicurarsi che ogni caso sia testato. Immagino che questa parte sia un'ottima risposta alla domanda dell'autore " Puoi esserne sicuro, vero? ".
Modifica 31/05/2013: commento di dionadar
t.integer += 1;
è senza la garanzia che l'overflow non accadrà NON sicuro, e in effetti può tecnicamente invocare UB! (L'overflow firmato è UB: C ++ 11 5/4 "Se durante la valutazione di un'espressione, il risultato non è definito matematicamente o non è compreso nell'intervallo di valori rappresentabili per il suo tipo, il comportamento non è definito.") Si noti che non firmato i numeri interi non overflow, ma eseguono i loro calcoli in una classe di equivalenza modulo 2 ^ # bit.
Dionadar si riferisce alla seguente riga, che in effetti ha un comportamento indefinito.
t.integer += 1 ; // 1. nothrow/nofail
La soluzione qui è verificare se l'intero è già al suo valore massimo (usando std::numeric_limits<T>::max()
) prima di fare l'aggiunta.
Il mio errore andava nella sezione "Errore normale vs. errore", ovvero un errore. Non invalida il ragionamento e non significa che il codice sicuro per le eccezioni sia inutile perché impossibile da raggiungere. Non puoi proteggerti dallo spegnimento del computer, dai bug del compilatore o persino dai tuoi bug o da altri errori. Non puoi raggiungere la perfezione, ma puoi provare ad avvicinarti il più possibile.
Ho corretto il codice tenendo presente il commento di Dionadar.