Newsgroup: it.comp.lang.c++
Titolo: Distruttori

Mi potete cortesemente spiegare che senso ha prevedere un distruttore in un
oggetto nel quale non è prevista l'allocazione dinamica della memoria?
Comunque sia quando finisce lo scope dell'oggetto,l'oggetto stesso viene
distrutto con un distruttore di default,no?!?
Un'altra cosa che non capisco è questa:
Perchè se chiamo il distruttore dell'oggetto prima che finisca la sua
visibilità questo non viene distrutto,a meno che non ci sia memoria allocata
dinamicamente?
Grazie
Ciao
Francesco

 Francesco
Il distruttore non distrugge fisicamente la memoria allocata per un oggetto:
semplicemente la rende memoria non inizializzata (raw memory) e si preoccupa
di fornire implicitamente al compilatore la dimensione totale dei suoi dati
membro non dinamici, affinchè questo possa poi deallocare la memoria.
Chiamare direttamente il distruttore di una classe è un'operazione che
andrebbe sempre evitata. Questo perchè la maggiorparte delle classi di noi
programmatori mortali (nonchè quasi tutte quelle della libreria standard)
assumono che il distruttore distrugga l'oggetto definitivamente e per
l'ultima volta. Oltretutto, come fai a costruire nuovamente l'oggetto così
distrutto? E anche se dovessi riuscirci non faresti comunque una cosa
completamente sicura.
Ti consiglio, piuttosto, di utilizzare l'algoritmo "swap&destroy", che fà
una cosa simile ma in maniera COMPLETAMENTE SICURA. Purtroppo non ho molto
tempo per descrivertelo, così ti scrivo un frammento di codice sperando che
sia chiaro.
class A {
public:
  A() { /* un sacco di istruzioni */ }
  ~A() { /* un sacco di istruzioni */ }
  /* un sacco di dati membro */
private:
  void Swap(A& other) {
     /* scambia i dati membro di *this e other */
     std::swap(membro_1, other.membro_1);
     std::swap(membro_2, other.membro_2);
     /* [...] */
  }
public:
  void Reset() {
     /* distrugge e ricostruisce l'oggetto */
     A tmp; // costruisce un nuovo oggetto A
     Swap(tmp); // scambia tutti i dati di *this con quelli di tmp
  } // Uscita dallo scope: tmp, che contiene i dati di *this, viene
distrutto
};
Ormai ci sono e quindi te lo spiego: la funzione Reset ha il compito di
distruggere l'oggetto e poi ricostruirlo. Per farlo crea un oggetto
temporaneo di tipo A, che è l'oggetto che ci serve. Dopodichè scambia TUTTI
i dati membro del nostro oggetto con quelli dell'oggetto temporaneo. A
questo punto il nostro oggetto è A TUTTI GLI EFFETTI una copia dell'oggetto
tmp, il quale a sua volta è una copia del vecchio stato del nostro oggetto.
Così, quando tmp esce dallo scope, noi abbiamo eliminato completamente ogni
traccia del nostro vecchio oggetto.
Ora scusa ma scappo... Byez!

oggetto nel quale non è prevista l'allocazione dinamica della memoria?
Per esempio potrebbe servire a chiudere un file tenuto aperto durante
la sessione di lavoro, oppure chiudere una sessione tcp, o un
collegamento via seriale con un modem, oppure scrivere su un log che
il componente ha terminato il suo lavoro, ecc..ecc...
Non sta scritto da nessuna parte che il distruttore di una classe
debba solamente distruggere la memoria allocata.
Il Ghenio
---------
// TODO: Place your ass here


O anche a garantirsi che una determinata funzione venga eseguita sia
quando l'esecuzione procede normalmente sia quando viene lanciata
un'eccezione.
Ciao,
Nicola

 };
Francamente prevedere un metodo di Reset mi pare un errore di disegno,
indipendente dal fatto che sia sicuro o meno.
Ciao,
Nicola

 Come fa a sapere quanto deallocare?
Non mi sembra che il tuo controesempio sia molto pertinente... qui si
parla di deallocazione di classi polimorfiche... un char non è una
classe e di conseguenza non c'è traccia di polimorfismo nel tuo
codice.
Invece le cose stanno come dice AG5F... Se posso permettermi ti
consiglio la lettura di Herb Sutter con i suoi "Exceptional C++" e
"More Exceptional C++".

Se serve avere una funzione che resetti l'oggetto non vedo niente di
male nell'averlo e sicuramente l'implementazione data è molto sicura
rispetto alle eccezioni, sicuramente la funzione swap è molto utile
per, ancora, una implementazione sicura del costruttore di copia e
dell'operatore =.
Saluti,
   Mirko.

 "More Exceptional C++".
