Domanda

La maggior parte delle persone dice che non getta mai un'eccezione al di fuori di un distruttore, poiché ciò comporta un comportamento indefinito. Stroustrup sottolinea che "il vettore distruttore invoca esplicitamente il distruttore per ogni elemento. Ciò implica che se un distruttore di elementi lancia, la distruzione del vettore fallisce ... Non c'è davvero un buon modo per proteggersi dalle eccezioni lanciate dai distruttori, quindi la biblioteca non fornisce garanzie se un distruttore di elementi lancia " (dall'appendice E3.2) .

Questo articolo sembra dire il contrario: il lancio di distruttori sono più o meno a posto.

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

Se si verifica un errore durante un'operazione di pulizia, lo ignori e basta? Se si tratta di un errore potenzialmente gestibile nello stack ma non proprio nel distruttore, non ha senso gettare un'eccezione dal distruttore?

Ovviamente questo tipo di errori sono rari, ma possibili.

È stato utile?

Soluzione

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 in sostanza si riduce a:

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

Il distruttore finirà quindi l'oggetto chiamando questi metodi (se l'utente non lo ha fatto in modo esplicito), ma eventuali eccezioni vengono catturate e eliminate (dopo aver tentato di risolvere il problema).

Quindi in effetti si passa la responsabilità all'utente. Se l'utente è in grado di correggere le eccezioni, chiamerà manualmente le funzioni appropriate ed elaborerà 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 propaghino dal distruttore.

Quindi, se l'utente di un oggetto file desidera eseguire una gestione speciale per problemi associati alla chiusura del file, chiamerà manualmente close () e gestirà eventuali eccezioni. Se d'altra parte non gliene importa, il distruttore sarà lasciato a gestire la situazione.

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

Modifica:

Apparentemente anche in "C ++ più efficace"
Articolo 11: Impedisci alle eccezioni di lasciare i distruttori

Altri suggerimenti

Lanciare fuori da un distruttore può provocare un arresto, perché questo distruttore potrebbe essere chiamato come parte di " Stack svolgendo " ;. Lo svolgimento della pila è una procedura che si verifica quando viene generata un'eccezione. In questa procedura, tutti gli oggetti che sono stati inseriti nello stack dopo il "tentativo" e fino a quando non verrà generata l'eccezione, verrà chiusa - > i loro distruttori saranno chiamati. E durante questa procedura, non è consentito un altro lancio di eccezioni, poiché non è possibile gestire due eccezioni alla volta, quindi, ciò provocherà una chiamata a abort (), il programma si arresterà in modo anomalo e il controllo tornerà al sistema operativo.

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

Nota che il seguente 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 medico di classe può avere due diverse responsabilità:

  • (R) rilascia la semantica (aka libera quella memoria)
  • (C) commit semantica (aka flush file su disco)

Se consideriamo la domanda in questo modo, penso che si possa sostenere che la semantica (R) non dovrebbe mai causare un'eccezione da un dtor poiché non c'è a) nulla che possiamo fare al riguardo eb) molte risorse libere le operazioni non prevedono nemmeno il controllo degli errori, ad es void gratuito (void * p); .

Gli oggetti con semantica (C), come un oggetto file che deve svuotare con successo i suoi dati o una connessione al database (" scope protetta ") che esegue un commit nel dtor sono di un altro tipo: possiamo facciamo qualcosa per l'errore (a livello di applicazione) e non dovremmo davvero continuare come se nulla fosse successo.

Se seguiamo il percorso RAII e consentiamo oggetti che hanno una semantica (C) nei loro agenti, penso che dovremmo anche consentire il caso strano in cui tali agenti possono lanciare. Ne consegue che non si dovrebbero mettere tali oggetti in contenitori e ne consegue che il programma può ancora terminate () se un commit-dtor genera mentre un'altra eccezione è attiva.


Per quanto riguarda la gestione degli errori (semantica Commit / Rollback) e le eccezioni, si parla bene da uno Andrei Alexandrescu : Gestione degli errori in C ++ / Flusso di controllo dichiarativo (conservato in NDC 2014 )

Nei dettagli, spiega come la libreria Folly implementa un UncaughtExceptionCounter per il loro Strumenti ScopeGuard .

(Devo notare che altri avevano anche idee simili.)

Sebbene il discorso non si concentri sul lancio da un d'tor, mostra uno strumento che può essere usato oggi per sbarazzarsi di problemi con quando lanciare da un d'tor.

