Perché le specifiche delle eccezioni sono errate?


50

Tornati a scuola circa 10 anni fa, ti stavano insegnando a usare gli identificatori di eccezione. Dal momento che il mio background è uno di quei programmatori Torvaldish C che evita ostinatamente il C ++ a meno che non sia costretto a farlo, finisco sporadicamente in C ++ e, quando lo faccio, uso ancora gli identificatori di eccezioni poiché è quello che mi è stato insegnato.

Tuttavia, la maggior parte dei programmatori C ++ sembra disapprovare gli specificatori di eccezioni. Ho letto il dibattito e gli argomenti di vari guru del C ++, come questi . Per quanto ne capisco, si riduce a tre cose:

  1. Gli identificatori di eccezione utilizzano un sistema di tipi incompatibile con il resto della lingua ("sistema di tipi ombra").
  2. Se la tua funzione con un identificatore di eccezione genera qualcos'altro tranne quello che hai specificato, il programma verrà terminato in modi errati e inaspettati.
  3. Gli identificatori di eccezione verranno rimossi nel prossimo standard C ++.

Mi sto perdendo qualcosa qui o sono tutti questi i motivi?

Le mie opinioni:

Riguardo a 1): E allora. Il C ++ è probabilmente il linguaggio di programmazione più incoerente mai realizzato, in termini di sintassi. Abbiamo le macro, il goto / etichette, l'orda (accaparramento?) Di comportamenti non definiti / non specificati / definiti dall'implementazione, i tipi interi mal definiti, tutte le regole di promozione del tipo implicito, parole chiave per casi speciali come amico, auto , registrati, esplicito ... E così via. Qualcuno potrebbe probabilmente scrivere diversi libri spessi di tutte le stranezze in C / C ++. Quindi perché le persone reagiscono contro questa particolare incoerenza, che è un piccolo difetto rispetto a molte altre caratteristiche molto più pericolose della lingua?

Riguardo a 2): Non è mia responsabilità? Ci sono molti altri modi in cui posso scrivere un bug fatale in C ++, perché questo caso particolare è peggio? Invece di scrivere throw(int)e poi lanciare Crash_t, posso anche affermare che la mia funzione restituisce un puntatore a int, quindi crea un typecast selvaggio ed esplicito e restituisce un puntatore a Crash_t. Lo spirito di C / C ++ è sempre stato quello di lasciare la maggior parte delle responsabilità al programmatore.

Che dire dei vantaggi allora? Il più ovvio è che se la tua funzione prova a lanciare esplicitamente qualsiasi tipo diverso da quello che hai specificato, il compilatore ti darà un errore. Credo che lo standard sia chiaro al riguardo (?). I bug si verificano solo quando la tua funzione chiama altre funzioni che a loro volta generano il tipo sbagliato.

Proveniente da un mondo di programmi C deterministici e integrati, preferirei sicuramente sapere esattamente quale funzione mi verrà lanciata. Se c'è qualcosa nella lingua che lo supporta, perché non usarlo? Le alternative sembrano essere:

void func() throw(Egg_t);

e

void func(); // This function throws an Egg_t

Penso che ci sia una grande possibilità che il chiamante ignori / dimentichi di implementare il try-catch nel secondo caso, meno nel primo caso.

A quanto ho capito, se una di queste due forme decide di lanciare improvvisamente un altro tipo di eccezione, il programma andrà in crash. Nel primo caso perché non è consentito lanciare un'altra eccezione, nel secondo caso perché nessuno si aspettava che generasse un SpanishInquisition_t e quindi quell'espressione non viene catturata dove avrebbe dovuto essere.

Nel caso di quest'ultimo, avere qualche ultima possibilità di cattura (...) al livello più alto del programma non sembra davvero migliore di un crash del programma: "Ehi, da qualche parte nel tuo programma qualcosa ha lanciato un'eccezione strana e non gestita . ". Non puoi ripristinare il programma una volta che sei così lontano da dove è stata generata l'eccezione, l'unica cosa che puoi fare è uscire dal programma.

E dal punto di vista dell'utente non potrebbe importare di meno se ricevono una finestra di messaggio malvagio dal sistema operativo che dice "Programma terminato. Blablabla all'indirizzo 0x12345" o una finestra di messaggio malvagia dal programma che dice "Eccezione non gestita: myclass. func.something". Il bug è ancora lì.


Con il prossimo standard C ++ non avrò altra scelta che abbandonare gli identificatori di eccezione. Ma preferirei ascoltare alcune solide argomentazioni sul perché siano cattive, piuttosto che "Sua Santità l'ha dichiarato e quindi è così". Forse ci sono più argomenti contro di loro rispetto a quelli che ho elencato, o forse c'è più per loro di quello che mi rendo conto?


30
Sono tentato di sottovalutare questo come "rant, travestito da domanda". Mi chiedi tre punti validi su E.specs, ma perché ci dai fastidio con il tuo C ++ - è-così-fastidioso-e-io-come-C-migliori divagazioni?
Martin Ba,

