ita - try catch c++ spiegazione




lanciare eccezioni da un distruttore (11)

La maggior parte delle persone dice di non gettare mai un'eccezione da un distruttore: ciò si traduce in un comportamento indefinito. Stroustrup sottolinea che "il distruttore vettoriale richiama esplicitamente il distruttore per ogni elemento, questo implica che se un distruttore di elementi getta, la distruzione del vettore fallisce ... Non c'è davvero un buon modo per proteggersi dalle eccezioni generate dai distruttori, quindi la libreria non fornisce alcuna garanzia se un distruttore di elementi lancia "(dall'appendice E3.2) .

Questo articolo sembra dire diversamente: il lancio di distruttori è più o meno ok.

Quindi la mia domanda è questa: se il lancio da un distruttore provoca un comportamento indefinito, come gestisci gli errori che si verificano durante un distruttore?

Se si verifica un errore durante un'operazione di pulizia, la ignori semplicemente? Se si tratta di un errore che può potenzialmente essere gestito dallo stack ma non nel destructor, non ha senso gettare un'eccezione dal distruttore?

Ovviamente questi tipi di errori sono rari, ma possibili.


D: Quindi la mia domanda è questa: se il lancio da un distruttore provoca un comportamento indefinito, come gestisci gli errori che si verificano durante un distruttore?

A: ci sono diverse opzioni:

  1. Lascia che le eccezioni escano dal tuo distruttore, indipendentemente da quello che succede altrove. E così facendo sii consapevole (o addirittura spaventato) che std :: terminate potrebbe seguire.

  2. Non lasciare mai che l'eccezione esca dal tuo distruttore. Potresti scrivere su un log, se possibile, qualche grosso testo rosso.

  3. il mio preferito : se std::uncaught_exception restituisce false, lascia che le eccezioni scorrono. Se restituisce true, ricade all'approccio di registrazione.

Ma è bello lanciarci d'ufficio?

Sono d'accordo con la maggior parte di quanto sopra che il lancio è meglio evitare in distruttore, dove può essere. Ma a volte sei meglio accettare che possa accadere e gestirlo bene. Sceglierei 3 sopra.

Ci sono alcuni casi strani in cui è davvero una buona idea lanciare da un distruttore. Come il codice di errore "must check". Questo è un tipo di valore che viene restituito da una funzione. Se il chiamante legge / controlla il codice di errore contenuto, il valore restituito si distrugge silenziosamente. Ma , se il codice di errore restituito non è stato letto dal momento in cui i valori di ritorno escono dall'ambito, verrà generata qualche eccezione dal suo distruttore .


Quindi la mia domanda è questa: se il lancio da un distruttore provoca un comportamento indefinito, come gestisci gli errori che si verificano durante un distruttore?

Il problema principale è questo: non puoi fallire . Cosa significa fallire, dopotutto? Se il commit di una transazione su un database fallisce e fallisce il fallimento (fallisce il rollback), cosa succede all'integrità dei nostri dati?

Poiché i distruttori vengono invocati sia per i percorsi normali che per quelli eccezionali (fail), essi stessi non possono fallire o altrimenti "falliamo fallendo".

Questo è un problema concettualmente difficile, ma spesso la soluzione è trovare un modo per assicurarsi che il fallimento non possa fallire. Ad esempio, un database potrebbe scrivere modifiche prima di eseguire il commit su una struttura o file di dati esterni. Se la transazione fallisce, la struttura del file / dati può essere buttata via. Tutto ciò che deve quindi garantire è quello di trasferire le modifiche da quella struttura / file esterno a una transazione atomica che non può fallire.

La soluzione pragmatica è forse solo assicurarsi che le probabilità di fallire in caso di fallimento siano astronomicamente improbabili, dal momento che rendere le cose impossibili da fallire può essere quasi impossibile in alcuni casi.

La soluzione più appropriata per me è scrivere la logica di non-cleanup in modo tale che la logica di cleanup non possa fallire. Ad esempio, se sei tentato di creare una nuova struttura dati per ripulire una struttura dati esistente, forse potresti cercare di creare quella struttura ausiliaria in anticipo in modo che non dobbiamo più crearla all'interno di un distruttore.

Questo è tutto molto più facile a dirsi che a farsi, ammettiamolo, ma è l'unico modo veramente corretto che vedo di fare. A volte penso che ci dovrebbe essere la possibilità di scrivere una logica distruttiva separata per normali percorsi di esecuzione da quelli eccezionali, poiché a volte i distruttori si sentono un po 'come se avessero il doppio delle responsabilità tentando di gestirli entrambi (un esempio è guardie di campo che richiedono un esplicito licenziamento) non lo richiederebbero se potessero differenziare percorsi di distruzione eccezionali da quelli non eccezionali).

