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

Ciao a tutti!
Ho una domanda da porvi: mi capita spesso che nei miei programmi in C++ alla
fine del codice di un metodo vengono chiamati erroneamente i distruttori
delle classi mandate in argomento al metodo, e non va bene.
Ecco un esempio:
Sto scrivendo una libreria matematica e ho scritto la classe CNumber,
costruttore particolare così dichiarato:
CNumber::CNumber(CVString Number);
questa funzione prende in argomento una variabile di tipo CVString (una
classe da me scritta appositamente), che, controllando, non viene modificata
fino alla fine del codice della funzione. Dopo il return però viene chiamato
il distruttore di CVString, che mi cancella il buffer della stringa, e
creando tanti casini (ma questa è un altra storia).
Ora: ecco il problema: mi trovo nel corpo della funzione main e...
int main()
{
 CNumber Zero("0.0");
...
}
Il problema è che così viene prima chiamato il costruttore di CVString che
accetta una stringa di char in input, poi viene chiamato il costruttore
della CNumber, che a sua volta chiama (e qui sta l'errore) il distruttore di
CVString.
La funzione termina, e viene richiamato (giustamente) il distruttore di
CVString, che è già stata deallocata.
Per prevenire questi casini il distruttore di CVString è scritto pressappoco
così:
CVString::~CVString()
{
 if( m_Buffer != NULL )
 {
  delete[] m_Buffer;
  m_Buffer = NULL;
 }
 m_BufferSize = 0;
 m_Len = 0;
}
Il problema però è che quando debuggo il codice in questo punto noto che il
campo m_Buffer (char*) non ha più il valore NULL assegnato dal distruttore
chiamato prima erroneamente, ma ha un altro valore, mentre invece gli altri
campi hanno conservato il loro valore originario.
Come mai succede tutto questo?
Grazie a tutti per l'aiuto!
Dome

 Come mai succede tutto questo?
Non l'ho capito bene :-) Comunque perchè non usi std::string?
-- 
Andrea Bergia - studente          Linux Registred User #281550
Per favore non mandatemi allegati in Word o PowerPoint.
Si veda 
http://www.fsf.org/philosophy/no-word-attachments.html

> Non l'ho capito bene :-) Comunque perchč non usi std::string?
All'inizio la libreria sfruttava le std::string, poi ho scritto una classe
mia (le CVString) e pare vadano piů veloci delle std::string. Inoltre ho
scritto qualche funzione particolare che non č presente nelle string, e ho
fatto prima a creare una nuova classe...
Ad ogni modo il problema č che mi vengono chiamati piů volte i distruttori
per uno stesso oggetto, anche in posizioni in cui non me lo aspetterei: ad
esempio se do come argomento ad una funzione un certo oggetto O, alla fine
del codice della funzione viene chiamato ~O (e non voglio).
Dome

 del codice della funzione viene chiamato ~O (e non voglio).
Non e' possibile che un distruttore sia chiamato piu' volte per lo
stesso oggetto.  Punto.
Probabilmente quando passi un istanza della tua classe come argomento
della funzione, la passi per valore (e non per riferimento); in questo
caso viene chiamato il costruttore di copia sull'oggetto originale,
allo scopo di creare un temporaneo da passare alla funzione.  Il
temporaneo viene, come tutti gli oggetti, distrutto quando esce di
scopo (all'atto del ritorno dalla funzione).
Se distruggere una copia dell'oggetto modifica l'oggetto copiato, la
tua classe e' malformata: tipicamente contiene dei pointers, e ti sei
dimenticato di creare un costruttore di copia (e magari un operatore
di assegnazione); o, se lo hai creato, non funziona correttamente.
-- 
Maurizio Loreti                         
http://www.pd.infn.it/~loreti/mlo.html
Dept. of Physics, Univ. of Padova, Italy              ROT13: 
ybe...@cq.vasa.vg

Ciao DOM3,
    purtroppo non mi e' molto chiara la dinamica. Se vuoi puoi provare a
rispiegarmela in maniera piu' dettagliata.
Comunque, cosi' a primo acchitto ti chiedo se hai valutato il fatto che la
variabile di tipo CVString passata in argomento al costruttore di CNumber,
viene costruita per copia e, non trattandosi di memoria allocata
dinamicamente, viene distrutta (leggi:ne viene invocato il distrutttore)
*automaticamente* quando esce di scope (leggi: all chiusura dell'ultima
graffa che delimita il corpo del costruttore di CNumber).
Hai provato a modificare il prototipo del costruttore incriminato come
segue?
CNumber::CNumber(CVString *)
HTH,
ciao,
baba

fino alla fine del codice della funzione.
Hmmm... "modificata". Quindi non e' immutabile. Quindi
credo che sarebbe sensato passare un const reference
per evitare di duplicare stringhe senza motivo (o anche
solo per evitare aggiornamenti di reference count se
implementi stringhe COW).

Ovviamente quelle assegnazioni nel distruttore non
servono. I membri che assegni smettono di esistere
non appena si raggiunge la parentesi graffa.
Anche il test su m_Buffer non serve. Chiamare delete[]
su un puntatore nullo non ha nessun effetto.
Comunque dai sintomi che esponi sono fortemente
propenso a credere che tu ti sia dimenticato del
fatto che il C++ effettua delle copie e delle
assegnazioni con implementazioni di default
basate su copia/assegnazione membro-a-membro.
Questo provoca problemi nel caso di classi che
contengono puntatori o handle in genere se questa
implementazione standard non e' accettabile.
Una regola semplice da ricordare e' che se ti serve fare
qualcosa nel distruttore allora e' molto molto probabile
che tu abbia bisogno anche di qualcosa nel costruttore
di copia e nell'operatore di assegnazione; al limite
quello che puo' essere richiesto e' semplicemente
di impedire che delle versioni errate di queste funzioni
vengano automaticamente sintetizzate dal compilatore.
Supponiamo:
   class CVString
   {
     private:
       int  size;
       char *buf;
     public:
       CVString(const char *s)
       : size(s),buf(new char[1+size])
       {
         strcpy(buf,s);
       }
       ~CVString()
       {
         delete[] s;
       }
   };
Questa classe contiene un errore grave... se infatti
scrivi
   void foo(CVString s)
   {
   }
   void bar()
   {
     CVString s1("test");
     foo(s1);
   }
quello che succede e' che il compilatore crea un
oggetto CVString partendo dal literal "test" nell'istanza
s1 e poi crea una copia da passare a foo. Questa copia,
visto che la classe non indica un sistema specifico per
crearla e che non esistono impedimenti a sintetizzare in
automatico un costruttore di copia, viene costruita nel
modo standard che corrisponde a:
   CVString(const CVString& other)
    : size(other.size),buf(other.buf)
   { }
in pratica semplicemente i membri "size" e "buf" vengono
copiati. Nota... nel caso di buf viene copiato il PUNTATORE,
e non viene quindi allocata un'altra area e fatta una copia
dei dati; quindi originale e copia puntano entrambi alla
STESSA area di memoria.
Alla fine dell'esecuzione di foo la copia costruita
per essere usata nella funzione (nota che e' stata
passata per *VALORE* e non per const reference) viene
elimintata e quindi il distruttore di s provvede ad
eseguire una "delete buf[]" che libera lo spazio di
memoria a cui pero' s1 in bar sta ancora puntando!
Alla fine di bar quello che succede e' che anche s1 viene
distrutta e il suo distruttore provvede ad effettuare
una seconda volta la chiamata "delete[] buf" con lo
stesso indirizzo (con effetti potenzialmente disastrosi).
Questo spiegherebbe perche' impostare "buf" a null non
serve... quello che vedi impostare e' il puntatore buf
della stringa s di foo, quello che vedi nella seconda
chiamata e' invece il puntatore della stringa originale.
Una possibile soluzione sarebbe aggiungere un
costruttore di copia corretto del tipo...
  CVString(const CVString& other)
   : size(other.size),buf(new char[1+size])
  {
    memcpy(buf,other.buf,1+size);
  }
Oltre al costruttore di copia devi pero' ricordarti
anche l'assegnazione. Infatti la versione creata
automaticamente dal compilatore C++ nel caso che tu
non ne fornisca una e che non ci siano altri
impedimenti e' del tipo:
  CVString& operator=(const CVString& other)
  {
    size = other.size;
    buf = other.buf;
    return *this;
  }
che creerebbe problemi seri nel caso che
venisse eseguito codice come
   CVString s1("Hello"), s2("world");
   s1 = s2;
visto che al momento dell'assegnazione avresti che
il valore di s1.buf verrebbe buttato (senza fare
la delete[] e quindi generando un memory leak) e
sostituito da un altro puntatore all'area puntata da
s2.buf (con problemi di doppia distruzione di questo
buffer come nel caso del costruttore di copia creato
automaticamente dal compilatore).
HTH
Andrea

 di assegnazione); o, se lo hai creato, non funziona correttamente.
Ah! Piccola cosa: come sono gia i copy constructors? Come devono essere
scritti di solito?
Grazie ancora,
Dome

 implementi stringhe COW).
