Devo ereditare da std :: exception?


98

Ho visto almeno una fonte affidabile (una classe C ++ che ho preso) raccomandare che le classi di eccezioni specifiche dell'applicazione in C ++ dovrebbero ereditare da std::exception. Non sono chiaro i vantaggi di questo approccio.

In C # le ragioni per ereditare da ApplicationExceptionsono chiare: ottieni una manciata di metodi, proprietà e costruttori utili e devi solo aggiungere o sovrascrivere ciò di cui hai bisogno. Con std::exceptionsembra che tutto ciò che ottieni sia un what()metodo per sovrascrivere, che potresti anche creare tu stesso.

Quindi quali sono i vantaggi, se ce ne sono, dell'utilizzo std::exceptioncome classe base per la mia classe di eccezione specifica per l'applicazione? Ci sono buone ragioni per non ereditare std::exception?


Potresti voler dare un'occhiata a questo: stackoverflow.com/questions/1605778/1605852#1605852
sbi

2
Sebbene come nota a margine non correlata alla particolare domanda, le classi C ++ che prendi non devono necessariamente essere fonti affidabili di buone pratiche solo per loro diritto.
Christian Rau

Risposte:


69

Il vantaggio principale è che il codice che utilizza le tue classi non deve conoscere il tipo esatto di ciò che stai facendo throw, ma può solo catchil file std::exception.

Modifica: come hanno notato Martin e altri, in realtà si desidera derivare da una delle sottoclassi std::exceptiondichiarate <stdexcept>nell'intestazione.


21
Non c'è modo di passare un messaggio a std :: exception. std :: runtime_error accetta una stringa ed è derivato da std :: exception.
Martin York

14
Non dovresti passare un messaggio al costruttore del tipo di eccezione (considera che i messaggi devono essere localizzati). Definisci invece un tipo di eccezione che categorizzi semanticamente l'errore, memorizza nell'oggetto eccezione ciò di cui hai bisogno per formattare un messaggio user-friendly , quindi fallo nel sito di cattura.
Emil

std::exceptionè definita nella <exception>intestazione ( prova ). -1
Alexander Shukaev

5
Allora qual è il tuo punto? La definizione implica la dichiarazione, cosa vedi che c'è di sbagliato qui?
Nikolai Fetissov

40

Il problema std::exceptionè che non esiste un costruttore (nelle versioni conformi allo standard) che accetti un messaggio.

Di conseguenza preferisco derivare da std::runtime_error. Questo è derivato da, std::exceptionma i suoi costruttori consentono di passare una stringa C o std::stringa al costruttore che verrà restituito (come a char const*) quando what()viene chiamato.


11
Non è una buona idea formattare un messaggio user-friendly al punto di lancio perché questo accoppierebbe il codice di livello inferiore con funzionalità di localizzazione e quant'altro. Memorizza invece nell'oggetto eccezione tutte le informazioni rilevanti e lascia che il sito di cattura formatti un messaggio intuitivo in base al tipo di eccezione e ai dati che trasporta.
Emil

16
@ Emil: Certo, se fai eccezione, porta messaggi visualizzabili dall'utente, anche se generalmente sono solo a scopo di registrazione. Nel sito di lancio non hai il contesto per creare un messaggio utente (poiché questa potrebbe essere comunque una libreria). Il sito di cattura avrà più contesto e può generare il messaggio appropriato.
Martin York

3
Non so da dove ti sia venuta l'idea che le eccezioni siano solo a scopo di registrazione. :)
Emil

1
@ Emil: abbiamo già affrontato questo argomento. Dove ho sottolineato che questo non è ciò che stai mettendo nell'eccezione. La tua comprensione dell'argomento sembra buona ma la tua argomentazione è imperfetta. I messaggi di errore generati per l'utente sono completamente diversi dai messaggi di registro per lo sviluppatore.
Martin York

