Qual'è la differenza concettuale tra finally e un destructor?


12

Innanzitutto, sono consapevole del perché non esiste un costrutto "finalmente" in C ++? ma una lunga discussione di commenti su un'altra domanda sembra giustificare una domanda separata.

A parte il problema che finallyin C # e Java possono praticamente esistere solo una volta (== 1) per ambito e un singolo ambito può avere più (== n) distruttori C ++, penso che siano essenzialmente la stessa cosa. (Con alcune differenze tecniche.)

Tuttavia, un altro utente ha sostenuto :

... Stavo cercando di dire che un dtor è intrinsecamente uno strumento per (Release sematics) e infine è intrinsecamente uno strumento per (Commit semantics). Se non vedi perché: considera perché è legittimo gettare eccezioni l'una sull'altra in blocchi finalmente, 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 dei dati, infine è per il rilascio del controllo. Sono diversi; è un peccato che C ++ li colleghi insieme.)

Qualcuno può chiarire questo?

Risposte:


6
  • Transazione ( try)
  • Uscita / risposta errore ( catch)
  • Errore esterno ( throw)
  • Errore programmatore ( assert)
  • Rollback (la cosa più vicina potrebbe essere la protezione dell'ambito in lingue che li supportano in modo nativo)
  • Rilascio di risorse (distruttori)
  • Flusso di controllo indipendente dalle varie transazioni ( finally)

Non è possibile fornire una descrizione migliore del finallyflusso di controllo indipendente dalle transazioni varie. Non si associa necessariamente in modo così diretto a nessun concetto di alto livello nel contesto di una mentalità di transazione e di recupero degli errori, specialmente in un linguaggio teorico che ha sia distruttori che finally.

Quello che mi manca di più intrinsecamente è una caratteristica del linguaggio che rappresenta direttamente il concetto di rollback degli effetti collaterali esterni. Le protezioni di portata in lingue come la D sono la cosa più vicina a cui riesco a pensare che si avvicina a rappresentare quel concetto. Da un punto di vista del flusso di controllo, un rollback nell'ambito di una particolare funzione dovrebbe distinguere un percorso eccezionale da uno normale, automatizzando simultaneamente il rollback implicitamente di eventuali effetti collaterali causati dalla funzione nel caso in cui la transazione fallisca, ma non quando la transazione ha esito positivo . È abbastanza facile da fare con i distruttori se, diciamo, impostiamo un valore booleano come succeededtrue al termine del nostro blocco try per impedire la logica di rollback in un distruttore. Ma è un modo piuttosto indiretto per farlo.

Sebbene ciò possa sembrare che non risparmierebbe molto, l'inversione degli effetti collaterali è una delle cose più difficili da correggere (es: ciò che rende così difficile scrivere un contenitore generico sicuro dalle eccezioni).


4

In un certo senso lo sono - allo stesso modo in cui una Ferrari e un transito possono essere entrambi usati per strofinare i negozi per una pinta di latte anche se sono progettati per usi diversi.

È possibile inserire un costrutto try / finally in ogni ambito e ripulire tutte le variabili definite nell'ambito nel blocco finally per emulare un distruttore C ++. Questo è, concettualmente, ciò che fa C ++: il compilatore chiama automaticamente il distruttore quando una variabile esce dall'ambito (cioè alla fine del blocco dell'ambito). Dovresti organizzare il tuo tentativo / finalmente, quindi il tentativo è la prima cosa e, infine, l'ultima cosa in ogni ambito. Dovresti anche definire uno standard per ogni oggetto per avere un metodo specifico che utilizzi per ripulire il suo stato che chiameresti nel blocco finally, anche se immagino che potresti lasciare la normale gestione della memoria fornita dalla tua lingua ripulisci l'oggetto ora svuotato quando lo desidera.

Non sarebbe carino farlo, e anche se .NET ha introdotto IDispose come un distruttore gestito manualmente, e usando i blocchi nel tentativo di rendere leggermente più semplice la gestione manuale, non è ancora qualcosa che vorresti fare in pratica .


4

Dal mio punto di vista, la differenza principale è che un distruttore in c ++ è un meccanismo implicito (invocato automaticamente) per rilasciare risorse allocate mentre il tentativo ... finalmente può essere usato come meccanismo esplicito per farlo.

Nei programmi c ++ il programmatore è responsabile del rilascio delle risorse allocate. Questo è di solito implementato nel distruttore di una classe e fatto immediatamente quando una variabile esce dall'ambito o o quando viene chiamato delete.

Quando in c ++ viene creata una variabile locale di una classe senza utilizzare newle risorse di tali istanze liberate implicite dal distruttore in presenza di un'eccezione.

// c++
void test() {
    MyClass myClass(someParameter);
    // if there is an exception the destructor of MyClass is called automatically
    // this does not work with
    // MyClass* pMyClass = new MyClass(someParameter);

} // on test() exit the destructor of myClass is implicitly called

In java, c # e altri sistemi con una gestione automatica della memoria, il garbage collector dei sistemi decide quando un'istanza di classe viene distrutta.

// c#
void test() {
    MyClass myClass = new MyClass(someParameter);
    // if there is an exception myClass is NOT destroyed so there may be memory/resource leakes

    myClass.destroy(); // this is never called
}

Non vi è alcun meccanismo implicito in tal senso, quindi è necessario programmarlo esplicitamente utilizzando Try Infine

// c#
void test() {
    MyClass myClass = null;

    try {
        myClass = new MyClass(someParameter);
        ...
    } finally {
        // explicit memory management
        // even if there is an exception myClass resources are freed
        myClass.destroy();
    }

    myClass.destroy(); // this is never called
}

In C ++, perché il distruttore viene chiamato automaticamente solo con un oggetto stack e non con un oggetto heap in caso di eccezione?
Giorgio,

@Giorgio Perché le risorse dell'heap vivono in uno spazio di memoria che non è direttamente collegato allo stack di chiamate. Ad esempio, immagina un'applicazione multithread con 2 thread Ae B. Se viene lanciato un thread, il rollback della A'stransazione non dovrebbe distruggere le risorse allocate B, ad esempio: gli stati del thread sono indipendenti l'uno dall'altro e la memoria persistente presente nell'heap è indipendente da entrambi. Tuttavia, in genere in C ++, la memoria heap è ancora legata agli oggetti nello stack.

@Giorgio Ad esempio, un std::vectoroggetto potrebbe vivere nello stack ma puntare alla memoria sullo heap - sia l'oggetto vettoriale (sullo stack) che il suo contenuto (sullo heap) verrebbero deallocati durante uno svolgimento dello stack in quel caso, poiché distruggere il vettore nello stack invocherebbe un distruttore che libera la memoria associata sull'heap (e allo stesso modo distruggerebbe quegli elementi heap). In genere per la sicurezza delle eccezioni, la maggior parte degli oggetti C ++ vive nello stack, anche se sono solo maniglie che puntano alla memoria sull'heap, automatizzando il processo di liberazione della memoria sia dello heap che dello stack nello svolgimento dello stack.

4

Sono contento che tu l'abbia pubblicato come una domanda. :)

Stavo cercando di dire che i distruttori e finallysono concettualmente diversi:

  • I distruttori sono per il rilascio di risorse ( dati )
  • finallyè per tornare al chiamante ( controllo )

Considera, diciamo, questo ipotetico pseudo-codice:

try {
    bar();
} finally {
    logfile.print("bar has exited...");
}

finallyqui sta risolvendo interamente un problema di controllo e non un problema di gestione delle risorse.
Non avrebbe senso farlo in un distruttore per una serie di motivi:

  • Nessuna cosa viene "acquisita" o "creata"
  • La mancata stampa nel file di registro non comporterà perdite di risorse, corruzione dei dati, ecc. (Presupponendo che il file di registro qui non venga reinserito nel programma altrove)
  • È legittimo logfile.printfallire, mentre la distruzione (concettualmente) non può fallire

Ecco un altro esempio, questa volta come in Javascript:

var mo_document = document, mo;
function observe(mutations) {
    mo.disconnect();  // stop observing changes to prevent re-entrance
    try {
        /* modify stuff */
    } finally {
        mo.observe(mo_document);  // continue observing (conceptually, this can fail)
    }
}
mo = new MutationObserver(observe);
return observe();

Nell'esempio sopra, ancora una volta, non ci sono risorse da rilasciare.
In effetti, il finallyblocco sta acquisendo risorse internamente per raggiungere il suo obiettivo, che potrebbe potenzialmente fallire. Quindi, non ha senso usare un distruttore (se Javascript ne aveva uno).

D'altra parte, in questo esempio:

b = get_data();
try {
    a.write(b);
} finally {
    free(b);
}

finallysta distruggendo una risorsa, b. È un problema di dati. Il problema non è di restituire in modo pulito il controllo al chiamante, ma piuttosto di evitare perdite di risorse.
Il fallimento non è un'opzione e non dovrebbe (concettualmente) mai accadere.
Ogni versione di bè necessariamente associata a un'acquisizione e ha senso usare RAII.

In altre parole, solo perché è possibile utilizzare sia per simulare sia ciò non significa che entrambi siano lo stesso problema o che entrambi siano soluzioni appropriate per entrambi i problemi.


Grazie. Non sono d'accordo, ma ehi :-) Penso che nei prossimi giorni sarò in grado di aggiungere una risposta visiva completamente opposta ...
Martin Ba

2
In che modo il fatto che finallyviene principalmente utilizzato per rilasciare risorse (non di memoria) è determinante in questo?
Bart van Ingen Schenau,

1
@BartvanIngenSchenau: non ho mai sostenuto che qualsiasi lingua attualmente esistente abbia una filosofia o implementazione che corrisponda a ciò che ho descritto. Le persone non hanno ancora finito di inventare tutto ciò che potrebbe esistere ancora. Ho solo sostenuto che ci sarebbe valore nel separare le due nozioni in quanto sono idee diverse e hanno casi d'uso diversi. Per soddisfare la tua curiosità, credo che D abbia entrambi. Probabilmente ci sono anche altre lingue. Non lo considero rilevante, e non mi potrebbe importare di meno perché, ad esempio, Java fosse favorevole finally.
user541686

1
Un esempio pratico che ho riscontrato in JavaScript sono le funzioni che cambiano temporaneamente il puntatore del mouse in una clessidra durante alcune operazioni lunghe (che potrebbero generare un'eccezione), e poi riportarlo alla normalità nella finallyclausola. La visione del mondo C ++ introdurrebbe una classe che gestisce questa "risorsa" di un'assegnazione a una variabile pseudo-globale. Che senso concettuale ha questo? Ma i distruttori sono il martello di C ++ per l'esecuzione del codice di fine blocco richiesto.
dan04

1
@ dan04: Grazie mille, questo è l'esempio perfetto per questo. Potrei giurare che mi sarei imbattuto in così tante situazioni in cui la RAII non aveva senso, ma ho avuto difficoltà a pensarci.
user541686

1

La risposta di k3b la esprime davvero bene:

un distruttore in c ++ è un meccanismo implicito (invocato automaticamente) per rilasciare risorse allocate mentre il tentativo ... finalmente può essere usato come meccanismo esplicito per farlo.

Per quanto riguarda le "risorse", mi piace fare riferimento a Jon Kalb: RAII dovrebbe significare che l'acquisizione di responsabilità è inizializzazione .

Ad ogni modo, per quanto riguarda implicito vs esplicito, questo sembra davvero essere:

  • Un agente è uno strumento per definire quali operazioni devono avvenire - implicitamente - al termine della vita di un oggetto (che spesso coincide con la fine del campo di applicazione)
  • Un blocco di fine è uno strumento per definire - esplicitamente - quali operazioni devono avvenire a fine ambito.
  • Inoltre, tecnicamente, puoi sempre lanciare da finalmente, ma vedi sotto.

Penso che sia tutto per la parte concettuale, ...


... ora ci sono alcuni dettagli interessanti di IMHO:

Inoltre, non credo che il c'tor / d'tor debba concettualmente "acquisire" o "creare" qualsiasi cosa, a parte la responsabilità di eseguire del codice nel distruttore. Ed è quello che alla fine fa anche: esegui del codice.

E mentre il codice in un blocco finalmente può certamente generare un'eccezione, non è abbastanza distinzione per me dire che sono concettualmente diversi tra esplicito e implicito.

(Inoltre, non sono affatto convinto che il codice "buono" debba finalmente essere lanciato - forse questa è un'altra intera domanda a sé.)


Cosa ne pensi del mio esempio Javascript?
user541686

Per quanto riguarda i tuoi altri argomenti: "Vorremmo davvero registrare la stessa cosa senza distinzione?" Sì, è solo un esempio e ti manca un po 'il punto, e sì, nessuno ha mai proibito di registrare dettagli più specifici per ogni caso. Il punto qui è che non puoi certo affermare che non c'è mai una situazione in cui vorresti registrare qualcosa che è comune a entrambi. Alcune voci del registro sono generiche, altre sono specifiche; vuoi entrambi. E ancora una volta, ti stai perdendo completamente il punto concentrandoti sulla registrazione. Motivare esempi di 10 righe è difficile; per favore prova a non perdere il punto.
user541686

Non hai mai affrontato questi ...
user541686

@Mehrdad - Non ho affrontato il tuo esempio javascript perché mi ci vorrebbe un'altra pagina per discutere di ciò che penso. (Ci ho provato, ma mi ci è voluto così tanto tempo per dire qualcosa di coerente che l'ho saltato :-)
Martin Ba

@Mehrdad - come per gli altri tuoi punti - sembra che dovremmo essere d'accordo su non essere d'accordo. Vedo a cosa stai mirando con la differenza, ma non sono convinto che siano qualcosa di concettualmente diverso: Principalmente perché sono principalmente nel campo che pensa di lanciare finalmente è una pessima idea ( nota : anche io pensa nel tuo observeresempio che lanciare ci sarebbe una pessima idea.) Sentiti libero di aprire una chat, se vuoi discuterne ulteriormente. È stato sicuramente divertente pensare ai tuoi argomenti. Saluti.
Martin Ba,
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.