Nel futuro , potrebbe essere una caratteristica standard per questo, vedi N3614 , e un discussione al riguardo .

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

  

Note

     

Un esempio in cui viene utilizzato int -returning uncaught_exceptions è ... ... prima   crea un oggetto guardia e registra il numero di eccezioni non rilevate   nel suo costruttore. L'output viene eseguito dall'oggetto guard   distruttore a meno che foo () generi ( nel qual caso il numero di non rilevati   le eccezioni nel distruttore sono maggiori di quelle del costruttore   osservata )

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

Se distruggo un oggetto Foo e il distruttore Foo lancia un'eccezione, cosa posso ragionevolmente farne? Posso registrarlo o posso ignorarlo. È tutto. Non riesco a "correggere" perché l'oggetto Foo è già sparito. Nel migliore dei casi, registro l'eccezione e continuo come se non fosse successo nulla (o ho chiuso il programma). Vale davvero la pena di causare comportamenti indefiniti lanciando da un distruttore?

È pericoloso, ma non ha senso dal punto di vista della leggibilità / comprensione del codice.

Quello che devi chiedere è in questa situazione

int foo()
{
   Object o;
   // As foo exits, o's destructor is called
}

Cosa dovrebbe catturare l'eccezione? Il chiamante di pippo dovrebbe? O dovresti farlo? Perché il chiamante di foo dovrebbe preoccuparsi di qualche oggetto interno a foo? Potrebbe esserci un modo in cui il linguaggio lo definisce per avere un senso, ma sarà illeggibile e difficile da capire.

Ancora più importante, dove va la memoria per Object? Dove va la memoria dell'oggetto posseduto? È ancora assegnato (apparentemente perché il distruttore ha fallito)? Considera anche che l'oggetto era nello spazio di stack , quindi ovviamente è andato a prescindere.

Quindi considera questo caso

class Object
{ 
   Object2 obj2;
   Object3* obj3;
   virtual ~Object()
   {
       // What should happen when this fails? How would I actually destroy this?
       delete obj3;

       // obj 2 fails to destruct when it goes out of scope, now what!?!?
       // should the exception propogate? 
   } 
};

Quando la cancellazione di obj3 fallisce, come posso effettivamente eliminare in modo che non venga garantito? È un mio maledetto ricordo!

Ora considera nel primo frammento di codice L'oggetto scompare automaticamente perché è nello stack mentre Object3 è nell'heap. Dato che il puntatore a Object3 è sparito, sei un po 'SOL. Hai una perdita di memoria.

Ora un modo sicuro per fare le cose è il seguente

class Socket
{
    virtual ~Socket()
    {
      try 
      {
           Close();
      }
      catch (...) 
      {
          // Why did close fail? make sure it *really* does close here
      }
    } 

};

Vedi anche queste FAQ

Dalla bozza ISO per C ++ (ISO / IEC JTC 1 / SC 22 N 4411)

Quindi i distruttori dovrebbero generalmente catturare le eccezioni e non lasciarle propagare fuori dal distruttore.

  

3 Il processo di chiamare distruttori per oggetti automatici costruiti sul percorso da un blocco try a un lancio-     l'espressione è chiamata "svolgitura dello stack". [Nota: se un distruttore chiamato durante lo svolgimento della pila termina con un     eccezione, viene chiamato std :: terminate (15.5.1). Quindi i distruttori dovrebbero generalmente catturare le eccezioni e non lasciarlo     si propagano fuori dal distruttore. - nota finale]

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

Tutti gli altri hanno spiegato perché lanciare distruttori è terribile ... cosa puoi fare al riguardo? Se stai eseguendo un'operazione che potrebbe non riuscire, crea 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 desiderano monitorare l'esito positivo o negativo della pulizia, possono semplicemente chiamare la routine di pulizia esplicita.

Ad 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...
};

In aggiunta alle risposte principali, che sono buone, complete e accurate, vorrei commentare l'articolo a cui fai riferimento: quello che dice "lanciare eccezioni nei distruttori non è così male".

L'articolo prende la linea "quali sono le alternative al lancio di eccezioni" e elenca alcuni problemi con ciascuna delle alternative. Avendolo fatto, si conclude che, poiché non possiamo trovare un'alternativa priva di problemi, dovremmo continuare a gettare eccezioni.

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

Faccio parte del gruppo che ritiene che la "protezione con ambito" " il lancio di modelli nel distruttore è utile in molte situazioni, in particolare per i test unitari. Tuttavia, tieni presente che in C ++ 11, il lancio di un distruttore comporta una chiamata a std :: terminate poiché i distruttori sono implicitamente annotati con noexcept .