Infatti era qui l'errore... in un paio di metodi delle classi che ho scritto
ho dimenticato di mettere il reference :))) Ops :)
Dome

ho dimenticato di mettere il reference :))) Ops :)
Quello credo dovrebbe portare solo a problemi di performance,
non di funzionamento. Era *un errore* (peraltro relativamente
veniale), non *l'errore*.
Avere oggetti che se copiati creano problemi di funzionamento
e' estremamente pericoloso in C++; il problema e' che ci
sono molti contesti in cui copie vengono fatte implicitamente
e, cosa anche peggiore, ci sono casi in cui il fatto di farle
o meno e' a libera scelta del compilatore.
HTH
Andrea

 Dome
qualcosa del genere
TObj::TObj(const TObj &obj);
HTH
ciao,
baba

 scritti di solito?
Riprendi il tuo testo di C++ ed aprilo al capitolo 1.


> Riprendi il tuo testo di C++ ed aprilo al capitolo 1.
Ehm... autodidatta :)
Dome

 o meno e' a libera scelta del compilatore.
No no... fidati che era *l'errore* dato che mettendo il reference tutto è
andato a posto.
Il problema in questo caso non era di performance: la classe CVString ha tre
campi: due int e un char* che è il buffer della stringa. Senza il reference
viene creata una copia dell'oggetto in questione con un copy constructor
predefinito (quello default inserito dal compilatore penso... ad ogni modo
non scritto da me) che semplicemente alloca una CVString, e ne copia i
valori delle variabili dall'argomento da me passato alla funzione. Questo
vuol dire che ora il campo char* punta alla stessa area di memoria della
classe data in argomento.
Al termine della funzione la copia viene correttamente distrutta, ma nel
distruttore dealloca l'area di memoria puntata anche dalla CVString data in
argomento, che però NON è stata ancora distrutta. A questo punto quando alla
fine viene chiamato il distruttore della CVString data precedentemente in
input alla funzione, succede il fattaccio: l'area puntata dal campo char* è
stata deallocata, e il distruttore prova nuovamente a deallocare... immagina
tu le conseguenze ^_^ (comunque ho messo un controllo sulla "bonta'" del
puntatore: se vale NULL non viene deallocato, ma è ovvio che se la variabile
m_Buffer (il char*) non è stata toccata, ma è stata toccata solo l'area a
cui essa puntava, m_Buffer non varrà mai NULL...)
Spero di essermi spiegato :)
Dome

 distruttori delle classi mandate in argomento al metodo, e non va bene.
