Try-catch o ifs per la gestione degli errori in C ++


30

Le eccezioni sono ampiamente utilizzate nella progettazione del motore di gioco o è più preferibile usare istruzioni if ​​pure? Ad esempio con eccezioni:

try {
    m_fpsTextId = m_statistics->createText( "FPS: 0", 16, 20, 20, 1.0f, 1.0f, 1.0f );
    m_cpuTextId = m_statistics->createText( "CPU: 0%", 16, 20, 40, 1.0f, 1.0f, 1.0f );
    m_frameTimeTextId = m_statistics->createText( "Frame time: 0", 20, 20, 60, 1.0f, 1.0f, 1.0f );
    m_mouseCoordTextId = m_statistics->createText( "Mouse: (0, 0)", 20, 20, 80, 1.0f, 1.0f, 1.0f );
    m_cameraPosTextId = m_statistics->createText( "Camera pos: (0, 0, 0)", 40, 20, 100, 1.0f, 1.0f, 1.0f );
    m_cameraRotTextId = m_statistics->createText( "Camera rot: (0, 0, 0)", 40, 20, 120, 1.0f, 1.0f, 1.0f );
} catch ... {
    // some info about error
}

e con if:

m_fpsTextId = m_statistics->createText( "FPS: 0", 16, 20, 20, 1.0f, 1.0f, 1.0f );
if( m_fpsTextId == -1 ) {
    // show error
}
m_cpuTextId = m_statistics->createText( "CPU: 0%", 16, 20, 40, 1.0f, 1.0f, 1.0f );
if( m_cpuTextId == -1 ) {
    // show error
}
m_frameTimeTextId = m_statistics->createText( "Frame time: 0", 20, 20, 60, 1.0f, 1.0f, 1.0f );
if( m_frameTimeTextId == -1 ) {
    // show error
}
m_mouseCoordTextId = m_statistics->createText( "Mouse: (0, 0)", 20, 20, 80, 1.0f, 1.0f, 1.0f );
if( m_mouseCoordTextId == -1 ) {
    // show error
}
m_cameraPosTextId = m_statistics->createText( "Camera pos: (0, 0, 0)", 40, 20, 100, 1.0f, 1.0f, 1.0f );
if( m_cameraPosTextId == -1 ) {
    // show error
}
m_cameraRotTextId = m_statistics->createText( "Camera rot: (0, 0, 0)", 40, 20, 120, 1.0f, 1.0f, 1.0f );
if( m_cameraRotTextId == -1 ) {
    // show error
}

Ho sentito che le eccezioni sono un po 'più lente degli if e non dovrei mescolare le eccezioni con il metodo if-check. Tuttavia, con le eccezioni, ho un codice più leggibile che con tonnellate di if dopo ogni metodo di inizializzazione () o qualcosa di simile, anche se a volte sono troppo pesanti per un solo metodo secondo me. Sono ok per lo sviluppo di giochi o meglio attenersi a semplici if?


Molte persone sostengono che Boost è dannoso per lo sviluppo del gioco, ma ti consiglio di guardare ["Boost.optional"] ( boost.org/doc/libs/1_52_0/libs/optional/doc/html/index.html ) farà il tuo esempio ifs è un po 'più bello
ManicQin

C'è un bel discorso sulla gestione degli errori di Andrei Alexandrescu, ho usato un approccio simile al suo senza eccezioni.
Thelvyn,

Risposte:


48

Risposta breve: leggi Gestione errori sensibili 1 , Gestione errori sensibili 2 e Gestione errori sensibili 3 di Niklas Frykholm. In realtà, leggi tutti gli articoli su quel blog, mentre ci sei. Non dirò che sono d'accordo con tutto, ma la maggior parte è oro.

Non usare eccezioni. Ci sono molte ragioni. Elencherò quelli principali.

