Newsgroup: it.comp.lang.c++
Titolo: eccezione in distruttore

Quando un distruttore chiamato in seguito ad un'eccezione tira un'altra 
eccezione il programma termina in modo "brusco".
class A
{
	public:
	A() {}
	virtual ~A()
	{
		throw 0;
	}
};
void funz()
{
	A a;
	throw 0;
}
int main()
{
	A a;
	throw 0;
	return 0;	
}
C'è qualche pattern standard di soluzione per questo problema?
Ho trovato due soluzioni :
1.
realizzare una struttura di eccezioni incatenate (con single-linked 
list) autocostruita, e tirando le eccezioni con una opportuna 
Exception::Throw(...) che verifica che non ci sia una qualche eccezione 
già pendente. Funziona, ma ha ancora qualche problema.
2.
svuotare i distruttori e implementare la loro funzionalità in metodi 
close(), lasciando nei distruttori solo la verifica che l'utente abbia 
chiamato la close().
C'è qualcosa di meglio ?
Una di queste due soluzioni è nettamente preferibile all'altra ?

 C'è qualche pattern standard di soluzione per questo problema?
Secondo Meyer ed Eckel (e non solo loro), un distruttore che invoca 
eccezioni è "Male". Forse devi ripensare al loro utilizzo...
Ciao!
-- 
---
*************** | Have you ever retired a human by mystake?
*** Azathot *** |
*************** | To contact me, ask me gently. Thanks.

 
Sono contento che anche per loro è "male".
Esempio pratico:
class File
{
protected:
	int m_fd;
public:
	File(const char * filename)
	{
		m_fd = open(filename,...);
		if(m_fd==-1)
			throw new MyException(errno);
	}
	virtual ~File()
	{
		if(-1==close(m_fd))
			throw new MyException(errno);
	}
};
questa la rifaresti così : ?
class File
{
protected:
	int m_fd;
public:
	File()
	{
		m_fd = -1; // valore di file descriptor non valido
	}
	void Open(const char * filename)
	{
		assert(m_fd==-1);
		m_fd = open(filename,...);
		if(m_fd==-1)
			throw new MyException(errno);
	}
 
	void Close()
	{
		assert(m_fd!=-1);
		if(-1==close(m_fd))
			throw new MyException(errno);
	}
 
	virtual ~File()
	{
		assert(m_fd==-1);	
	}
};

 };
Uhmm... considerazione: cosa succede _veramente_ se close() fallisce? 
Andando a leggere la parte "NOTES" di man 2 close, troviamo:
       Not checking the return value of close  is  a  common  but
       nevertheless  serious programming error.  It is quite pos­
       sible that errors on a  previous  write(2)  operation  are
       first  reported  at  the  final  close.   Not checking the
       return value when closing the file may lead to silent loss
       of  data.   This  can  especially be observed with NFS and
       disk quotas.
       A successful close does not guarantee that  the  data  has
       been  successfully  saved  to  disk,  as the kernel defers
       writes. It is not common for a  filesystem  to  flush  the
       buffers  when the stream is closed. If you need to be sure
       that the data is physically stored use fsync(2).  (It will
       depend on the disk hardware at this point.)
Quindi il serio problema è che può esservi stata della perdita di dati in 
quanto l'ultima scrittura riporta un errore all'altezza, appunto, del 
close. Però questo può essere evitato inserendo una chiamata a fsync( ) 
(che, tra le altre cose, garantisce lo svuotamento dei buffers). A questo 
punto, dopo aver garantito che ogni scrittura abbia REALMENTE[1] scritto 
dei dati, secondo me il distruttore andrebbe riscritto così:
        virtual ~File() {
                // fsync( m_fd ); // Da inserire solo se non lo fai 
                //                      all'altezza dei Write della classe
                close(m_fd);
        };
L'assert() mi sembra del tutto inopportuna: infatti la classe File non è 
completamente costruita se m_fd è == a -1 (in quanto viene lanciata una 
eccezione in File() ).
Ciao!
-- 
[1] O tramite fsync (basse performances) o tramite una cache "ragionata".


 
L'assert() nel distruttore è nella versione (2) in cui il costruttore 
non apre il file. Ed è utilizzato per controllare se l'utente ha 
chiamato Close() prima di distruttgere l'oggetto.
Comunque hai solo aggirato il problema, forzando con fsync e close a non 
avere mai errori in fase di liberazione di una risorsa.
Il problema è che l'unica risorsa che si può liberare senza problemi è 
la memoria con delete o free, il primo un operatore, la seconda ritorna 
void.
Per tanti altri tipi di risorse: file, oggetti gdi, connessioni a 
database e tanti altri, le librerie base (in c liscio) possono dare 
errore anche in fase di chiusura.
Quindi quale è il metodo più corretto per wrapparle in classi C++ (se 
esiste). Oppure bisgona riununciare, e continuare ad usarle direttamente ?

 errore anche in fase di chiusura.