molto probabilmente ti sei dimenticato del costruttore per copia (e anche 
di operator=(), ma in questo caso non c'entra). Dato che non lo hai 
definito il compilatore ne ha creato uno di default che si limita a 
copiare i dati membro-a-membro.
Nel costruttore 
        CNumber::CNumber(CVString Number)
Number viene passato per valore: questo significa che viene creato un 
temporaneo chiamando il copy ctor di CVString: tale temporaneo ha gli 
*stessi* campi dell'oggetto originale, puntatore al buffer compreso; 
quando la chiamata a CNumber::CNumber() termina il temporaneo viene 
distrutto, chiamandone il distruttore: ovviamente il distruttore libera 
la memoria allocata dal buffer, ma incidentalmente il buffer era *lo 
stesso* usato dal nostro oggetto originale, che viene cosi` corrotto.
Inoltre quando poi l'oggetto originale verra` distrutto (ad esempio alla 
fine del programma) il suo distruttore effettuera` la delete[] sullo 
stesso buffer gia` deallocato, con il risultato di provocare un 
bell'undefined behaviour.
ciao Anto
PS: probabilmente le tue CVString sono piu` veloci di std::string proprio 
perche` "trascurano" le copie...
-- 
"Computer science is not about computers any more than astronomy
is about telescopes." -- EW Dijkstra

 perche` "trascurano" le copie...
No no... messo a posto e testato :)
Sono stringhe fatte a posta per quello che mi servono (manipolazione di
vettori di numeri) quindi fanno benone il loro dovere...
Dome

 scritto ho dimenticato di mettere il reference :))) Ops :)
attento che l'errore non era il mancato passaggio per reference: l'errore 
e` la mancanza del copy ctor e il passaggio per valore non fa altro che 
"portare a galla" tale errore.
Se tu passi per reference ti trovi in una situazione in cui l'errore 
*c'e`* ma non si manifesta: ovviamente questa non e` una bella cosa.
Prova ad esempio a considerare questa funzione:
CVString myname()
{
        CVString result = "Antonio";
        return result;
}
In questo caso si presenta lo stesso bug di prima, ma come fai a 
"correggerlo" senza scrivere il copy ctor?
ciao Anto


puntatore: se vale NULL non viene deallocato
Come ti ho detto e' un controllo inutile. Deallocare
un puntatore nullo in C++ e' perfettamente lecito e
non fa assolutamente niente.
HTH
Andrea

Ehm... autodidatta :)
Se vuoi un consiglio leggiti un buon testo sul C++.
Se conosci il C credo che "The C++ Programming Language"
possa andare bene (io mi ci sono trovato bene).
Secondo me il C++ e' un po' troppo tagliente per essere
imparato a tentativi. Inoltre nel C++ ci sono molte
"asimmetrie" e "stranezze" che esistono solo per ragioni
storiche di evoluzione del linguaggio.
Per tanto che uno sia intelligente non riuscira' mai
a dedurre quale e' stata l'evoluzione storica del
linguaggio C++ ... l'unica possibilita' e' leggere.
Nota anche che in C++ si assume che il programmatore
sappia sempre perfettamente cosa sta facendo.
Quindi quando qualcosa viene usato in modo non corretto
il sistema runtime non e' tenuto a segnalare la cosa...
ma si cade nel territorio denominato "undefined
behaviour" in cui puo' succedere di tutto.
Se si e' fortunati questo significa un bel crash, ma
purtroppo questo "di tutto" si traduce spesso in
un subdolo "non succede niente" e quindi ci si
illude che il programma stia funzionando quando invece
sta solo aspettando il momento piu' propizio per
rivelare il malfunzionamento (secondo Murphy l'errore
si presentera' nell'istante in cui provoca danno reale
maggiore ... ad esempio il giorno della presentazione ;))
C'e' chi dice che il C++ e' ormai troppo complicato
per essere imparato tutto e bene da una sola persona,
impararlo poi a tentativi e' veramente come voler
svuotare il mare usando un cucchiaino da caffe'.
Andrea

 creazione di copie e' cosi' naturale e implicita.