Questi sono due bei libri ed Herb e' una persona in gamba (e' pure molto
simpatico), ma questo e' un problema di fondamenti dell'informatica, non
di C++ avanzato.

E' il concetto che un oggetto possa aver bisogno di esser resettato che
e' un errore fondamentale di disegno; se un oggetto rappresenta
un'entita' il suo lifetime deve coincidere con il comportamento
dell'entita' che rappresenta. Ed in natura sono pochini i casi di
entita' che muoiono e risorgono.
La tecnica corretta e' limitare lo scope dell'oggetto in questione ed
usarne un altro.
Ciao,
Nicola

 memoria. Tuttavia a noi serve la dimensione di D, e non quella di B:
In realtà, il problema non riguarda le dimensioni dell'oggetto, bensì la
possibilità di capire se il puntatore alla classe base identifica un
oggetto B a sè stante, o un oggetto B che è parte di un oggetto D, o uno
che è parte di un'altro tipo di oggetto.  Questo è cruciale, perché se nel
caso di derivazione singola il puntatore a B può facilmente avere lo
stesso valore del puntatore a D, nel caso di derivazione multipla ciò
varrà al massimo per una delle classi basi.
In sostanza, al momento del delete deve essere possibile
  - risalire all'indirizzo dell'oggetto D 
    (conoscendo l'indirizzo dell'oggetto B)
  - conoscere quale distruttore invocare
  - conoscere quale operator delete invocare 
    (D potrebbe averlo ridefinito)
Nessuno di questi punti riguarda le dimensioni di D. Del resto, operator
delete non riceve altri argomenti oltre al void* che identifica il blocco
da deallocare.
Max

 programmatori "immortali" fanno cosi'.
In caso di allocazione in aree di memoria non direttamente gestite dal
gestore standard è necessario chiamare esplicitamente il distruttore.
Ciao,
Fulvio
--
*---------------------------------------
* Fulvio Esposito
* E-Mail: 
ful...@libero.it
* WWW: 
http://digilander.iol.it/fesposito
*---------------------------------------

 gestore standard è necessario chiamare esplicitamente il distruttore.
In generale sono d'accordo, ma e' bene separare totalmente i contesti:
un conto e' scrivere gestori di memoria che soddisfano esigenze
particolari, un altro prevedere come comportamento di una classe
l'azzeramento di una sua istanza.
Ciao,
Nicola

 nella classe derivata.
Questo è quello che accade con molti compilatori, almeno nel caso di
derivazione singola. Ma non dimentichiamo che per lo Standard in questi
casi il comportamento è indefinito.
Inoltre, come ho scritto nell'altro messaggio, il problema non è solo
invocare il distruttore giusto, ma anche usare l'operator delete
appropriato, visto che la classe derivata potrebbe averlo ridefinito.
Max

 da deallocare.
Pero' in questo caso non mi sembra che si parli di un delete a void* (anche
perche' tale tipo di conversione implicita in C++ non esiste).
Bensi' di un delete al tipo B, che viene derivato. In questo caso non viene
utilizzato l'operatore globale delete(void*), ma ne viene utilizzato uno
costruito ad hoc dal compilatore (B::delete).
Laddove il compilatore non puo' essere in grado di risalire al tipo
effettivo dell'oggetto che vogliamo distruggere, e' necessario "aiutarlo"
utilizzando un distruttore virtuale rimandando il check a run-time.
Quindi con:
class B {
    virtual ~B();
/* un sacco di dati e funzioni membro */
};
class D : public B {
    virtual ~D();
/* altri dati e funzioni membro */
};
int main() {
B* pb = new D;
delete D; // errore
}
si ottiene il comportamento corretto ovvero la chiamata in cascata
sull'oggetto pb del distruttore di D e di B.
Senza usare il virtual si avrebbe la chiamata al solo distruttore di B,
poiche' non e' possibile al compilatore "riconoscere" staticamente  il tipo
effettivo di pb (almeno sicuramente con RTTI disabilitata).

 dinamica da una "allocation function", la cui forma standard e':