Ok, però devi verificare cosa significa e che impatto ha un errore in 
chiusura, così come ho verificato che close(2) può dare effetti collaterali 
come perdita di dati, altri saranno gli effetti collaterali, ma sicuramente 
rimediabili.
Ciao!
-- 


 
Sì, effettivamente in molti casi se fallisce la chisura non c'è niente 
da fare, solitamente non è possibile ritentarla, ma sarebbe opportuno 
avvisare l'utente del problema.

        };
così però sposti solo uno dei problemi e ne introduci un altro...
mi spiego:
con fsync vuoi rimediare un solo effetto collaterale, cioè quello del
mancato flush effettivo dello stream sul disco, close cmq fallisce.
il problema è che anche fsync può fallire, inoltre secondo le mie
manpage fsync non è ansi né posix.. aggiungi anche un problemino di
portabilità quindi sembra..
a me sembra che il punto vada fatto su perché fallisce close non sul
cosa succede quando fallisce, per rendere il distruttore nothrow..
leggendo la manpage di close... ne si evince che close fallisce solo
in due casi..
ERRORS
     The close() function will fail if:
     [EBADF]            D is not an active descriptor.
     [EINTR]            An interrupt was received.
il primo viene escluso a priori in maniera banale:
usando una classe RAII che wrappi il descriptor, si evita che la close
possa fallire in quel modo ( è vero che l'utente della classe potrebbe
bruteforzare il descriptor e chiuderlo anche se non si fornisce un
accessor per raggiungere il descriptor... ma.. non ci si protegge
contro Machiavelli.. )
per il secondo caso di failure.. si conosce a priori la possibilità
che l'evento accada... è quindi comunque possibile considerare la
close() come never failing dati dei presupposti..
a quel punto è possibile scrivere la classe come RAII
//----------------------------------------------------------------------
// FileImplAnsi.hpp
#ifndef FILEIMPLANSI_HPP_INCLUDED
#define FILEIMPLANSI_HPP_INCLUDED
#include <string>
#include <cstdio>
class FileImpl
{ 
 public:
   typedef int HandleType;
 private:
   int handle_;
 public:
   HandleType getHandle() const { return handle_; } 
   void open(const std::string& name, blahblah..)
   {
       handle_ = std::fopen( bluabhal);
       if(handle == -1) throw( blahblahblah );   
   } 	
   void close(){ std::close(handle_); }
};
#endif
//----------------------------------------------------------------------
// File.hpp
#ifndef FILE_HPP_INCLUDED
#define FILE_HPP_INCLUDED
#ifdef ANSI_IMPL
 #include "FileImplAnsi.hpp"
#endif
#include <memory>
class File
{
   const std::auto_ptr<FileImpl> pimpl_;
 
   File(const File&);
   File& operator=(const File&); 
 protected:
   FileImpl::HandleType getHandle() const { return pimpl_->handle; } 
 public:
   File(const std::string& name, blahbla...)
	pimpl_( new FileImpl() )
   { 
	pimpl_->open(name); 
   }
   virtual ~File(){ pimpl_->close(); }
};
//----------------------------------------------------------------------
.... scusate le imprecisioni ...
sbirish.

 a quel punto è possibile scrivere la classe come RAII
Non capisco qual è il vantaggio di usare l'idioma PImpl in questo caso, se 
poi il nostro obiettivo è quello di derivare: ottieni sia una diminuzione 
di performances inutile (doppia indirezione) che l'impossibilità di 
derivare banalmente.
Ciao!
-- 


 avvisare l'utente del problema.
L'utente si avvisa in un altro momento: se una operazione può comportare un 
fallimento, ed il fallimento può comportare una perdita di dati, 
l'informazione deve essere data il più vicino geograficamente nel codice 
dove il problema si è posto. Quindi sul Write( ... ) piuttosto che a 
livello del distruttore del File.


 };
Ciao,
Nicola
-- 
Posted via Mailgate.ORG Server - 
http://www.Mailgate.ORG