Andrzej Krzemie & # 324; ski ha un ottimo post sull'argomento dei distruttori che lanciano:

Sottolinea che C ++ 11 ha un meccanismo per sovrascrivere il noexcept predefinito per i distruttori:

  

In C ++ 11, un distruttore è implicitamente specificato come noexcept . Anche se non aggiungi alcuna specifica e definisci il distruttore in questo modo:

  class MyType {
        public: ~MyType() { throw Exception(); }            // ...
  };
     

Il compilatore aggiungerà comunque in modo invisibile la specifica noexcept al distruttore. Ciò significa che nel momento in cui il distruttore genera un'eccezione, verrà chiamato std :: terminate , anche se non si verifica una doppia eccezione. Se sei davvero determinato a permettere ai tuoi distruttori di lanciare, dovrai specificarlo esplicitamente; hai tre opzioni:

     
      
  • Specifica esplicitamente il distruttore come noexcept (false) ,
  •   
  • Eredita la tua classe da un'altra che già specifica il suo distruttore come noexcept (false) .
  •   
  • Inserisci un membro di dati non statico nella tua classe che specifica già il suo distruttore come noexcept (false) .
  •   

Infine, se decidi di lanciare il distruttore, dovresti sempre essere consapevole del rischio di una doppia eccezione (lanciare mentre lo stack viene srotolato a causa di un'eccezione). Ciò provocherebbe una chiamata a std :: terminate ed è raramente ciò che desideri. Per evitare questo comportamento, puoi semplicemente verificare se esiste già un'eccezione prima di lanciarne una nuova usando std :: uncaught_exception () .

  

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

A: Esistono diverse opzioni:

  1. Lascia che le eccezioni escano dal tuo distruttore, indipendentemente da ciò che accade altrove. E nel fare ciò sii consapevole (o anche spaventato) che std :: terminate può seguire.

  2. Non lasciare mai fluire l'eccezione dal tuo distruttore. Può essere scritto su un registro, se c'è un grosso testo rosso cattivo.

  3. my fave : se std :: uncaught_exception restituisce false, lasciate che le eccezioni escano. Se restituisce true, ricorrere all'approccio di registrazione.

Ma è bello buttare dentro d'tors?

Sono d'accordo con la maggior parte di quanto sopra che il lancio è meglio evitare nel distruttore, dove può essere. Ma a volte è meglio accettare che ciò accada e gestirlo bene. Vorrei scegliere 3 sopra.

Ci sono alcuni casi strani in cui è in realtà una grande idea da lanciare da un distruttore. Come il " deve controllare " codice di errore. Questo è un tipo di valore che viene restituito da una funzione. Se il chiamante legge / controlla il codice di errore contenuto, il valore restituito viene distrutto silenziosamente. Ma , se il codice di errore restituito non è stato letto dal momento in cui i valori di ritorno escono dall'ambito, genererà un'eccezione, dal suo distruttore .

Attualmente seguo la politica (che molti affermano) che le classi non dovrebbero attivamente generare eccezioni dai loro distruttori ma dovrebbero invece fornire un pubblico "chiuso" metodo per eseguire l'operazione che potrebbe non riuscire ...

... ma credo che i distruttori per le classi di tipo contenitore, come un vettore, non debbano mascherare le eccezioni generate dalle classi che contengono. In questo caso, in realtà utilizzo un "libero / vicino" metodo che si chiama ricorsivamente. Sì, ho detto ricorsivamente. C'è un metodo per questa follia. La propagazione delle eccezioni si basa sul fatto che esiste uno stack: se si verifica una singola eccezione, entrambi i distruttori rimanenti continueranno a funzionare e l'eccezione in sospeso si propagherà una volta tornata la routine, il che è fantastico. Se si verificano più eccezioni, allora (a seconda del compilatore) si propagherà la prima eccezione o il programma verrà chiuso, il che va bene. Se si verificano così tante eccezioni che la ricorsione trabocca nello stack, allora qualcosa è seriamente sbagliato e qualcuno lo scoprirà, il che va bene. 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 in relazione al lancio di eccezioni dai loro distruttori.

Martin Ba (sopra) è sulla buona strada, l'architetto è diverso per la logica RELEASE e COMMIT.

Per il rilascio:

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

Per Commit:

Qui è dove vuoi lo stesso tipo di oggetti wrapper RAII che cose come std :: lock_guard forniscono mutex. Con quelli non metti la logica di commit nel diario AT ALL. Hai un'API dedicata, quindi oggetti wrapper che RAII lo commetterà nei LORO medici e gestirà gli errori lì. Ricorda, puoi CATTARE le eccezioni in un distruttore bene; è emetterli che è mortale. Ciò consente anche di implementare criteri e gestione di errori diversi semplicemente creando un wrapper diverso (ad es. Std :: unique_lock vs. std :: lock_guard) e garantisce di non dimenticare di chiamare la logica di commit, che è l'unica metà giustificazione decente per averlo inserito in un dtor al 1 ° posto.

Imposta un evento di allarme. In genere, gli eventi di allarme rappresentano una forma migliore di notifica dell'errore durante la pulizia degli oggetti

A differenza dei costruttori, in cui il lancio di eccezioni può essere un modo utile per indicare che la creazione di oggetti ha avuto successo, le eccezioni non devono essere gettate nei distruttori.

Il problema si verifica quando viene generata un'eccezione da un distruttore durante il processo di svolgimento dello stack. In tal caso, il compilatore si trova in una situazione in cui non sa se continuare il processo di svolgimento dello stack o gestire la nuova eccezione. Il risultato finale è che il tuo programma verrà interrotto immediatamente.

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

  

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

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

Poiché i distruttori sono invocati sia per percorsi normali che eccezionali (fail), essi stessi non possono fallire, altrimenti non riusciamo a fallire ".

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 le modifiche prima di eseguire il commit in una struttura o file di dati esterni. Se la transazione fallisce, è possibile eliminare la struttura di file / dati. Tutto ciò che deve quindi garantire è che il commit delle modifiche da quella struttura / file esterno sia una transazione atomica che non può fallire.

  

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

La soluzione più corretta per me è scrivere la tua 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 di dati per ripulire una struttura di dati esistente, forse potresti cercare di creare in anticipo quella struttura ausiliaria in modo da non dover più crearla all'interno di un distruttore.

Questo è tutto molto più facile a dirsi che a farsi, è vero, ma è l'unico modo davvero corretto che vedo per farlo. A volte penso che dovrebbe esserci la possibilità di scrivere una logica di distruttore separata per percorsi di esecuzione normale lontano da quelli eccezionali, dal momento che a volte i distruttori si sentono un po 'come se avessero il doppio delle responsabilità cercando di gestire entrambi (un esempio sono le guardie dell'ambito che richiedono un licenziamento esplicito non lo richiederebbero se potessero differenziare percorsi di distruzione eccezionali da percorsi non eccezionali.

Il problema finale è che non possiamo non fallire, ed è un problema di progettazione concettuale difficile da risolvere perfettamente in tutti i casi. Diventa più facile se non ti impigli troppo in complesse strutture di controllo con tonnellate di oggetti adolescenti che interagiscono tra loro, e invece modella i tuoi progetti in modo leggermente più voluminoso (esempio: sistema di particelle con un distruttore per distruggere l'intera particella sistema, non un distruttore non banale separato per particella). Quando modellate i vostri progetti a questo tipo di livello più grossolano, avete meno distruttori non banali da affrontare e spesso potete anche permettervi qualsiasi memoria / elaborazione ambientale necessaria per garantire che i vostri distruttori non possano fallire.

E questa è una delle soluzioni più semplici naturalmente è quella di utilizzare i distruttori meno spesso. Nell'esempio di particella sopra, forse dopo aver distrutto / rimosso una particella, si dovrebbero fare alcune cose che potrebbero fallire per qualsiasi motivo. In tal caso, invece di invocare tale logica attraverso il dtor 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 disturbare con 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.

Esistono spesso soluzioni simili a quelle che emergono se si evita di occuparsi di molti oggetti adolescenti con distruttori non banali. Dove puoi rimanere impigliato in un pasticcio in cui sembra quasi impossibile essere eccezionalmente sicuro è quando rimani impigliato in molti oggetti per adolescenti che hanno tutti dottori non banali.

Sarebbe molto utile se nothrow / noexcept si traducesse effettivamente in un errore del compilatore se qualcosa che lo specifica (incluse le funzioni virtuali che dovrebbero ereditare la specifica noexcept della sua classe base) tentasse di invocare qualsiasi cosa potesse lanciare. In questo modo saremmo in grado di catturare tutte queste cose in fase di compilazione se in realtà scrivessimo inavvertitamente un distruttore che potrebbe lanciare.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top