gettando eccezioni da un distruttore


257

La maggior parte delle persone afferma di non gettare mai un'eccezione da un distruttore: farlo comporta un comportamento indefinito. Stroustrup sottolinea che "il distruttore vettoriale richiama esplicitamente il distruttore per ogni elemento. Ciò implica che se un distruttore elemento lancia, la distruzione vettoriale non riesce ... Non c'è davvero un buon modo per proteggere dalle eccezioni lanciate dai distruttori, quindi la libreria non fornisce alcuna garanzia in caso di lancio di un elemento distruttore "(dall'appendice E3.2) .

Questo articolo sembra dire il contrario - che i lanciatori distruttori vanno più o meno bene.

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? Se si tratta di un errore potenzialmente gestibile nello stack ma non proprio nel distruttore, non ha senso gettare un'eccezione dal distruttore?

Ovviamente questi tipi di errori sono rari, ma possibili.


36
"Due eccezioni alla volta" è una risposta di borsa ma non è la vera ragione. La vera ragione è che un'eccezione dovrebbe essere generata se e solo se le postcondizioni di una funzione non possono essere soddisfatte. Il postcondizionamento di un distruttore è che l'oggetto non esiste più. Questo non può non accadere. Qualsiasi operazione di fine vita soggetta a guasti deve pertanto essere chiamata come metodo separato prima che l'oggetto esca dall'ambito di applicazione (le funzioni sensibili normalmente hanno comunque solo un percorso di successo).
spray

29
@spraff: sei consapevole che ciò che hai detto implica "buttare via RAII"?
Kos,

16
@spraff: dover chiamare "un metodo separato prima che l'oggetto esca dall'ambito" (come hai scritto) in realtà getta via RAII! Il codice che utilizza tali oggetti dovrà garantire che tale metodo venga chiamato prima che venga chiamato il distruttore. Infine, questa idea non aiuta affatto.
Frunsi,

8
@Frunsi no, perché questo problema deriva dal fatto che il distruttore sta cercando di fare qualcosa al di là del semplice rilascio di risorse. È allettante dire "Voglio sempre finire con XYZ" e pensare che questo sia un argomento per mettere tale logica nel distruttore. No, non essere pigro, scrivi xyz()e mantieni pulito il distruttore dalla logica non RAII.
spruzzo l'

6
@Frunsi Ad esempio, commettere qualcosa su un file non è necessariamente OK nel distruttore di una classe che rappresenta una transazione. Se il commit ha avuto esito negativo, è troppo tardi per gestirlo quando tutto il codice coinvolto nella transazione è uscito dall'ambito. Il distruttore dovrebbe scartare la transazione a meno che non commit()venga chiamato un metodo.
Nicholas Wilson,

Risposte:


198

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.

Pertanto, 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 a loro non importa, 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 ++"
Articolo 11: Impedire alle eccezioni di lasciare distruttori


5
"A meno che non ti dispiaccia potenzialmente terminare l'applicazione, probabilmente dovresti ingoiare l'errore." - questa dovrebbe probabilmente essere l'eccezione (scusate il gioco di parole) piuttosto che la regola - cioè, fallire velocemente.
Erik Forbes,

15
Non sono d'accordo. La chiusura del programma interrompe lo svolgimento dello stack. Non verrà chiamato più distruttore. Qualsiasi risorsa aperta verrà lasciata aperta. Penso che ingoiare l'eccezione sarebbe l'opzione preferita.
Martin York,

20
Il sistema operativo può ripulire le risorse di cui è il proprietario. Memoria, FileHandles ecc. Che dire di risorse complesse: connessioni DB. Quel collegamento verso l'ISS che hai aperto (invierà automaticamente le connessioni chiuse)? Sono sicuro che la NASA vorrebbe che tu chiudessi la connessione in modo pulito!
Martin York,