Possono effettivamente essere più lenti, sebbene ciò sia minimizzato un po 'sui compilatori più recenti. Alcuni compilatori supportano "zero eccezioni generali" per i percorsi di codice che in realtà non attivano l'eccezione (anche se questo è un po 'una bugia, poiché ci sono ancora dati extra che la gestione delle eccezioni richiede, gonfiando le dimensioni del file eseguibile / dll). Il risultato finale è che sì, l'utilizzo delle eccezioni è più lento e in qualsiasi percorso di codice critico per le prestazioni , è necessario evitarle assolutamente. A seconda del compilatore, averli abilitati a tutti può aggiungere sovraccarico. Gonfiano sempre sempre le dimensioni del codice, in modo piuttosto significativo in molti casi, il che può influire negativamente sulle prestazioni dell'hardware di oggi.

Le eccezioni rendono il codice molto più fragile. C'è un famigerato grafico (che purtroppo non riesco a trovare in questo momento) che sostanzialmente mostra su un grafico la difficoltà di scrivere un codice sicuro contro le eccezioni rispetto a un codice non sicuro, e il primo è una barra significativamente più grande. Ci sono semplicemente molti piccoli trucchi con eccezioni e molti modi per scrivere codice che sembra eccezionalmente sicuro ma in realtà non lo è. Anche l'intero comitato C ++ 11 si è preso gioco di questo e ha dimenticato di aggiungere un'importante funzione di supporto per l'uso corretto di std :: unique_ptr, e anche con quelle funzioni di supporto, ci vuole più battitura per usarli che non a e la maggior parte dei programmatori ha vinto ' capisco anche cosa c'è che non va se non lo fanno.

Più specificamente per l'industria dei giochi, alcuni compilatori / runtime forniti da alcuni distributori non supportano completamente le eccezioni o addirittura non le supportano affatto. Se il tuo codice utilizza eccezioni, potresti essere ancora al giorno d'oggi costretto a riscrivere parti del codice per portarlo su nuove piattaforme. (Non sono sicuro che sia cambiato nei 7 anni da quando le console sono state rilasciate; semplicemente non usiamo eccezioni, le abbiamo disabilitate anche nelle impostazioni del compilatore, quindi non so che qualcuno con cui parlo abbia controllato anche di recente.)

La linea di pensiero generale è abbastanza chiara: usare le eccezioni per circostanze eccezionali . Usali quando il tuo programma entra in "Non ho idea di cosa fare, forse spero che lo faccia qualcun altro, quindi lancerò un'eccezione e vedrò cosa succede." Usali quando nessun'altra opzione ha senso. Usali quando non ti interessa se accidentalmente perdi un po 'di memoria o non riesci a ripulire una risorsa perché hai incasinato l'uso di appositi handle intelligenti. In tutti gli altri casi, non usarli.

Per quanto riguarda il codice come il tuo esempio, hai molti altri modi per risolvere il problema. Uno dei più robusti, anche se non necessariamente il più ideale nel tuo semplice esempio, è quello di inclinarsi verso tipi di errore monadici. Cioè, createText () può restituire un tipo di handle personalizzato anziché un numero intero. Questo tipo di handle dispone di accessori per l'aggiornamento o il controllo del testo. Se l'handle viene messo in uno stato di errore (perché createText () non è riuscito), le ulteriori chiamate all'handle semplicemente falliscono silenziosamente. Puoi anche interrogare l'handle per vedere se ha commesso un errore e, in tal caso, quale è stato l'ultimo errore. Questo approccio ha un overhead maggiore rispetto ad altre opzioni, ma è abbastanza solido. Usalo nei casi in cui è necessario eseguire una lunga serie di operazioni in alcuni contesti in cui una singola operazione potrebbe non riuscire nella produzione, ma in cui non è possibile / impossibile / vinto "

Un'alternativa all'implementazione della gestione degli errori monadici è, piuttosto che utilizzare oggetti handle personalizzati, fare in modo che i metodi sull'oggetto contesto gestiscano con grazia ID handle non validi. Ad esempio, se createText () restituisce -1 quando fallisce, allora qualsiasi altra chiamata a m_statistics che prende uno di quegli handle dovrebbe uscire con grazia se -1 viene passato.

Allo stesso modo è possibile inserire l'errore di stampa all'interno della funzione che in realtà non funziona. Nel tuo esempio, createText () probabilmente ha molte più informazioni su cosa è andato storto, quindi sarà in grado di scaricare un errore più significativo nel registro. Ci sono pochi vantaggi in questo caso nel trasmettere l'errore di gestione / stampa ai chiamanti. Fallo quando i chiamanti hanno bisogno di personalizzare la gestione (o usare l'iniezione delle dipendenze). Tieni presente che avere una console di gioco che può apparire ogni volta che viene registrato un errore è una buona idea e aiuta anche qui.

L'opzione migliore (già presentata nella serie di articoli collegata sopra) per le chiamate che non ti aspetti di fallire in qualsiasi ambiente sano - come il semplice atto di creare BLOB di testo per un sistema statistico - è semplicemente avere la funzione fallito (createText nel tuo esempio). Puoi essere ragionevolmente sicuro che createText () non fallirà nella produzione a meno che qualcosa non sia stato completamente eliminato (ad esempio, l'utente ha eliminato i file di dati dei caratteri o per qualche motivo ha solo 256 MB di memoria, ecc.). In molti di questi casi, non c'è nemmeno una buona cosa da fare quando si verifica un fallimento. Fuori dalla memoria? Potresti anche non essere in grado di effettuare un'allocazione necessaria per creare un bel pannello della GUI per mostrare all'utente l'errore OOM. Caratteri mancanti? Rende difficile visualizzare gli errori per l'utente. Qualunque cosa non vada,