10
@Martin: Perché voglio sottolineare che sono sia parziale che non aggiornato con tutti i dettagli, ma anche che considero il linguaggio con occhi ingenui e / o non ancora rovinati. Ma anche che C e C ++ sono già linguaggi incredibilmente imperfetti, quindi un difetto in più o in meno non ha importanza. Il post era in realtà molto peggio prima che lo modificassi :) Gli argomenti contro gli specificatori di eccezioni sono anche abbastanza soggettivi e quindi è difficile discuterli senza diventare soggettivi.

SpanishInquisition_t! Divertente! Personalmente sono rimasto colpito dall'uso del puntatore del frame dello stack per generare eccezioni e sembra che possa rendere il codice molto più pulito. Tuttavia, non ho mai scritto codice con eccezioni. Chiamami vecchio stile, ma i valori di ritorno funzionano bene per me.
Shahbaz,

@Shahbaz Come si può leggere tra le righe, anche io sono abbastanza vecchio stile, ma ancora non mi sono mai chiesto se le eccezioni da sole siano buone o cattive.

2
Il passato di tiro è gettato , non è gettato . È un verbo forte.
TRiG

Risposte:


48

Le specifiche delle eccezioni sono errate perché sono applicate in modo debole e quindi in realtà non ottengono molto, e sono anche cattive perché costringono il runtime a verificare eccezioni impreviste in modo che possano terminare (), invece di invocare UB , questo può sprecare una quantità significativa di prestazioni.

Quindi, in sintesi, le specifiche delle eccezioni non sono applicate abbastanza forte nella lingua per rendere il codice più sicuro, e implementarle come specificato è stato un grande consumo di prestazioni.


2
Ma il controllo del runtime non è colpa della specifica dell'eccezione in quanto tale, ma piuttosto di qualsiasi parte dello standard che afferma che dovrebbe esserci una terminazione se viene lanciato il tipo errato. Non sarebbe più intelligente rimuovere semplicemente il requisito che porta a questo controllo dinamico e lasciare che tutto sia valutato staticamente dal compilatore? Sembra che RTTI sia il vero colpevole qui, o ...?

7
@Lundin: non è possibile determinare staticamente tutte le eccezioni che potrebbero essere generate da una funzione in C ++, poiché il linguaggio consente modifiche arbitrarie e non deterministiche al flusso di controllo in fase di esecuzione (ad esempio tramite puntatori a funzione, metodi virtuali e simili). L'implementazione del controllo statico delle eccezioni durante la compilazione come Java richiederebbe modifiche fondamentali al linguaggio e molta compatibilità C per essere disabilitata.

3
@OrbWeaver: Penso che i controlli statici siano del tutto possibili: le specifiche dell'eccezione farebbero parte delle specifiche di una funzione (come il valore di ritorno) e i puntatori di funzione, le sostituzioni virtuali, ecc. Dovrebbero avere specifiche compatibili. L'obiezione principale sarebbe che si tratta di una caratteristica del tutto o niente: le funzioni con specifiche di eccezione statiche non potevano chiamare funzioni legacy. (Chiamare le funzioni C andrebbe bene, dal momento che non possono lanciare nulla).
Mike Seymour,

2
@Lundin: le specifiche delle eccezioni non sono state rimosse, ma sono state deprecate. Il codice legacy che li utilizza sarà comunque valido.
Mike Seymour,

1
@Lundin: le vecchie versioni di C ++ con specifiche di eccezione sono perfettamente compatibili. Non saranno in grado di compilare o eseguire inaspettatamente se il codice era valido prima.
DeadMG

18

Uno dei motivi per cui nessuno li usa è perché il tuo presupposto principale è sbagliato:

"Il più ovvio [vantaggio] è che se la tua funzione prova a lanciare esplicitamente qualsiasi tipo diverso da quello che hai specificato, il compilatore ti darà un errore."

struct foo {};
struct bar {};

struct test
{
    void baz() throw(foo)
    {
        throw bar();
    }
};

int main()
{
    test x;
    try { x.baz(); } catch(bar &b) {}
}

Questo programma viene compilato senza errori o avvisi .

Inoltre, anche se l'eccezione sarebbe stata rilevata , il programma termina comunque.


Modifica: per rispondere a un punto della tua domanda, cattura (...) (o meglio, cattura (std :: exeption &) o altra classe base, e poi cattura (...)) è ancora utile anche se non lo fai sapere esattamente cosa è andato storto.