7
Se un'applicazione sta per "fallire rapidamente" interrompendo, in primo luogo non dovrebbe generare eccezioni. Se fallisce restituendo il controllo allo stack, non dovrebbe farlo in un modo che potrebbe causare l'interruzione del programma. Uno o l'altro, non scegliere entrambi.
Tom,

2
@LokiAstari Il protocollo di trasporto che stai utilizzando per comunicare con un veicolo spaziale non è in grado di gestire una connessione interrotta? Ok ...
doug65536,

54

Il lancio di un distruttore può provocare un arresto, poiché questo distruttore potrebbe essere chiamato come parte di "Svolgimento dello stack". 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 è stata lanciata l'eccezione, verranno terminati -> verranno chiamati i loro distruttori. 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.


1
puoi per favore elaborare come è stato chiamato abort () nella situazione sopra. Significa che il controllo dell'esecuzione era ancora con il compilatore C ++
Krishna Oza, il

1
@Krishna_Oza: Abbastanza semplice: ogni volta che viene generato un errore, il codice che genera un errore controlla un po 'che indica che il sistema di runtime è in fase di svolgimento dello stack (ovvero, gestendo alcuni altri throwma non aver ancora trovato un catchblocco per esso) nel qual caso viene chiamato std::terminate(no abort) invece di sollevare una (nuova) eccezione (o continuare a svolgere lo stack).
Marc van Leeuwen,