Il problema fondamentale è che non possiamo fallire, ed è un difficile problema di progettazione concettuale risolvere perfettamente in tutti i casi. Diventa più facile se non si è troppo coinvolti in strutture di controllo complesse con tonnellate di oggetti teeny che interagiscono tra loro, e invece modellare i tuoi progetti in un modo leggermente più ingombrante (esempio: sistema particellare con un distruttore per distruggere l'intera particella sistema, non un distruttore non banale separato per particella). Quando modellate i vostri progetti con questo tipo di livello più grossolano, avete meno distruttori non banali da trattare e potete anche permettervi qualsiasi sovraccarico di memoria / elaborazione per assicurarvi che i vostri distruttori non possano fallire.

E questa è una delle soluzioni più semplici, naturalmente, è quella di usare i distruttori meno spesso. Nell'esempio delle particelle di cui sopra, forse dopo aver distrutto / rimosso una particella, dovrebbero essere fatte alcune cose che potrebbero fallire per qualsiasi motivo. In tal caso, invece di invocare tale logica attraverso il corpo della particella che potrebbe essere eseguita in un percorso eccezionale, si potrebbe invece fare tutto dal sistema particellare quando rimuove una particella. La rimozione di una particella potrebbe sempre essere eseguita durante un percorso non eccezionale. Se il sistema viene distrutto, forse può semplicemente eliminare tutte le particelle e non preoccuparsi di quella logica di rimozione delle singole particelle che può fallire, mentre la logica che può fallire viene eseguita solo durante la normale esecuzione del sistema di particelle quando rimuove una o più particelle.

Ci sono spesso soluzioni come quella che emergono se si evita di trattare con un sacco di oggetti teeny con distruttori non banali. Dove puoi essere aggrovigliato in un casino dove sembra quasi impossibile essere un'eccezione: la sicurezza è quando ti ritrovi intrappolato in un sacco di oggetti teeny che hanno tutti dvd non banali.

Sarebbe di grande aiuto se nothrow / noexcept venissero effettivamente tradotti in un errore del compilatore se qualcosa che lo specifica (incluse le funzioni virtuali che dovrebbero ereditare la specifica noexcept della sua classe base) ha tentato di richiamare qualsiasi cosa che potesse lanciare. In questo modo saremo in grado di catturare tutta questa roba in fase di compilazione se in realtà scriviamo un distruttore inavvertitamente che potrebbe lanciare.


A differenza dei costruttori, dove l'eccezione di lancio può essere un modo utile per indicare che la creazione dell'oggetto è riuscita, le eccezioni non dovrebbero essere gettate nei distruttori.

Il problema si verifica quando un'eccezione viene lanciata da un distruttore durante il processo di svolgimento dello stack. Se ciò accade, il compilatore viene messo in una situazione in cui non sa se continuare il processo di unwinding dello stack o gestire la nuova eccezione. Il risultato finale è che il tuo programma verrà terminato immediatamente.

Di conseguenza, il miglior modo di agire è semplicemente quello di astenersi dall'utilizzare del tutto le eccezioni nei distruttori. Scrivi invece un messaggio in un file di registro.


Attualmente seguo la politica (che così molti stanno dicendo) che le classi non dovrebbero generare eccezioni dai loro distruttori ma dovrebbero invece fornire un metodo pubblico "vicino" per eseguire l'operazione che potrebbe fallire ...

... ma credo che i distruttori per le classi di tipo contenitore, come un vettore, non dovrebbero mascherare le eccezioni generate dalle classi che contengono. In questo caso, effettivamente utilizzo un metodo "free / close" che si chiama in modo ricorsivo. Sì, ho detto ricorsivamente. C'è un metodo per questa follia. La propagazione delle eccezioni si basa sul fatto che ci sia uno stack: se si verifica una singola eccezione, entrambi i restanti distruttori verranno comunque eseguiti e l'eccezione in sospeso si propagherà una volta che la routine ritorna, il che è ottimo. Se si verificano più eccezioni, allora (a seconda del compilatore) la prima eccezione si propagherà o il programma terminerà, il che è ok. Se si verificano così tante eccezioni che la ricorsione supera lo stack allora qualcosa è seriamente sbagliato, e qualcuno lo scoprirà, il che è anche ok. Personalmente, sbaglio dal lato degli errori che esplodono piuttosto che essere nascosto, segreto e insidioso.

Il punto è che il contenitore rimane neutrale, e spetta alle classi contenute decidere se si comportano o si comportano male per quanto riguarda il lancio di eccezioni dai loro distruttori.


Dobbiamo differenziare qui invece di seguire ciecamente consigli generali per casi specifici .

Si noti che quanto segue ignora il problema dei contenitori di oggetti e cosa fare di fronte a più oggetti di oggetti all'interno dei contenitori. (E può essere ignorato parzialmente, poiché alcuni oggetti non sono adatti per essere inseriti in un contenitore.)

L'intero problema diventa più facile da pensare quando dividiamo le classi in due tipi. Un dirigente di classe può avere due diverse responsabilità:

  • (R) rilascia semantica (alias libera quella memoria)
  • (C) commettere semantica (ovvero fluire file su disco)

Se consideriamo la domanda in questo modo, allora penso che si possa sostenere che la semantica (R) non dovrebbe mai causare un'eccezione da un dtor in quanto vi è a) nulla che possiamo fare al riguardo eb) molte operazioni con risorse libere non fornire anche il controllo degli errori, ad esempio void free(void* p); .

Gli oggetti con la semantica (C), come un oggetto file che ha bisogno di svuotare correttamente i dati o una connessione al database ("protetta dall'ambito") che esegue un commit nel dtor sono di un tipo diverso: Possiamo fare qualcosa sull'errore (su il livello dell'applicazione) e non dovremmo davvero continuare come se nulla fosse accaduto.