Il solo arresto anomalo è del tutto corretto fintanto che (a) si registra l'errore in un file di registro e (b) lo si fa solo su errori che non sono causati dalle normali azioni dell'utente.

Non direi la stessa cosa per molte app server, in cui la disponibilità è fondamentale e il monitoraggio del watchdog non è sufficiente, ma è abbastanza diverso dallo sviluppo del client di gioco . Vorrei anche evitare di usare C / C ++ in quanto le strutture di gestione delle eccezioni di altri linguaggi tendono a non mordere come quelle del C ++ poiché sono ambienti gestiti e non hanno tutti i problemi di sicurezza delle eccezioni che il C ++ ha. Qualsiasi problema relativo alle prestazioni viene inoltre mitigato poiché i server tendono a concentrarsi maggiormente sul parallelismo e sulla velocità effettiva rispetto alle garanzie di latenza minima come i client di gioco. Anche i server di giochi d'azione per sparatutto e simili possono funzionare abbastanza bene se scritti in C #, ad esempio, dal momento che raramente spingono l'hardware ai suoi limiti come tendono a fare i client FPS.


1
+1. Inoltre c'è la possibilità di aggiungere attributi come warn_unused_resultper funzionare in alcuni compilatori che consente di catturare il codice di errore non gestito.
Maciej Piechotka,

È venuto qui vedendo C ++ nel titolo ed era assolutamente sicuro che la gestione degli errori monadici non sarebbe già qui. Roba buona!
Adam,

che dire dei luoghi in cui le prestazioni non sono fondamentali e si punta principalmente a un'implementazione rapida? ad esempio quando stai implementando la logica di gioco?
Ali1S232,

e che dire dell'idea di utilizzare la gestione delle eccezioni solo a scopo di debug? come quando rilasci il gioco, ti aspetti che tutto vada per il meglio senza problemi. quindi usi le eccezioni per trovare e correggere i bug durante lo sviluppo e successivamente in modalità di rilascio rimuovi tutte quelle eccezioni.
Ali1S232,

1
@Gajoo: per la logica di gioco, le eccezioni rendono la logica più difficile da seguire (poiché rende tutto il codice più difficile da seguire). Anche in Pythnn e C # le eccezioni sono rare per la logica di gioco, nella mia esperienza. per il debugging, l'hard assert è in genere molto più conveniente. Interrompi e interrompi nel momento esatto in cui qualcosa va storto, non dopo aver srotolato grandi porzioni dello stack e perso tutti i tipi di informazioni contestuali a causa della semantica generata dall'eccezione. Per la logica di debug, si desidera creare strumenti e informazioni / modificare le GUI, in modo che i progettisti possano ispezionare e modificare.
Sean Middleditch il

13

La vecchia saggezza di Donald Knuth stesso è:

"Dovremmo dimenticare le piccole inefficienze, diciamo circa il 97% delle volte: l'ottimizzazione prematura è la radice di tutti i mali".

Stampa questo come un grande poster e appendilo ovunque tu faccia una programmazione seria.

Quindi, anche se try / catch è un po 'più lento, lo userei per impostazione predefinita:

  • Il codice deve essere il più leggibile e comprensibile possibile. Si potrebbe scrivere il codice una volta, ma si avrà letto molte volte più per il debug di questo codice, per il debug altro codice, per capire, per il miglioramento, ...

  • Correttezza rispetto alle prestazioni. Fare le cose if / else correttamente non è banale. Nel tuo esempio viene eseguito in modo errato, perché non è possibile mostrare un errore ma più multipli. Devi usare in cascata if / then / else.

  • Più facile da usare in modo coerente: Try / catch abbraccia uno stile in cui non è necessario verificare la presenza di errori in ogni riga. D'altra parte: uno mancante if / else e il tuo codice potrebbe rovinare il caos. Certamente solo in circostanze irriproducibili.

Quindi l'ultimo punto:

Ho sentito che le eccezioni sono un po 'più lente degli if

L'ho sentito circa 15 anni fa, non l'ho sentito da nessuna fonte credibile di recente. I compilatori potrebbero essere migliorati o altro.