Considera che il tuo utente ha premuto un pulsante di menu per "salva". Il gestore per il pulsante menu invita l'applicazione a salvare. Questo potrebbe fallire per una miriade di ragioni: il file si trovava su una risorsa di rete scomparsa, c'era un file di sola lettura e non poteva essere salvato, ecc. Ma al gestore non importa davvero perché qualcosa non ha funzionato; importa solo che sia riuscito o fallito. Se ci è riuscito, va tutto bene. Se fallito, può dirlo all'utente. L'esatta natura dell'eccezione è irrilevante. Inoltre, se si scrive un codice corretto, sicuro per le eccezioni, significa che qualsiasi errore di questo tipo può propagarsi attraverso il sistema senza annullarlo, anche per il codice che non si preoccupa dell'errore. Ciò semplifica l'estensione del sistema. Ad esempio, ora si salva tramite una connessione al database.


4
@Lundin è stato compilato senza avvisi. E no, non puoi. Non affidabile. È possibile chiamare tramite i puntatori a funzione o potrebbe essere una funzione membro virtuale e la classe derivata genera deriv_exception (di cui la classe base non può sapere).
Kaz Dragon

Buona fortuna Embarcadero / Borland dà un avvertimento per questo: "lanciare un'eccezione viola l'identificatore di eccezione". Apparentemente GCC no. Non c'è motivo per cui non possano implementare un avviso però. Allo stesso modo, uno strumento di analisi statica potrebbe facilmente trovare questo errore.

14
@Lundin: Per favore, non iniziare a discutere e ripetere informazioni false. La tua domanda era già una sorta di sfogo, e affermare cose false non aiuta. Uno strumento di analisi statica NON PU find trovare se ciò accade; farlo comporterebbe la risoluzione del problema di arresto. Inoltre, dovrebbe analizzare l'intero programma e molto spesso l'intera fonte non è disponibile.
David Thornley,

1
@Lundin lo stesso strumento di analisi statica potrebbe dirti quali eccezioni generate lasciano il tuo stack, quindi in questo caso l'utilizzo delle specifiche delle eccezioni non ti compra ancora nulla tranne un potenziale falso negativo che porterà il tuo programma in cui avresti potuto gestire il caso di errore in un modo molto più bello (come annunciare l'errore e continuare a correre: vedere la seconda metà della mia risposta per un esempio).
Kaz Dragon

1
@Lundin Una buona domanda. La risposta è che, in generale, non sai se le funzioni che stai chiamando genereranno eccezioni, quindi il presupposto sicuro è che lo fanno tutti. Dove catturare questi è una domanda a cui ho risposto in stackoverflow.com/questions/4025667/… . I gestori di eventi in particolare formano i confini del modulo naturale. Permettere che le eccezioni sfuggano a un sistema di finestra / evento generico (ad es.) Verrà punito. Il gestore dovrebbe prenderli tutti se il programma deve sopravvivere lì.
Kaz Dragon

17

Questa intervista con Anders Hejlsberg è abbastanza famosa. In esso, spiega perché il team di progettazione di C # ha scartato le eccezioni verificate in primo luogo. In breve, ci sono due ragioni principali: la versatilità e la scalabilità.

So che l'OP si sta concentrando sul C ++ mentre Hejlsberg sta discutendo del C #, ma i punti che Hejlsberg fa valere sono perfettamente applicabili anche al C ++.


5
"i punti che Hejlsberg sottolinea sono perfettamente applicabili anche al C ++" .. Sbagliato! Le eccezioni verificate come implementate in Java costringono il chiamante a gestirle (e / o il chiamante, ecc.). Le specifiche di eccezione in C ++ (sebbene piuttosto deboli e inutili) si applicano solo a un singolo "limite di chiamata di funzione" e non "propagano lo stack di chiamate" come fanno le eccezioni verificate.
Martin Ba,

3
@Martin: sono d'accordo con te sul comportamento delle specifiche di eccezione in C ++. Ma la mia frase che citi "i punti che Hejlsberg afferma sono perfettamente applicabili anche al C ++" si riferisce, beh, ai punti che Hejlsberg sottolinea, piuttosto che alle specifiche del C ++. In altre parole, sto parlando qui della versione e della scalabilità del programma, piuttosto che della propagazione delle eccezioni.
CesarGon,

3
Non ero d'accordo con Hejlsberg abbastanza presto: "La preoccupazione che ho riguardo alle eccezioni verificate è le manette che mettono ai programmatori. Vedi i programmatori che raccolgono nuove API che hanno tutte queste clausole di lancio, e poi vedi quanto è contorto il loro codice, e ti rendi conto le eccezioni verificate non le stanno aiutando. È una specie di questi progettisti API dittatoriali che ti spiegano come gestire la tua eccezione. " --- Le eccezioni vengono verificate o meno, devi comunque gestire le eccezioni generate dall'API che stai chiamando. Le eccezioni verificate lo rendono semplicemente esplicito.
quant_dev,

5
@quant_dev: No, non devi gestire le eccezioni generate dall'API che stai chiamando. Un'opzione perfettamente valida è quella di non gestire le eccezioni e lasciarle riempire lo stack di chiamate che il chiamante può gestire.
CesarGon,

4
@CesarGon Non gestire le eccezioni è anche fare una scelta su come gestirle.
quant_dev,
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.