derivare banalmente.
ciao Azathot,
il vantaggio è quello di un bridge + wrapper façade (cfr. POSA o la
doc di ACE e GoF) in quel modo "sebbene" tu abbia una doppia
indirezione.. hai comunque un bridge che ti permette di staccare
l'implementazione dall'interfaccia comune, dal che deriva direttamente
la conseguenza,che in automatico puoi implementare il File con
CreateFile o altre api osspecific, da cui deriva la possibilità di
utilizzare memory mapped files ecc.. ( oppure come vuoi tu di mettere
in una impl l'fsync per tenere lo stream sempre updatato )
dopotutto se fosse bastata una implementazione puramente attinente
allo standard, sarebbe stato più che sufficente utilizzare fstream (
suppongo ) senza la necessità di reinventare la ruota.
riguardo al derivare banalmente.. non vedo perché non potresti, se ti
riferisci invece al fatto che la classe sia noncopiable e
nonassignable, quello è dato dal fatto che non vedo una buona
semantica di copia per il File in una classe RAII simile, nulla ti
vieta cmq di scrivere un proxy copiabile di File o di utilizzare uno
shared_ptr o simili per metterlo in un container..
non trovo la doppia indirezione uno svantaggio tale da precludermi
l'utilizzo di un pattern che può darmi dei vantaggi tali da garantirmi
una efficenza di gran lunga superiore a quella che può darmi una pura
classe non bridgata ( senza contare che in questo modo elimini il
coupling dovuto al codice os-dependant leggibilità ecc..) 
per quanto riguarda l'efficenza.. nel caso sia necessaria.. sarà poi
il profiler a parlare.. a priori ritengo che un approcio simile sia
meno errorprone.
il fatto che compaiano l'idioma pimpl e l'idioma del const auto_ptr
sono solo conseguenze del fatto che volessi usare un bridge.
sbirish.

 meno errorprone.
Il profiler può evidenziare i colli di bottiglia, non una inefficienza 
diffusa.


diffusa.
ACE è usato anche in ambienti realtime (c'è una marea di
documentazione oltre al posa a riguardo).. se guardi come schmidt ha
implementato un reactor vedi almeno 3 o 4 bridge uno dentro l'altro..
decisamente non parlerei di inefficenza diffusa, piuttosto di
portabilità, leggibilità, mantenibilità, ed efficenza, per quanto
nella mia modesta posizione considero alcune scelte fatte nella
libreria discutibili.
Nel mio parere inoltre quello che tu chiami "inefficenze diffuse", nel
mio punto di vista sono "potenziali ottimizzazioni non dovute".
sbirish.

 decisamente non parlerei di inefficenza diffusa, 
Io invece sì, forse perché non parlo per teorie.
Ciao!


Ciao!
.. ciao

non trovo la doppia indirezione uno svantaggio tale da precludermi
chiedo scusa... l'indirezione è una sola.. non so perché ho scritto
doppia =)
sbirish.

 JAWS e ACE non sono teorie. conosci entrambi?
Li hai mai visti usati in ambienti reali? No, non usare Google, ti prego.
Parlo della TUA esperienza personale.
Se ne hai, si intende.


 doppia =)
Forse perché fai il doppio del lavoro? :D