Se seguiamo il percorso di RAII e ammettiamo oggetti che hanno una semantica (C) nel loro caso, penso che dovremmo anche tenere conto del caso strano in cui tali motivi possono essere lanciati. Ne consegue che non si dovrebbero mettere tali oggetti in contenitori e ne consegue anche che il programma può ancora terminate() se un commit-dtor lancia mentre un'altra eccezione è attiva.

Per quanto riguarda la gestione degli errori (semantica di Commit / Rollback) e le eccezioni, c'è una buona conversazione da parte di Andrei Alexandrescu : Gestione degli errori in C ++ / Flusso di controllo dichiarativo (tenuto a NDC 2014 )

Nei dettagli, spiega come la libreria Folly implementa un UncaughtExceptionCounter per i ScopeGuard strumenti ScopeGuard .

(Dovrei notare che anche others avevano idee simili).

Mentre il discorso non si focalizza sul lancio da parte di un attore, mostra uno strumento che può essere utilizzato oggi per risolvere i problemi relativi al momento in cui lanciare un film.

In futuro , potrebbe esserci una funzione std per questo, vedere N3614 e una discussione a riguardo .

Aggiornamento '17: La funzione std di C ++ 17 per questo è std::uncaught_exceptions afaikt. Citerò rapidamente l'articolo cppref:

Gli appunti

Un esempio in cui viene utilizzato int -returning uncaught_exceptions è ... ... prima crea un oggetto guard e registra il numero di eccezioni non rilevate nel suo costruttore. L'output viene eseguito dal distruttore dell'oggetto guard, a meno che foo () non lanci ( nel qual caso il numero di eccezioni non rilevate nel distruttore è maggiore di quello osservato dal costruttore )


Il tuo distruttore potrebbe essere eseguito all'interno di una catena di altri distruttori. Lanciare un'eccezione che non viene catturata dal chiamante immediato può lasciare più oggetti in uno stato incoerente, causando così ancora più problemi e ignorando l'errore nell'operazione di pulizia.


In aggiunta alle risposte principali, che sono buone, complete e accurate, vorrei commentare l'articolo che hai fatto riferimento - quello che dice "il lancio di eccezioni nei distruttori non è poi così male".

L'articolo prende la linea "quali sono le alternative per lanciare le eccezioni" ed elenca alcuni problemi con ciascuna delle alternative. Fatto ciò, conclude che, poiché non riusciamo a trovare un'alternativa senza problemi, dovremmo continuare a lanciare eccezioni.

Il problema è che nessuno dei problemi che elenca con le alternative è quasi dannoso come il comportamento delle eccezioni, che, ricordiamo, è "comportamento indefinito del tuo programma". Alcune delle obiezioni dell'autore includono "esteticamente brutto" e "incoraggiare cattivo stile". Ora quale preferiresti avere? Un programma con uno stile cattivo o con un comportamento indefinito?


La vera domanda da porsi riguardo il lancio da un distruttore è "Cosa può fare il chiamante con questo?" C'è davvero qualcosa di utile che puoi fare con l'eccezione, che compenserebbe i pericoli creati dal lancio di un distruttore?

Se distruggo un oggetto Foo e il distruttore Foo lancia un'eccezione, cosa posso ragionevolmente fare con esso? Posso loggarlo o posso ignorarlo. È tutto. Non posso "aggiustarlo", perché l'oggetto Foo è già andato. Caso migliore, registro l'eccezione e continuo come se nulla fosse successo (o terminasse il programma). Vale veramente la pena di causare potenzialmente un comportamento indefinito lanciando da un distruttore?


Lanciare un'eccezione da un distruttore è pericoloso.
Se un'altra eccezione si sta già propagando, l'applicazione verrà chiusa.

#include <iostream>