53

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

  • (R) rilascia la semantica (ovvero libera quella memoria)
  • (C) commit semantica (aka scarica il 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 poiché non c'è a) nulla che possiamo fare al riguardo eb) molte operazioni a risorse libere non lo fanno prevedere anche il controllo degli errori, ad es .void free(void* p);

Gli oggetti con semantica (C), come un oggetto file che deve svuotare correttamente i suoi dati o una connessione al database ("portata protetta") che esegue un commit nel dtor sono di un tipo diverso: possiamo fare qualcosa riguardo all'errore (on livello di applicazione) e non dovremmo davvero continuare come se non fosse successo nulla.

Se seguiamo il percorso RAII e consentiamo oggetti che hanno una semantica (C) nei loro agenti, penso che dovremmo anche consentire lo strano caso 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()lanciare un commit-dtor mentre è attiva un'altra eccezione.


Per quanto riguarda la gestione degli errori (semantica Commit / Rollback) e le eccezioni, c'è un buon discorso di un Andrei Alexandrescu : Gestione degli errori nel flusso di controllo dichiarativo + C ++ (tenuto a NDC 2014 )

Nei dettagli, spiega in che modo la libreria Folly implementa una UncaughtExceptionCounterper i loro ScopeGuardstrumenti.

(Dovrei notare che anche altri avevano idee simili.)

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

In futuro , potrebbe esserci una funzionalità std per questo, vedere N3614 , e una discussione al riguardo .

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

Appunti

Un esempio in cui viene utilizzato int-returning uncaught_exceptionsè ... ... per prima cosa 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 non venga lanciato foo () ( nel qual caso il numero di eccezioni non rilevate nel distruttore è maggiore di quello osservato dal costruttore )


6
Altamente d'accordo. E aggiungendo un'altra semantica di rollback semantico (Ro). Utilizzato comunemente nella protezione dell'ambito. Come nel caso del mio progetto in cui ho definito una macro ON_SCOPE_EXIT. Il caso della semantica di rollback è che qui potrebbe accadere qualcosa di significativo. Quindi non dovremmo davvero ignorare l'errore.
Weipeng L

Sento che l'unica ragione per cui abbiamo commesso la semantica nei distruttori è che il C ++ non supporta finally.
user541686

@Mehrdad: finally è un dtor. Si chiama sempre, non importa quale. Per l'approssimazione sintattica di infine, vedere le varie implementazioni scope_guard. Al giorno d'oggi, con la macchina in atto (anche nello standard, è C ++ 14?) Per rilevare se il dtor è autorizzato a lanciare, può anche essere reso totalmente sicuro.
Martin Ba,

1
@MartinBa: Penso che ti sia sfuggito il punto del mio commento, il che è sorprendente dato che ero d' accordo con la tua idea che (R) e (C) sono diversi. Stavo cercando di dire che un dtor è intrinsecamente uno strumento per (R) ed finallyè intrinsecamente uno strumento per (C). Se non vedi perché: considera perché è legittimo gettare eccezioni l'una sull'altra in finallyblocchi e perché lo stesso non vale per i distruttori. (In un certo senso, è una questione di dati rispetto al controllo . I distruttori sono per il rilascio di dati, finallyè per il rilascio del controllo. Sono diversi; è un peccato che C ++ li
colleghi

1
@Mehrdad: passare troppo tempo qui. Se vuoi, puoi sviluppare i tuoi argomenti qui: programmers.stackexchange.com/questions/304067/… . Grazie.
Martin Ba,

21

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 Foooggetto e il Foodistruttore lancia un'eccezione, cosa posso ragionevolmente farne? Posso registrarlo o posso ignorarlo. È tutto. Non posso "ripararlo", perché l' Foooggetto è già sparito. Nel migliore dei casi, registro l'eccezione e continuo come se nulla fosse (o termino il programma). Vale davvero la pena causare potenzialmente comportamenti indefiniti lanciando da un distruttore?


11
Ho appena notato ... lanciare da un dtor non è mai un comportamento indefinito. Certo, potrebbe chiamare terminate (), ma questo è un comportamento molto ben specificato.
Martin Ba,

4
std::ofstreamIl distruttore arrossisce e quindi chiude il file. Durante lo svuotamento potrebbe verificarsi un errore completo del disco, con il quale puoi assolutamente fare qualcosa di utile: mostra all'utente una finestra di dialogo di errore che dice che il disco non ha spazio libero.
Andy,

13

È 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 cogliere 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 dello stack , quindi ovviamente è andato a prescindere.

Quindi prendere in considerazione 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 cancellare in un modo che è garantito per non fallire? È 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


Risorgendo questa risposta, ri: il primo esempio, circa int foo(), è possibile utilizzare un blocco funzione-try-block per avvolgere l'intera funzione foo in un blocco try-catch, compresi catturare i distruttori, se si è interessati a farlo. Non è ancora l'approccio preferito, ma è una cosa.
tyree731,

13

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 chiamata dei distruttori per oggetti automatici costruiti sul percorso da un blocco try a un'espressione di lancio è chiamato "impilamento dello stack". [Nota: se un distruttore chiamato durante lo svolgimento dello stack esce con un'eccezione, viene chiamato std :: terminate (15.5.1). Quindi i distruttori dovrebbero generalmente catturare le eccezioni e non lasciarle propagare fuori dal distruttore. - nota finale]


1
Non ha risposto alla domanda: l'OP è già a conoscenza di questo.
Arafangion,

2
@Arafangion Dubito che fosse a conoscenza di questo (std :: terminate essere chiamato) poiché la risposta accettata ha fatto esattamente lo stesso punto.
lothar,

@Arafangion come in alcune risposte qui alcune persone hanno detto che abort () viene chiamato; O è che std :: terminate a sua volta chiama la funzione abort ().
Krishna Oza,

7

Il distruttore potrebbe essere eseguito 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.


7

Faccio parte del gruppo che ritiene che il lancio del modello "guarded scope" nel distruttore sia utile in molte situazioni, in particolare per i test unitari. Tuttavia, tenere presente che in C ++ 11, il lancio di un distruttore comporta una chiamata std::terminatepoiché i distruttori sono implicitamente annotati noexcept.

Andrzej Krzemieński ha un ottimo post sul tema dei distruttori che lanciano:

Sottolinea che C ++ 11 ha un meccanismo per sovrascrivere il valore predefinito noexceptper 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 invisibilmente le specifiche noexceptal distruttore. E questo significa che nel momento in cui il tuo distruttore lancia un'eccezione, std::terminateverrà chiamato, anche se non ci fosse 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 specifica già 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 std::terminateed è raramente ciò che si desidera. Per evitare questo comportamento, puoi semplicemente verificare se esiste già un'eccezione prima di lanciarne uno nuovo usando std::uncaught_exception().