1
@ Emil. Questa discussione è vecchia di un anno. Crea la tua domanda a questo punto. Le tue argomentazioni per quanto mi riguarda sono semplicemente sbagliate (e sulla base dei voti le persone tendono ad essere d'accordo con me). Vedi Qual è un "buon numero" di eccezioni da implementare per la mia libreria? e catturare le eccezioni generali è davvero una cosa negativa? Non hai fornito alcuna nuova informazione dalla tua precedente diatriba.
Martin York

17

Il motivo per ereditare da std::exceptionè la sua classe base "standard" per le eccezioni, quindi è naturale per altre persone in un team, ad esempio, aspettarselo e catturare la base std::exception.

Se stai cercando comodità, puoi ereditare da std::runtime_errorquel fornisce il std::stringcostruttore.


2
Derivare da qualsiasi altro tipo di eccezione standard eccetto std :: exception probabilmente non è una buona idea. Il problema è che derivano da std :: exception non virtualmente, il che può portare a bug sottili che coinvolgono ereditarietà multipla in cui un catch (std :: exception &) potrebbe silenziosamente non riuscire a catturare un'eccezione a causa della conversione in std :: exception essendo ambiguo.
Emil

2
Ho letto l'articolo sul boost sull'argomento. Sembra ragionevole in un senso logico puro. Ma non ho mai visto MH eccezioni in natura. Né prenderei in considerazione l'utilizzo di MH in eccezioni, quindi sembra una mazza per risolvere un problema inesistente. Se questo fosse un problema reale, sono sicuro che avremmo visto un'azione da parte del comitato per gli standard per correggere un difetto così evidente. Quindi la mia opinione è che std :: runtime_error (e family) siano ancora eccezioni perfettamente accettabili da lanciare o da cui derivare.
Martin York

5
@ Emil: Derivare da altre eccezioni standard inoltre std::exceptionè un'idea eccellente . Quello che non dovresti fare è ereditare da più di uno.
Mooing Duck

@MooingDuck: se derivi da più di un tipo di eccezione standard, ti ritroverai con std :: exception derivata (non virtualmente) più volte. Vedi boost.org/doc/libs/release/libs/exception/doc/… .
Emil

1
@ Emil: Questo segue da quello che ho detto.
Mooing Duck

13

Una volta ho partecipato alla pulizia di una grande base di codice in cui gli autori precedenti avevano lanciato int, HRESULTS, std :: string, char *, classi casuali ... cose diverse ovunque; basta nominare un tipo e probabilmente è stato lanciato da qualche parte. E nessuna classe base comune. Credimi, le cose erano molto più ordinate una volta arrivati ​​al punto che tutti i tipi lanciati avevano una base comune che potevamo catturare e sapere che nulla sarebbe passato. Quindi, per favore, fai un favore a te stesso (e a coloro che dovranno mantenere il tuo codice in futuro) e fallo in questo modo dall'inizio.


12

Dovresti ereditare da boost :: exception . Fornisce molte più funzionalità e modi ben compresi per trasportare dati aggiuntivi ... ovviamente, se non stai usando Boost , ignora questo suggerimento.


5
Tuttavia si noti che boost :: exception in sé non deriva da std :: exception. Anche quando derivi da boost :: exception, dovresti anche derivare da std :: exception (come nota separata, ogni volta che derivi da tipi di eccezione, dovresti usare l'ereditarietà virtuale).
Emil

10

Sì, dovresti derivare da std::exception.

Altri hanno risposto che std::exceptionha il problema che non è possibile passare un messaggio di testo, tuttavia in genere non è una buona idea tentare di formattare un messaggio utente nel punto in cui viene lanciato. Utilizzare invece l'oggetto eccezione per trasportare tutte le informazioni rilevanti al sito di cattura che può quindi formattare un messaggio intuitivo.


6
+1, buon punto. Sia da una separazione delle preoccupazioni che da una prospettiva i18n, è decisamente meglio lasciare che il livello di presentazione costruisca il messaggio dell'utente.
John M Gant,

@JohnMGant ed Emil: Interessante, potete indicare un esempio concreto di come questo può essere fatto. Capisco che si possa derivare std::exceptione portare le informazioni dell'eccezione. Ma chi sarà responsabile della creazione / formattazione del messaggio di errore? La ::what()funzione o qualcos'altro?
alfC

1
Dovresti formattare il messaggio nel sito di cattura, molto probabilmente a livello di applicazione (piuttosto che di libreria). Lo prendi per tipo, quindi cerca informazioni, quindi localizza / formatta il messaggio utente appropriato.
Emil

9

Il motivo per cui potresti voler ereditare da std::exceptionè perché ti consente di lanciare un'eccezione che viene catturata in base a quella classe, ovvero:

class myException : public std::exception { ... };
try {
    ...
    throw myException();
}
catch (std::exception &theException) {
    ...
}

6

C'è un problema con l'ereditarietà di cui dovresti essere a conoscenza è il sezionamento degli oggetti. Quando scrivi throw e;un'espressione di lancio inizializza un oggetto temporaneo, chiamato oggetto eccezione, il cui tipo viene determinato rimuovendo qualsiasi qualificatore cv di primo livello dal tipo statico dell'operando di throw. Potrebbe non essere quello che ti aspetti. Esempio di problema che potresti trovare qui .

Non è un argomento contro l'ereditarietà, è solo un'informazione "da conoscere".


3
Penso che il take away sia quel "throw e;" è il male e "lancia;" è ok.
James Schek

2
Sì, throw;va bene, ma non è ovvio che dovresti scrivere qualcosa del genere.
Kirill V. Lyadvinsky

1
È particolarmente doloroso per gli sviluppatori Java in cui il rilancio viene eseguito utilizzando "throw e;"
James Schek

Considera di derivare solo da tipi di base astratti. La cattura di un tipo di base astratto per valore darà un errore (evitando di affettare); catturare un tipo di base astratto per riferimento e quindi provare a lanciarne una copia ti darà un errore (di nuovo evitando di affettare.)
Emil

È inoltre possibile risolvere questo problema con l'aggiunta di una funzione membro, raise()come ad esempio: virtual void raise() { throw *this; }. Ricorda solo di sovrascriverlo in ogni eccezione derivata.
Justin Time - Ripristina Monica il

5

Differenza: std :: runtime_error vs std :: exception ()

Sia che si dovrebbe ereditare da essa o meno spetta a voi. Lo standard std::exceptionei suoi discendenti standard propongono una possibile struttura gerarchica delle eccezioni (divisione in logic_errorsubhierarchy e runtime_errorsubhierarchy) e una possibile interfaccia dell'oggetto eccezione. Se ti piace, usalo. Se per qualche motivo hai bisogno di qualcosa di diverso, definisci il tuo framework di eccezioni.


3

Poiché la lingua lancia già std :: exception, è necessario prenderla comunque per fornire una segnalazione degli errori decente. Puoi anche usare lo stesso trucco per tutte le tue eccezioni impreviste. Inoltre, quasi tutte le librerie che generano eccezioni le deriverebbero da std :: exception.

In altre parole, è l'uno o l'altro

catch (...) {cout << "Unknown exception"; }

o

catch (const std::exception &e) { cout << "unexpected exception " << e.what();}

E la seconda opzione è decisamente migliore.


3

Se tutte le tue possibili eccezioni derivano da std::exception, il tuo blocco di cattura può semplicemente catch(std::exception & e)essere certo di catturare tutto.

Dopo aver acquisito l'eccezione, puoi utilizzare quel whatmetodo per ottenere maggiori informazioni. C ++ non supporta la digitazione anatra, quindi un'altra classe con un whatmetodo richiederebbe un catch diverso e un codice diverso per usarlo.


7
non sottoclasse std :: exception e quindi catch (std :: exception e). Devi catturare un riferimento a std :: exception & (o un puntatore) altrimenti stai tagliando i dati della sottoclasse.
jmucchiello

1
Questo è stato un errore stupido, di certo lo so meglio. stackoverflow.com/questions/1095225/…
Mark Ransom

3

La prima domanda è se derivare da qualsiasi tipo di eccezione standard o meno. In questo modo si abilita un singolo gestore di eccezioni per tutte le eccezioni della libreria standard e la propria, ma incoraggia anche tali gestori catch-them-all. Il problema è che si dovrebbero catturare solo le eccezioni che si sa come gestire. In main (), ad esempio, catturare tutte le eccezioni std :: è probabilmente una buona cosa se la stringa what () verrà registrata come ultima risorsa prima di uscire. Altrove, tuttavia, è improbabile che sia una buona idea.

Una volta deciso se derivare da un tipo di eccezione standard o meno, la domanda è quale dovrebbe essere la base. Se la tua applicazione non necessita di i18n, potresti pensare che la formattazione di un messaggio sul sito della chiamata sia altrettanto buono che salvare le informazioni e generare il messaggio sul sito della chiamata. Il problema è che il messaggio formattato potrebbe non essere necessario. È meglio utilizzare uno schema di generazione di messaggi pigri, magari con memoria preallocata. Quindi, se il messaggio è necessario, verrà generato all'accesso (e, possibilmente, memorizzato nella cache nell'oggetto eccezione). Pertanto, se il messaggio viene generato quando viene lanciato, è necessario un derivato std :: exception, come std :: runtime_error come classe base. Se il messaggio viene generato pigramente, std :: exception è la base appropriata.


0

Un altro motivo per le eccezioni sottoclasse è un aspetto di progettazione migliore quando si lavora su grandi sistemi incapsulati. Puoi riutilizzarlo per cose come messaggi di convalida, query degli utenti, errori irreversibili del controller e così via. Piuttosto che riscrivere o riagganciare tutta la tua convalida come i messaggi, puoi semplicemente "catturarla" nel file sorgente principale, ma lanciare l'errore ovunque nell'intero insieme di classi.

Ad esempio, un'eccezione fatale terminerà il programma, un errore di convalida cancellerà solo lo stack e una query dell'utente farà una domanda all'utente finale.

In questo modo puoi anche riutilizzare le stesse classi ma su interfacce diverse. Ad esempio, un'applicazione Windows può utilizzare una finestra di messaggio, un servizio Web mostrerà html e il sistema di reporting lo registrerà e così via.


0

Sebbene questa domanda sia piuttosto vecchia e abbia già ricevuto molte risposte, voglio solo aggiungere una nota su come gestire correttamente le eccezioni in C ++ 11, dal momento che mi manca continuamente nelle discussioni sulle eccezioni:

Usa std::nested_exceptionestd::throw_with_nested

È descritto su StackOverflow qui e qui , come puoi ottenere un backtrace sulle tue eccezioni all'interno del tuo codice senza bisogno di un debugger o di un log complicato, semplicemente scrivendo un gestore di eccezioni appropriato che rilancerà le eccezioni nidificate.

Dato che puoi farlo con qualsiasi classe di eccezione derivata, puoi aggiungere molte informazioni a tale backtrace! Puoi anche dare un'occhiata al mio MWE su GitHub , dove un backtrace sarebbe simile a questo:

Library API: Exception caught in function 'api_function'
Backtrace:
~/Git/mwe-cpp-exception/src/detail/Library.cpp:17 : library_function failed
~/Git/mwe-cpp-exception/src/detail/Library.cpp:13 : could not open file "nonexistent.txt"

Non è nemmeno necessario creare una sottoclasse std::runtime_errorper ottenere molte informazioni quando viene generata un'eccezione.

L'unico vantaggio che vedo nella sottoclasse (invece di usare solo std::runtime_error) è che il tuo gestore di eccezioni può catturare la tua eccezione personalizzata e fare qualcosa di speciale. Per esempio:

try
{
  // something that may throw
}
catch( const MyException & ex )
{
  // do something specialized with the
  // additional info inside MyException
}
catch( const std::exception & ex )
{
  std::cerr << ex.what() << std::endl;
}
catch( ... )
{
  std::cerr << "unknown exception!" << std::endl;
}
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.