Ahhh! Ok ok, qui ti do ragione! Infatti appena rimetto mano al codice mi
creo il costruttore copia della benedetta classe CVString!
Grazie per l'aiuto!
Dome

 svuotare il mare usando un cucchiaino da caffe'.
Eh si.. penso che mi comprerò un buon libro... ehm... so bene che dovrò
leggere un libro in inglese ma ci provo lo stesso: esiste un libro
equivalente a quello da te consigliatomi ma in italiano?
Grazie

 Grazie
La traduzione di quello che ti aveva consigliato:
"Il linguaggio C++", B. Stroustrup (consiglio terza edizione con la parte
sulla libreria standard e la parte sulla progettazione).
In effetti la tua paura di cadere in un testo di lingua inglese č piuttosto
giustificata, considerata la percentuale di libri del settore che vengono
tradotti... perņ questo č proprio la "Bibbia" del C++, per cui l'han
tradotto (nemmeno troppo male, IMHO).
Buona lettura,
    Rony
--
======================================================
Rony Cesana
Sistemi Informativi srl - Gruppo Liberologico
via F. Goya 2 - 56010 loc. Ghezzano - Pisa Italy
tel.: 
+39 050 877088
 - fax: 
+39 050 8755498
pers.: 
+39 347 2582666
e-mail: 
r.ce...@liberologico.com
www.liberologico.com
======================================================

