Il C ++ sembra preferire l'uso delle eccezioni più spesso.
Vorrei suggerire in realtà meno di Objective-C per alcuni aspetti perché la libreria standard C ++ non genererebbe generalmente errori del programmatore come l'accesso fuori limite di una sequenza ad accesso casuale nella sua forma di progettazione del caso più comune (in operator[]
, cioè) o cercando di dereferenziare un iteratore non valido. Il linguaggio non si basa sull'accesso a un array fuori limite, sulla dereferenziazione di un puntatore nullo o su qualsiasi cosa di questo tipo.
Prendere gli errori del programmatore in gran parte fuori dall'equazione di gestione delle eccezioni in realtà elimina una categoria molto ampia di errori a cui spesso rispondono altre lingue throwing
. Il C ++ tende a assert
(che non viene compilato in build di rilascio / produzione, solo build di debug) o semplicemente in errore in questi casi, probabilmente in parte perché il linguaggio non vuole imporre il costo di tali controlli di runtime come sarebbe necessario per rilevare tali errori del programmatore, a meno che il programmatore non voglia specificamente pagare i costi scrivendo il codice che esegue tali controlli da solo.
Sutter incoraggia persino a evitare eccezioni in tali casi negli standard di codifica C ++:
Lo svantaggio principale dell'utilizzo di un'eccezione per segnalare un errore di programmazione è che non si desidera realmente che si verifichi lo svolgersi dello stack quando si desidera che il debugger si avvii sulla riga esatta in cui è stata rilevata la violazione, con lo stato della riga intatto. In breve: ci sono errori che sai che potrebbero accadere (vedi articoli da 69 a 75). Per tutto ciò che non dovrebbe, ed è colpa del programmatore se lo fa, c'è assert
.
Quella regola non è necessariamente fissata nella pietra. In alcuni casi più critici, potrebbe essere preferibile usare, diciamo, wrapper e uno standard di codifica che registri in modo uniforme dove si verificano errori del programmatore e throw
in presenza di errori del programmatore come cercare di deferire qualcosa di non valido o accedervi senza limiti, perché potrebbe essere troppo costoso non riuscire a recuperare in quei casi se il software ha una possibilità. Ma nel complesso, l'uso più comune della lingua tende a non gettare contro gli errori del programmatore.
Eccezioni esterne
Dove vedo le eccezioni incoraggiate il più delle volte in C ++ (secondo il comitato standard, ad esempio) è per "eccezioni esterne", come in un risultato inaspettato in qualche fonte esterna al di fuori del programma. Un esempio non riesce a allocare memoria. Un altro non riesce ad aprire un file critico necessario per l'esecuzione del software. Un altro non riesce a connettersi a un server richiesto. Un altro è un utente che blocca un pulsante di interruzione per annullare un'operazione il cui percorso di esecuzione del caso comune prevede di riuscire senza questa interruzione esterna. Tutte queste cose sono al di fuori del controllo del software immediato e dei programmatori che lo hanno scritto. Sono risultati inaspettati da fonti esterne che impediscono all'operazione (che dovrebbe essere considerata una transazione indivisibile nel mio libro *) di riuscire.
Le transazioni
Incoraggio spesso a considerare un try
blocco come una "transazione" perché le transazioni dovrebbero avere successo nel suo insieme o fallire nel suo insieme. Se stiamo provando a fare qualcosa e fallisce a metà strada, allora eventuali effetti collaterali / mutazioni apportati allo stato del programma generalmente devono essere ripristinati per riportare il sistema in uno stato valido come se la transazione non fosse mai stata eseguita affatto, proprio come un RDBMS che non riesce a elaborare una query a metà strada non dovrebbe compromettere l'integrità del database. Se si modifica lo stato del programma direttamente in detta transazione, è necessario "ripristinarlo" al verificarsi di un errore (e qui le protezioni dell'ambito possono essere utili con RAII).
L'alternativa molto più semplice è non mutare lo stato del programma originale; potresti mutare una copia di esso e quindi, se riesce, scambiare la copia con l'originale (assicurandoti che lo scambio non possa essere lanciato). In caso contrario, scartare la copia. Ciò vale anche se non si utilizzano eccezioni per la gestione degli errori in generale. Una mentalità "transazionale" è la chiave per il corretto recupero se si sono verificate mutazioni dello stato del programma prima di riscontrare un errore. O riesce nel suo insieme o fallisce nel suo insieme. A metà strada non riesce a fare le sue mutazioni.
Questo è stranamente uno degli argomenti meno frequentemente discussi quando vedo i programmatori che chiedono come fare correttamente la gestione degli errori o delle eccezioni, ma è il più difficile di tutti ottenere correttamente in qualsiasi software che vuole mutare direttamente lo stato del programma in molti di le sue operazioni. Purezza e immutabilità possono aiutare qui a raggiungere la sicurezza delle eccezioni tanto quanto aiutano con la sicurezza del filo, poiché non è necessario ripristinare un effetto di mutazione / effetto collaterale esterno che non si verifica.
Prestazione
Un altro fattore guida nell'usare o meno le eccezioni è la prestazione, e non intendo in alcun modo ossessivo, schiacciante e controproducente. Molti compilatori C ++ implementano la cosiddetta "gestione delle eccezioni a costo zero".
Offre un overhead di runtime zero per un'esecuzione senza errori, che supera anche quella della gestione degli errori del valore di ritorno C. Come compromesso, la propagazione di un'eccezione ha un grande sovraccarico.
Secondo quanto ho letto al riguardo, i percorsi di esecuzione del tuo caso comune non richiedono alcun sovraccarico (nemmeno l'overhead che normalmente accompagna la gestione e la propagazione del codice di errore in stile C), in cambio di una pesante inclinazione dei costi verso percorsi eccezionali ( il che significa che throwing
ora è più costoso che mai).
"Caro" è un po 'difficile da quantificare ma, per cominciare, probabilmente non vorrai lanciarti un milione di volte in un circuito ristretto. Questo tipo di progettazione presuppone che non si verifichino eccezioni sempre a destra ea sinistra.
Non Errori
E quel punto prestazionale mi porta a non errori, il che è sorprendentemente sfocato se guardiamo ogni altra lingua. Ma direi, dato il design EH a costo zero di cui sopra, che quasi sicuramente non vuoi throw
in risposta a una chiave che non viene trovata in un set. Perché non solo è probabilmente un non errore (la persona che cerca la chiave potrebbe aver creato il set e aspettarsi di cercare chiavi che non sempre esistono), ma sarebbe enormemente costoso in quel contesto.
Ad esempio, una funzione di intersezione di insiemi potrebbe voler scorrere due insiemi e cercare le chiavi che hanno in comune. Se non threw
riesci a trovare una chiave , eseguiresti un ciclo continuo e potresti riscontrare eccezioni nella metà o più delle iterazioni:
Set<int> set_intersection(const Set<int>& a, const Set<int>& b)
{
Set<int> intersection;
for (int key: a)
{
try
{
b.find(key);
intersection.insert(other_key);
}
catch (const KeyNotFoundException&)
{
// Do nothing.
}
}
return intersection;
}
Quell'esempio sopra è assolutamente ridicolo ed esagerato, ma ho visto, nel codice di produzione, alcune persone che provengono da altre lingue usando le eccezioni in C ++ in qualche modo come questo, e penso che sia un'affermazione ragionevolmente pratica che questo non è un uso appropriato delle eccezioni di sorta in C ++. Un altro suggerimento sopra è che noterai che il catch
blocco non ha assolutamente nulla da fare ed è solo scritto per ignorare forzatamente tali eccezioni, e di solito è un suggerimento (anche se non un garante) che le eccezioni probabilmente non vengono utilizzate in modo molto appropriato in C ++.
Per questi tipi di casi, un tipo di valore restituito che indica un errore (qualsiasi cosa, dal ritorno false
a un iteratore non valido nullptr
o qualsiasi cosa abbia senso nel contesto) è di solito molto più appropriato, e spesso anche più pratico e produttivo poiché un tipo non di errore di case di solito non richiede un processo di svolgimento dello stack per raggiungere il catch
sito analogico .
Domande
Dovrei andare con flag di errore interni se scelgo di evitare eccezioni. Sarà troppo fastidioso da gestire o forse funzionerà anche meglio delle eccezioni? Un confronto di entrambi i casi sarebbe la risposta migliore.
Evitare le eccezioni in C ++ mi sembra estremamente controproducente, a meno che tu non stia lavorando in un sistema incorporato o in un particolare tipo di caso che ne proibisce l'uso (nel qual caso dovresti anche fare di tutto per evitare tutto funzionalità di libreria e linguaggio che altrimenti throw
, come usare rigorosamente nothrow
new
).
Se devi assolutamente evitare eccezioni per qualsiasi motivo (es: lavorare attraverso i limiti dell'API C di un modulo di cui esporti l'API C), molti potrebbero non essere d'accordo con me, ma in realtà suggerirei di utilizzare un gestore / stato di errore globale come OpenGL con glGetError()
. È possibile utilizzarlo per l'archiviazione locale di thread per avere uno stato di errore univoco per thread.
La mia logica è che non sono abituato a vedere i team negli ambienti di produzione controllare accuratamente tutti i possibili errori, purtroppo quando vengono restituiti i codici di errore. Se fossero approfonditi, alcune API C potrebbero riscontrare un errore in quasi ogni singola chiamata API C e un controllo approfondito richiederebbe qualcosa di simile:
if ((err = ApiCall(...)) != success)
{
// Handle error
}
... con quasi ogni singola riga di codice che richiama l'API che richiede tali controlli. Eppure non ho avuto la fortuna di lavorare con team così approfonditi. Spesso ignorano tali errori metà, a volte anche la maggior parte delle volte. Questo è il più grande appello per me alle eccezioni. Se avvolgiamo questa API e la rendiamo uniformemente throw
al verificarsi di un errore, l'eccezione non può assolutamente essere ignorata e, a mio avviso, ed esperienza, è qui che risiede la superiorità delle eccezioni.
Ma se non è possibile utilizzare le eccezioni, lo stato di errore globale per thread ha almeno il vantaggio (enorme rispetto a me restituire i codici di errore) che potrebbe avere la possibilità di rilevare un errore precedente un po 'più tardi rispetto a quando si è verificato in una base di codice sciatta invece di perdere completamente e lasciarci completamente ignari di ciò che è accaduto. L'errore potrebbe essersi verificato alcune righe prima o in una precedente chiamata di funzione, ma a condizione che il software non si sia ancora arrestato in modo anomalo, potremmo essere in grado di iniziare a lavorare indietro e capire dove e perché si è verificato.
Mi sembra che, dato che i puntatori sono rari, dovrei scegliere dei flag di errore interni se scelgo di evitare le eccezioni.
Non direi necessariamente che i puntatori sono rari. Esistono persino metodi ora in C ++ 11 e versioni successive per ottenere i puntatori di dati sottostanti dei contenitori e una nuova nullptr
parola chiave. In genere non è saggio utilizzare i puntatori non elaborati per possedere / gestire la memoria se è possibile utilizzare qualcosa di simile unique_ptr
invece dato quanto sia fondamentale essere conformi alla RAII in presenza di eccezioni. Ma i puntatori grezzi che non possiedono / gestiscono la memoria non sono necessariamente considerati così male (anche da persone come Sutter e Stroustrup) e talvolta molto pratici come un modo per indicare le cose (insieme agli indici che indicano le cose).
Probabilmente non sono meno sicuri degli iteratori di container standard (almeno in versione, iteratori assenti controllati) che non rileveranno se si tenta di dereferenziarli dopo che sono stati invalidati. Il C ++ è ancora senza vergogna un po 'di un linguaggio pericoloso, direi, a meno che il tuo uso specifico di esso non voglia racimolare tutto e nascondere anche puntatori grezzi non proprietari. È quasi fondamentale, con l'eccezione, che le risorse siano conformi a RAII (che generalmente non comporta costi di runtime), ma a parte questo non è necessariamente il linguaggio più sicuro da utilizzare a favore dei costi che uno sviluppatore non desidera esplicitamente scambiare qualcos'altro. L'uso raccomandato non sta cercando di proteggerti da cose come puntatori penzolanti e iteratori invalidati, per così dire (altrimenti saremmo incoraggiati a usareshared_ptr
in tutto il luogo, a cui Stroustrup si oppone con veemenza). Sta cercando di proteggerti dal non riuscire a liberare / rilasciare / distruggere / sbloccare / ripulire correttamente una risorsa quando qualcosa throws
.