Il punto principale è: non eseguire l'ottimizzazione prematura. Fallo solo quando puoi provare per benchmark che il codice a portata di mano è il circuito interno ristretto di un codice molto usato e che lo stile di commutazione migliora notevolmente le prestazioni . Questo è il caso del 3%.


22
Lo farò -1 all'infinito. Non riesco a capire il danno che questa citazione di Knuth ha fatto al cervello degli sviluppatori. Considerare se utilizzare le eccezioni non è un'ottimizzazione prematura, è una decisione di progettazione, un'importante decisione di progettazione che ha conseguenze ben al di là delle prestazioni.
Sam Hocevar,

3
@SamHocevar: Mi dispiace, non capisco il tuo punto: La domanda è circa il possibile calo di prestazioni quando si usano le eccezioni. Il mio punto: non pensarci, il colpo non è poi così male (se presente) e altre cose sono molto più importanti. Qui sembri essere d'accordo aggiungendo "decisione progettuale" alla mia lista. OK. D'altra parte, dici che la citazione di Knuth è cattiva e implica che l'ottimizzazione prematura non è cattiva. Ma questo è esattamente quello che è successo qui IMO: la Q non pensa all'architettura, al design o ai diversi algoritmi, ma solo alle eccezioni e al loro impatto sulle prestazioni.
AH,

2
Non sarei d'accordo con la chiarezza in C ++. Il C ++ ha eccezioni non selezionate mentre alcuni compilatori implementano valori restituiti controllati. Di conseguenza, il codice appare più chiaro ma potrebbe contenere una perdita di memoria nascosta o addirittura mettere il codice in uno stato indefinito. Anche il codice di terze parti potrebbe non essere sicuro per le eccezioni. (La situazione è diversa in Java / C # che ha verificato le eccezioni, GC, ...). A partire dalla decisione di progettazione - se non attraversano i punti API, il refactoring da / a ogni stile può essere eseguito in modo semi-automatico con una riga perl.
Maciej Piechotka,

1
@SamHocevar: " La domanda riguarda ciò che è preferibile, non ciò che è più veloce. " Rileggi l'ultimo paragrafo della domanda. L' unica ragione per cui sta anche pensando di non usare le eccezioni è perché pensa che potrebbero essere più lenti. Ora, se pensi che ci siano altre preoccupazioni che il PO non ha preso in considerazione, sentiti libero di pubblicarle o di votare coloro che lo hanno fatto. Ma il PO è chiaramente focalizzato sull'esecuzione delle eccezioni.
Nicol Bolas,

3
@AH - se hai intenzione di citare Knuth, non dimenticare il resto (e IMO la parte più importante): " Eppure non dovremmo rinunciare alle nostre opportunità in quel 3% critico. Un buon programmatore non sarà cullato dall'autocompiacimento di tale ragionamento, sarà saggio esaminare attentamente il codice critico, ma solo dopo che questo codice sarà stato identificato . " Troppo spesso si vede questo come una scusa per non ottimizzare affatto, o per provare a giustificare che il codice sia lento nei casi in cui le prestazioni non sono un'ottimizzazione ma sono in realtà un requisito fondamentale.
Massimo

10

C'è già stata un'ottima risposta a questa domanda, ma vorrei aggiungere alcune considerazioni sulla leggibilità del codice e sul recupero degli errori nel tuo caso particolare.

Credo che il tuo codice potrebbe effettivamente apparire così:

m_fpsTextId = m_statistics->createText( "FPS: 0", 16, 20, 20, 1.0f, 1.0f, 1.0f );
m_cpuTextId = m_statistics->createText( "CPU: 0%", 16, 20, 40, 1.0f, 1.0f, 1.0f );
m_frameTimeTextId = m_statistics->createText( "Frame time: 0", 20, 20, 60, 1.0f, 1.0f, 1.0f );
m_mouseCoordTextId = m_statistics->createText( "Mouse: (0, 0)", 20, 20, 80, 1.0f, 1.0f, 1.0f );
m_cameraPosTextId = m_statistics->createText( "Camera pos: (0, 0, 0)", 40, 20, 100, 1.0f, 1.0f, 1.0f );
m_cameraRotTextId = m_statistics->createText( "Camera rot: (0, 0, 0)", 40, 20, 120, 1.0f, 1.0f, 1.0f );

Nessuna eccezione, nessun if. La segnalazione degli errori può essere effettuata in createText. E createTextpuò restituire un ID trama predefinito che non richiede di verificare il valore restituito, in modo che anche il resto del codice funzioni.

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.