[cut]
penso ci sia stato qualche malinteso; nell'esempio originale avevamo:
class B {
   /* un sacco di dati e funzioni membro */
};
class D : public B {
   /* altri dati e funzioni membro */
};
int main() {
   B* pb = new D;
   delete D; // errore
}
(immagino che l'ultima riga volesse essere "delete pb")
in effetti c'e` un errore, ma e` diverso da quello segnalato da AG5F: la 
classe B non ha distruttore virtuale, per cui la delete e` 
"semplicemente" un undefined behaviour.
ciao Anto
-- 
Antonio Cuni, 20 anni, studente
Brainbench C++ Master: 4.73, higher than 99% of all previous test takers.
C Language: 3.79, higher than 92% of all previous test takers.
Guida C/C++ di Programmazione.it: 
http://cpp.programmazione.it

 identifica il blocco da deallocare.
per la precisione volevo ricordare che esiste anche una overload di 
operator delete che prende come parametro anche la dimensione:
class Prova {
public:
        void operator delete(void*, size_t);
};


 (immagino che l'ultima riga volesse essere "delete pb")
Ehm... in effetti č cosě... piccolo errore di distrazione gente... scusate!

Volevo porre l'accento sull'importanza di dichiarare distruttori virtuali
qualora si prevedesse di derivare una classe...
Tralasciando le varie interpretazioni sull'ordine di chiamata a distruttori
ed operatori delete, comunque vada non è nè elegante e nè sicuro non
dichiarare il distruttore di una classe da derivare. In "Exceptional C++"
c'è un esempio che discute ESATTAMENTE questo caso (ora il libro l'ho
prestato e non posso dirvi il numero del tema, ma si trovava nella sezione
riguardante la progettazione delle classi e dell'ereditarietà): lo avevo
davanti mentre scrivevo il mio post e l'ho riletto più volte perchè anche io
ero convinto che una classe con distruttore di default potesse essere
tranquillamente derivata...
Per rispondere a Nicola Musatti, infine, riguardo la questione della
funzione Reset(), era scritta solo a titolo di esempio: in effetti non è
sempre elegante scrivere funzioni di questo tipo... tuttavia, a volte è
NECESSARIO resettare lo stato dell'oggetto, per poi inizializzarlo
nuovamente: è il caso della stragrande maggioranza delle funzioni
A& operator = (const A& other)
in cui un algoritmo swap&destroy consente di fare una cosa che è sinonimo di
"progettare bene una classe", anzichè del contrario: consente di
implementare l'operatore di assegnamento in funzione del distruttore per
copia.
---- Una precisazione forse inutile -----
Ho anche fatto un errore (che credo non abbia tratto in inganno nessuno) nel
mio esempio (errore di distrazione, può capitare!)
L'errore è nella riga
...
delete D;
...
Avrei dovuto scrivere "delete pb;" ed, in effetti, era quanto volevo
scrivere, ma andavo di fretta...

 di eventuali risorse allocate nella classe derivata.
in realta` la situazione e` ben peggiore: se elimino un oggetto 
attraverso un puntatore ad una sua classe base che non ha distruttore 
virtuale allora vado incontro ad un undefined behaviour: in caso di 
derivazione singola molto probabilmente si verifica quello che dici tu, 
ma se abbiamo derivazione multipla e/o virtuale la cosa migliore che ci 
possiamo aspettare e` un bel crash...


 copia.
La motivazione dell'uso di copy constructor e swap per l'implementazione
dell'operatore di assegnamento presentata da Herb Sutter e'
completamente diversa: in presenza di operazioni che potrebbero lanciare
eccezioni Herb si preoccupa di eseguire tutte le operazioni di
acquisizione del nuovo stato prima di distruggere quello vecchio e
quindi di compiere la sostituzione tramite un operazione che garantisca
di non lanciare eccezioni. Di qui l'uso del costruttore di copia per
creare appunto una copia temporanea dell'oggetto da assegnare
(operazione che potrebbe lanciare eccezioni) e quindi l'uso di
un'operazione di swap costruita in modo da copiare oggetti base e
sotto-oggetti ricorsivamente fino a compiere operazioni elementari su
tipi nativi, quindi con la garanzia di non lanciare eccezioni.
Ciao,
Nicola


Caro Nicola... diciamo esattamente la stessa cosa! Ho suggerito l'algoritmo
swap&destroy proprio per tenere in conto la sicurezza rispetto alle
eccezioni (dato che Francesco Rossi aveva dichiarato di richiamare
direttamente i distruttori delle proprie classi per poi, suppongo,
ricostruirli...)
A parte la questione del processo di allocazione della memoria (in cui
probabilmente hai ragione tu, ma che non giustifica il non dichiarare
distruttori virtuali in classi da derivare), diciamo esattamente le stesse
cose! :-D
Da parte mia mi riservo di comprare lo Stroustruppone non appena avrò finito
gli esami di stato (finanze permettendo)... poi potrò dire la mia anche sul
processo di allocazione della memoria (probabilmente ti darò ragione
:-P )...
:-D :-D :-D :-D :-D :-D :-D :-D :-D :-D :-D :-D :-D :-D :-D
Ciao,
[A]ndrea [G]iordano, Liceo Scientifico Pitagora, sezione [5F].
:-D :-D :-D :-D :-D :-D :-D :-D :-D :-D :-D :-D :-D :-D :-D