Se ne hai, si intende.
non si risponde ad una domanda con una domanda. 
si comunque, rubo dal posa2 praticamente tutti i giorni.
Ti invito comunque a non scendere sul personale, non posto in questo
newsgroup per "dimostrare le mie competenze", nè tantomeno per
confrontarle con le tue per le quali francamente non ho alcun
interesse.
ti cito testualmente l'esperienza ed i commenti di un mio carissimo
amico che ha letto il tuo post ( aggiungo solo un pò di punteggiatura 
visto che stavamo parlando in irc ) :
<cite from="Ivan Tonizza">
 Ho lavorato in ambienti dove non solo era sensibile il tempo di
 esecuzione ma doveva anche essere molto piccolo, nella fattispece
 trattasi di visione artificiale applicata all' automazione 
 industriale. I timings piu' lenti non sono stati un grosso problema 
 tranne in alcuni casi isolati dove semplicemente nella fase finale 
 dello sviluppo, quando ormai le interfaccie erano solide, ho rimosso 
 il pimpl (ma alla fine, non all' inizio). I tests a volte 
 richiedevano di andare a svariate centinaia di chilometri dall'  
 ufficio dove era situata la macchina fisica; ovviamente il tempo di 
 compilazione era una issue: 15 minuti persi a compilare per ogni 
 modifica a volte possono essere un problema e quando hai 9 mega di 
 sorgenti librerie escluse possono esserlo ulteriormente. Il pimpl e' 
 comodo per questo, alla peggio lo si rimuove poi senza grosse 
 sofferenze, taglia copia incolla lo fa ancora l' editor, inoltre per 
 quel che riguarda il bridge ed i patterns non e' una questione di 
 'far figo', e' una questione di non reinventare la ruota ogni volta. 
 Ho prototipizzato il sistema in due mesi circa e con poche modifiche 
 da prototipo e' diventato prodotto in questo devo molto al genio di 
 altre persone che hanno sintetizzato alcuni principi di design che ho
 esaminato e ritenuto corretti e su questi altre persone hanno 
 costruito delle soluzioni che ho ritenuto viabili per i problemi che 
 mi sono trovato davanti; alla fine e' questo quello che sta sotto l' 
 ingegneria del software: davanti ad un problema trovare una soluzione
 viabile 
</cite>
sbirish.

Forse perché fai il doppio del lavoro? :D
no guarda rileggendo... ho scoperto che l'ho scritto perché rispondevo
a quello che scrivevi tu.
<Azathot>
Non capisco qual è il vantaggio di usare l'idioma PImpl in questo 
caso, se poi il nostro obiettivo è quello di derivare: ottieni sia una
diminuzione di performances inutile (doppia indirezione) che 
l'impossibilità di derivare banalmente.
</azathot>
sbirish.

 interesse.
Sbirish, non c'è bisogno che tu ti difenda. Se ti vergogni delle tue 
competenze, non è un problema mio.


 Niente assert.
Perchè niente assert ?
Se l'utente cerca di chiudere un file che non ha mai aperto o che ha già 
chiuso c'è qualcosa che non va nel suo codice e penso che sia meglio 
avvisarlo.
Forse il problema è decidere se una libreria deve essere estremamente 
tollerante ad uso improprio, e cercare in ogni caso di procedere senza 
errori, oppure se deve segnalare ogni minimo errore di uso.
Se l'utente fa 2 close, non lo fa certo apposta, e il fatto di non 
permetterglielo forse lo può aiutare a trovare errori presenti nel suo 
codice.
ciao,
Daniele

 avvisarlo.
Forse il punto sta proprio nel fatto che l'utente non dovrebbe sapere cosa 
significa "chiudere" un file... la sua semantica dovrebbe rimanere 
all'interno della semantica del C++... lì non si "apre" o "chiude" un file 
ma si istanza e si distrugge un oggetto.


 competenze, non è un problema mio.
mah....
Tommy

 mah....
Caro Tommy, vedo che estrarre le frasi dal loro contesto è uno sport molto 
diffuso dalle tue parti. Invece di fare della sterile ed inutile polemica, 
ti conviene riflettere sul contributo che dai a questa discussione, in 
questo preciso istante.
Grazie.
Saluti.


 questo preciso istante.
Nessun contributo, ovvio... (e chi si aspettava di contribuire all
discussione ;-)?!? Mah...?!?
Come tu non dai nessun contributo con questo tuo messaggio del resto e con
quello prima quando parli con gli altri come fossero "cose da disprezzare".
Poi inutile parlare di questo, quindi chiudiamo questo dibattito che non ha
nulla a che fare con il C++, ma volevo solo estrapolare tutte le frasi dal
loro contesto (come dici tu) per farti vedere quanto sei ...... (lascio a te
la scelta del termine).
Tommy

 (lascio a te la scelta del termine).
Scommetto che non è un termine positivo. Il che mi rallegra, perché non c'è 
niente di più bello che essere offesi da una persona di scarsa levatura.


 niente di più bello che essere offesi da una persona di scarsa levatura.
Noooo..... non ricominciate dai. Fate pace. Una bella stretta di mano.
Dopotutto, a mio avviso, siete entrambi persone intelligenti.........

 codice.
Tieni conto pero' che nel momento in cui interponi un tuo livello di
astrazione tra l'utente e l'API originaria, sei tu che decidi cosa e' un
errore e cosa non lo e'. Tanto vale quindi andare in contro all'utente,
dove non ci sono controindicazioni.


 ma si istanza e si distrugge un oggetto.
In effetti questa e' una scelta possibile; un'alternativa e' fare come
'delete', ovvero garantire che la chiusura di un file gia' chiuso non
dia problemi.


 dia problemi.
Anche.