6

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.

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

Sto cercando una soluzione, ma stanno cercando di spiegare cosa è successo e perché. Voglio solo chiarire se la funzione di chiusura viene chiamata all'interno del distruttore?
Jason Liu,

5

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

L'articolo prende la linea "quali sono le alternative al lancio di eccezioni" ed elenca alcuni problemi con ciascuna delle alternative. Fatto ciò, 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 è tanto grave quanto il comportamento delle eccezioni, che, ricordiamo, è "comportamento indefinito del tuo programma". Alcune delle obiezioni dell'autore includono "esteticamente brutto" e "incoraggiano il cattivo stile". Ora quale preferiresti avere? Un programma con cattivo stile o che mostrava comportamenti indefiniti?


1
Comportamento non indefinito, ma piuttosto risoluzione immediata.
Marc van Leeuwen,

Lo standard dice "comportamento indefinito". Tale comportamento è spesso terminato ma non sempre.
DJClayworth,

No, leggi [cept.terminate] in Gestione eccezioni-> Funzioni speciali (che è 15.5.1 nella mia copia dello standard, ma la sua numerazione è probabilmente obsoleta).
Marc van Leeuwen,

2

D: 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?

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 può seguire std :: terminate.

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

  3. my fave : se std::uncaught_exceptionrestituisce false, lascia che le eccezioni vengano emesse. Se restituisce true, ricorrere all'approccio di registrazione.

Ma è bello buttare dentro d'tors?

Concordo 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à è un'ottima idea lanciare da un distruttore. Come il codice di errore "deve controllare". 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. Tuttavia , se il codice di errore restituito non è stato letto quando i valori restituiti non rientrano nell'ambito di applicazione, genererà un'eccezione dal suo distruttore .


4
Il tuo preferito è qualcosa che ho provato di recente, e risulta che non dovresti farlo. gotw.ca/gotw/047.htm
GManNickG

1

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

... ma credo che i distruttori per le classi di tipo container, come un vettore, non debbano mascherare le eccezioni generate dalle classi che contengono. In questo caso, in realtà utilizzo un metodo "free / close" 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 dallo stack, allora qualcosa è seriamente sbagliato e qualcuno lo scoprirà, il che va bene. Personalmente,

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.


1

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 programma verrà terminato immediatamente.

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


1
La scrittura di un messaggio nel file di registro può causare un'eccezione.
Konard,

1

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

Per il rilascio:

Dovresti mangiare degli errori. Stai liberando memoria, chiudendo le connessioni, ecc. Nessun altro nel sistema dovrebbe mai rivedere 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 metterlo in un dtor al 1 ° posto.


1

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?

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 per percorsi normali ed eccezionali (falliti), essi stessi non possono fallire, altrimenti stiamo "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 le modifiche prima di eseguire il commit in una struttura di dati o file 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 quella di assicurarsi che le possibilità di fallire in caso di fallimento siano astronomicamente improbabili, dal momento che rendere impossibili fallire le cose 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, devo ammetterlo, 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 gestire e spesso potete anche permettervi qualsiasi memoria / elaborazione ambientale necessaria per assicurare 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 dei dottori non banali.

Sarebbe molto utile se nothrow / noexcept fosse effettivamente tradotto 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 invocare qualsiasi cosa che potesse essere lanciata. In questo modo saremmo in grado di catturare tutta questa roba in fase di compilazione se in realtà scrivessimo inavvertitamente un distruttore che potrebbe lanciare.


1
La distruzione è fallita adesso?
curioso

Penso che significhi che i distruttori vengono chiamati durante un fallimento, per ripulire quell'errore. Quindi, se viene chiamato un distruttore durante un'eccezione attiva, non riesce a ripulire da un errore precedente.
user2445507

0

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

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.