class Bad
{
    public:
        // Added the noexcept(false) so the code keeps its original meaning.
        // Post C++11 destructors are by default `noexcept(true)` and
        // this will (by default) call terminate if an exception is
        // escapes the destructor.
        //
        // But this example is designed to show that terminate is called
        // if two exceptions are propagating at the same time.
        ~Bad() noexcept(false)
        {
            throw 1;
        }
};
class Bad2
{
    public:
        ~Bad2()
        {
            throw 1;
        }
};


int main(int argc, char* argv[])
{
    try
    {
        Bad   bad;
    }
    catch(...)
    {
        std::cout << "Print This\n";
    }

    try
    {
        if (argc > 3)
        {
            Bad   bad; // This destructor will throw an exception that escapes (see above)
            throw 2;   // But having two exceptions propagating at the
                       // same time causes terminate to be called.
        }
        else
        {
            Bad2  bad; // The exception in this destructor will
                       // cause terminate to be called.
        }
    }
    catch(...)
    {
        std::cout << "Never print this\n";
    }

}

Questo fondamentalmente si riduce a:

Qualunque cosa pericolosa (cioè che potrebbe generare un'eccezione) dovrebbe essere fatta con metodi pubblici (non necessariamente direttamente). L'utente della classe può quindi gestire potenzialmente queste situazioni utilizzando i metodi pubblici e rilevando eventuali eccezioni.

Il distruttore terminerà quindi l'oggetto chiamando questi metodi (se l'utente non l'ha fatto esplicitamente), ma qualsiasi lancio di eccezioni viene catturato e abbandonato (dopo aver tentato di risolvere il problema).

Quindi in effetti si passa la responsabilità sull'utente. Se l'utente è in grado di correggere le eccezioni, chiamerà manualmente le funzioni appropriate ed elabora eventuali errori. Se l'utente dell'oggetto non è preoccupato (poiché l'oggetto verrà distrutto), il distruttore viene lasciato a occuparsi degli affari.

Un esempio:

std :: fstream

Il metodo close () può potenzialmente generare un'eccezione. Il distruttore chiama close () se il file è stato aperto ma si assicura che eventuali eccezioni non si propagino fuori dal distruttore.

Pertanto, se l'utente di un oggetto file desidera eseguire una gestione speciale per i problemi associati alla chiusura del file, chiamerà manualmente close () e gestirà eventuali eccezioni. Se invece non gli interessa, allora il distruttore sarà lasciato a gestire la situazione.

Scott Myers ha un eccellente articolo sull'argomento nel suo libro "Effective C ++"

Modificare:

Apparentemente anche in "Più efficace C ++"
Elemento 11: impedire che le eccezioni lascino i distruttori


Martin Ba (sopra) è sulla strada giusta, architetto in modo diverso per la logica RELEASE e COMMIT.

Per la versione:

Dovresti mangiare qualsiasi errore. Stai liberando memoria, chiudendo le connessioni, ecc. Nessun altro nel sistema dovrebbe mai VEDERE di nuovo quelle cose e stai restituendo le risorse al sistema operativo. Se sembra che tu abbia bisogno di una reale gestione degli errori qui, è probabile che sia una conseguenza dei difetti di progettazione nel tuo modello a oggetti.

Per Commit:

Qui è dove vuoi lo stesso tipo di oggetti wrapper RAII che cose come std :: lock_guard forniscono mutex. Con quelli che non metti la logica di commit in the dtor AT ALL. Hai un'API dedicata per esso, quindi gli oggetti wrapper che RAII lo impegnerà in THEIR dtors e gestirà gli errori lì. Ricorda, puoi CATCHARE eccezioni in un distruttore bene; li sta emettendo che è mortale. Ciò consente anche di implementare criteri e gestione degli errori diversi semplicemente creando un wrapper diverso (ad es. Std :: unique_lock vs. std :: lock_guard), e garantisce che non dimenticherai di chiamare la logica di commit, che è l'unica a metà strada giustificazione decente per metterlo in un datore al primo posto.


Tutti gli altri hanno spiegato perché lanciare i distruttori sono terribili ... cosa puoi fare a riguardo? Se si sta eseguendo un'operazione che potrebbe non riuscire, creare un metodo pubblico separato che esegua la pulizia e possa generare eccezioni arbitrarie. Nella maggior parte dei casi, gli utenti lo ignoreranno. Se gli utenti vogliono monitorare il successo / fallimento della pulizia, possono semplicemente chiamare la routine di pulizia esplicita.

Per esempio:

class TempFile {
public:
    TempFile(); // throws if the file couldn't be created
    ~TempFile() throw(); // does nothing if close() was already called; never throws
    void close(); // throws if the file couldn't be deleted (e.g. file is open by another process)
    // the rest of the class omitted...
};




raii