tradotto (nemmeno troppo male, IMHO).
Tu dici ? e "namespaces" tradotto con "spaziature nei nomi", dove lo
mettiamo ? ;-)

 mettiamo ? ;-)
Stai scherzando, vero?  Ti prego, dimmi che stai scherzando!
Comunque la mia opinione e' che chiunque programmi deve imparare un
poco di inglese.  Non deve nemmeno essere in grado di sostenere una
conversazione e puo' benissimo ignorare come si pronuncia quello che
legge, ma deve capire la lingua scritta e conoscere i termini tecnici.
Infatti:
- i libri sono in inglese.  se sono tradotti, sono tradotti male e
  costano il doppio; ed in genere non sono tradotti.
- quando dovrai usare del software propietario, la documentazione
  sara' in inglese; oppure manchera' e dovrai leggerti i commenti in
  inglese nel codice.
- se ti verra' in mente di sfruttare un newsgroup utilissimo ed
  affollato da persone eccezionali come comp.lang.c++.moderated, i
  post saranno in inglese.
- se parteciperai a od osserverai in qualunque modo un progetto di
  free software, la lingua sara' l'inglese.
- e chi piu' ne ha piu' ne metta.


 mettiamo ? ;-)
quello a cui ti riferisci tu e` "Programmazione nella pratica" di 
Kernighan/Pike (peraltro ottimo e ben tradotto, se non fosse per quel 
"namespace"...)
Il libro di Stroustrup e` invece tradotto abbastanza bene, IMHO.


 Dome
Allora non hai fornito molti dati, ma da quel che ho capito:
1° CVString non ha un costruttore di copia
2° CNumber::CNumber(CVString Number) prende il parametro per copia e
non per riferimento
Questo implica che quando tu passi la string a CNumber lui ne crea una
copia e la usa dentro a CNumber::CNumber, essendo che è una copia
creata da lui al termine della funzione si preoccupa anche di
distruggerla, tu non hai definito un costruttore di copia quindi il
compilatore ne ha creato uno lui per te che però fa una semplice copia
bitabit dell'oggetto così la copia viene ad avere m_Buffer che punta
alla stessa posizione dell'm_Buffer dell'originale quindi quando viene
chiamato il distruttore della copia esso dealloca m_Buffer deallocando
anche quello della classe originale.
Le soluzioni sono Creare un costruttore di copia per CVString (E`
quella che ti consiglio) oppure Passare per riferimento o come
puntatore l'oggetto al costruttore di CNumber::CNumber (Questo
risolverebbe il problema in quella funzione, ma non in tutte le altre)