Newsgroup: it.comp.lang.c++
Titolo: Eccezioni sollevati da costruttori per copia

La terza edizione italiana del libro "C++" di Stroustrup contiene le 
seguenti parole nel paragrafo 
14.4.6.1
:
"la libreria standard ipotizza un comportamento corretto dei costruttori 
per copia -- ovvero, ipotizza che non sollevino eccezioni. Per questi 
motivi, occorre fare attenzione che un costruttore per copia sollevi 
eccezioni soltanto in condizioni veramente disastrose. E' poco probabile 
risucire a realizzare, in ogni possibile contesto d'uso, il completo 
ripristino dopo un'eccezione sollevata in un costruttore per copia."
E tra i suggerimenti alla fine del capitolo 14, c'e' scritto:
"9. Si eviti di sollevare eccezioni nei costruttori per copia".
Queste affermazioni mi lasciano piuttosto sgomento, in quanto mi sembra 
impossibile evitare di sollevare eccezioni nel costruttore per copia.
D'altra parte, nella stessa libreria standard, le stringhe e i 
contenitori sollevano eccezioni nei propri costruttori per copia.
Anzi, il costruttore per copia di vector, puo' sollevare due tipi di 
eccezioni; una quando tenta di allocare il buffer per gli oggetti da 
copiare, e questa e' ovviamente una bad_alloc, e inoltre, la copia di 
ogni oggetto puo' sollevare una sua eccezione nel suo costruttore di copia.
Ha veramente senso la raccomandazione di Stroustrup?
-- 
     Carlo Milanesi
 
http://digilander.libero.it/carlmila

 Ha veramente senso la raccomandazione di Stroustrup?
A livello teorico direi di si. Salvo fatto per la bad_alloc (che è
un'eccezione *molto* pesante) direi che in teoria la copia fra due
oggetti 'standard' è possibile fare in modo che non lanci eccezioni.
La bad_alloc la lascerei per il momento perdere: normalmente se parte
c'è poco da fare. O si riesce a liberare memoria (perchè il sistema
gestisce così la memoria) oppure molto probabilmente è caso di
cominciare a salvare il salvabile.
Per il resto ci sono oggetti che possono lanciare eccezioni perchè le
cose che gestiscono sotto il cofano possono farlo. Tipicamente un Lock
che io ho lockato non dovrei poterlo copiare (per motivi logici). Ma
forse varrebbe la pena che non sia copiabile affatto.
Magari mi sfugge qualcosa, comunque. Il punto è che il costruttore di
copia prende un  oggetto in uno stato valido e ne crea una copia: le
cose che possono andare male sono meno che quelle che si possono avere
manipolandolo oppure costruendolo da 0.
Ma poi immaginiamo questa funzione:
Widget foo(...) {
        Widget w(...);
        w.goo(...);
        return w;
}
Non mi piacerebbe pensare che semplicemente ritornando (costruttore per
copia) il tutto può scoppiare. Trovo gradevole l'idea che quella li sia
un'operazione quasi certamente salva.
Il fatto è che i costruttori di copia sono chiamati spessissimo sotto il
cofano. Per dire, anche quando passiamo un oggetto per metterlo in un
contenitore. Ecco, è IMHO gradevole poterci contare.
-- 
blog:  
http://www.akropolix.net/rik0/blogs
 | Uccidete i filosofi,
site:  
http://www.akropolix.net/rik0/
      | tenetevi riso e
forum: 
http://www.akropolix.net/forum/
     | bacchette per voi.

 cominciare a salvare il salvabile.
Invece penso che in vari tipi di applicazioni, comprese quelle che 
sviluppo io, la bad_alloc sia piuttosto comune e non deve avere effetti 
disastrosi.
Ecco un esempio (che non e' il mio):
Un programma di manipolazione di immagini raster permette di cambiare la 
risoluzione delle immagini. Se l'utente vuole trasformare un'immagine di 
  1000x1000 pixel (1 Megapixel) in una di 20000x20000 (400 Megapixel) se 
il computer ha abbastanza memoria l'operazione viene effettuata, 
altrimenti viene sollevata una bad_alloc, a fronte della quale il 
programma deve annullare l'operazione e mantenere la risoluzione 
precedente. Se si tratta di un programma di conversione a riga di 
comando, il fallimento coincide con la terminazione del programma, ma se 
si tratta di un programma grafico interattivo, il programma ovviamente 
deve continuare a funzionare.
In questo semplice caso, si ha una sola allocazione, ma alcuni programmi 
di calcolo numerico richiedono una risoluzione (spaziale o temporale) e 
discretizzano lo spazio in funzione di tale risoluzione. Al crescere 
della risoluzione, cresce, oltre che il tempo di elaborazione, anche 
l'occupazione di memoria. Se l'utente specifica una risoluzione elevata 
e il suo computer ha memoria insufficiente, ad un certa fase di calcolo 
l'elaborazione deve fallire e deve venire eseguita una rollback, che 
permetta all'utente di impostare una risoluzione inferiore e rilanciare 
il calcolo.
Non ritengo accettabile che tutta l'applicazione CAE termini solo 
perche' all'utente e' scappato uno zero in piu' nel campo "risoluzione".


 perche' all'utente e' scappato uno zero in piu' nel campo "risoluzione".
Eh, ma basta catchare anche bad_alloc, eh. Il punto č che, per
l'appunto, č una delle poche eccezioni che vedo giustificate in un
costruttore di copia.
Ma il punto č semplice: se la memoria non c'č, non c'č.
In determinati casi si puň provare a chiederne in altri modi e per le
piů svariate ragioni. O fai come dici tu, annulli l'operazione. Quello
che voglio dire č che la bad_alloc č un'eccezione per certi versi
diversa dalle altre eccezioni.
Tornando al costruttore di copia, appunto, accetto una bad_alloc. Ma i
tuoi casi non sono (direi) casi dovuti al costruttore di copia. Ovvero
mi aspetto che i tuoi oggetti da 400 Megapixel quando viene chiamato il
costruttore di copia non facciano una deep copy, ma condividano la
rappresentazione, per lo meno con un meccanismo copy on write. 
Per cui la semplice copia non causerebbe bad_alloc in ogni caso.
Comunque, come dicevo, la bad_alloc č accettabile e inevitabile in se e
per se. Le altre, se appena si puň, mi sembra ragionevole che non
partano.


Ma il punto č semplice: se la memoria non c'č, non c'č.
Anche perché immagino che se si arriva al punto che non c'č piů
memoria disponibile per allocazioni dinamiche, il sistema sta
strisciando di brutto, quindi non vedo come si potrebbe "andare
avanti".

 partano.
Sono d'accordo.


 ragionevole lanciare un'eccezione bad_alloc in un costruttore di copia?
Perchè 'dipende dal costruttore di copia', IMHO. In generale come dicevo
in un costruttore di copia cercherei (se possibile) di non chiamare new,
ma di condividere quanto possibile. Specie se la memoria allocata è
veramente molta. Se la memoria richiesta è poca per ciascun oggetto,
allora ha senso anche fare la deep copy: ma in questo caso se ti parte
una bad_alloc, 'there are no dicks': significa che proprio non hai più
memoria. E' un evento *parecchio* eccezionale.


 avanti".
beh, ma nell'esempio che ha citato Carlo Milanesi, il sistema 
non pareva essere prossimo all'esaurimento, dato che il confine 
si sforava con un singolo atto di allocazione monumentale (400 
MB). Metti che l'allocazione di quel colosso falliva perché il 
sistema ne racimolava solo 350 MB : non si può dire che in un 
simile caso l'intero programma era vicino al crollo qualsiasi 
allocazione successiva venisse richiesta. Magari si può ancora 
portare a casa la pellaccia e prendere tempo.
Poi, ovviamente, se ottieni bad_alloc nel tentativo di allocare 
una stringa di 1 KB, è evidente che il programma è 
scacchisticamente a 1-2 mosse dal beccare scacco matto e non ci 
piove più.
Però in generale è impossibile dire a priori se ci si trova nel 
caso 1 (richiesta di lusso) o nel caso 2 (richiesta vitale di 
routine).
Per fare un esempio lievemente diverso nella pratica ma simile 
concettualmente a quello della mega immagine a alta risoluzione 
: parecchio tempo fa in un programma che semplicemente copiava 
elenchi di files e directory, nella routine di allocazione del 
buffer, seguivo intenzionalmente una logica di overflow proprio 
per capire quanto potevo chiedere al massimo di memoria dinamica.
Cominciavo col chiedere 1 GB, e poi avanti con una ricerca 
dicotomica : catch (bad_alloc) e dimezzare la richiesta. Se 
falliva ancora, sempre dimezzare. Se aveva successo alzare del 
50 % (ossia a metà strada tra la prima successfull e l'ultima 
fallita). Certo il programma era banalissimo e a parte quel 
bufferone non aveva la necessità di usare più la memoria 
dinamica, per cui non crashava. Però in quella logica la 
bad_alloc non aveva più niente di particolarmente pericoloso.
Fungeva sotto Win. Ho sentito dire in passato, ma non conosco i 
sistemi, che sotto *nix questo approccio avrebbe fallito, perché 
sostanzialmente il sistema non ti dice mai NO ma ti mette 
l'applicazione nel freezer. Se fosse vero, la mia argomentazione 
non sarebbe generalizzabile a questi sistemi
ciao
Soviet_Mario

si sforava con un singolo atto di allocazione monumentale 
Il che vuol dire, stando a questo bizzarro ragionamento, che seppure
la memoria c'è, provvediamo noi a farla esaurire.

Andrea Laforgia ha scritto:

esatto ! In effetti non è bizzarro se visto in questo modo : il 
programma ti garantisce un calcolo sufficiente usando poca 
memoria, ma ti consente di provare a raffinarlo con uso più 
spendaccione della stessa, solo che questo raffinamento non è 
garantito a priori, dato che non si sa in che contesto girerà. 
Se all'utente serve, fa il tentativo, e non viene castigato per 
questo.
Ah, ovviamente c'è un baco di fondo in tutto questo discorso, ma 
non è, o almeno io la vedo così, nel mio approccio bizzarro (non 
volendo presumere di parlare anche per conto di Carlo Milanesi 
!), bensì nello standard medesimo riguardo all'allocazione 
dinamica :
il baco è che mancano funzioni diagnostiche che consentano di 
vedere, senza allocarla, se in un dato momento c'è una certa 
quantità di memoria disponibile (e magari anche chiedere all'OS 
quanta memoria totale e quanta usata al momento, per raffinare 
la stima se occorre, tutta roba che ad es è possibile via API, 
almeno in Win).
Poi so benissimo che la mera disponibilità in un dato istante 
abbia un'affidabilità per il futuro inversamente proporzionale 
al ritardo tra esplorazione e la richiesta effettiva, ma 
oculatamente usata l'ispezione delle risorse senza effetti 
collaterali permetterebbe strategie più indolori che non la 
cattura di eccezioni.
In effetti, benché non standard, già la librerie "C" di 10 anni 
fa cosentivano di esplorare lo "heap" per avere info orientative 
(tipo _heap_walk, _heap_check_node e roba simile, se ricordo i 
nomi).
All'epoca erano anche affidabili, in quanto facevano riferimento 
a un pur magro heap "locale", privato, del programma, e in 
quanto tale totalmente riservato al programmatore e non a 
processi estranei. Poi non me ne sono più occupato, ma suppongo 
che una trasposizione di tali routine in termini di heap globale 
di sistema, subappaltate dall'OS, non possano per definizione 
garantire pari affidabilità proprio perché costrette a 
collaborare nella gestione di risorse condivise (e con chissà 
quanti altri processi). Cmq un minimo di diagnostica esplorativa 
avrebbe almeno il pregio di trasformare un error trapping in un 
test anteposto (intendo immediatamente anteposto, pur senza 
voler scomodare codice non-interrompibile), con meno effetti 
collaterali e più facile ripiego alle soluzioni meno onerose.
Io non ho trovato nessuna alternativa migliore a sforare 
volontariamente e riprovare, per capire quanta memoria mi danno 
... ma se c'è una strategia migliore per provare ad allocare il 
massimo, sono curioso di impararla.
ciao
Soviet_Mario

 esatto ! In effetti non è bizzarro se visto in questo modo :
Lo è.

 Lo č.
Beh, mica č obbligatorio produrre simulatori fluidodinamici o 
programmi di modellazione di orbitali molecolari con risoluzioni 
arbitrarie impostate dall'utente.
Devo ammettere che anche in quel contesto, qualche versione free 
x uso privato, tipo chemsketch e chemdraw preferiscono 
l'approccio easy : mi dicono da subito che non vogliono fare 
simulazioni con piů di tot atomi, e stop.
Un vecchio hyperchem invece si cimentava comunque, e forse usava 
il disco, non so, fatto sta che macinava per ore e ore sin 
quando alla fine uno si sfibrava nell'attesa.
Da entrambi preferirei un approccio piů volenteroso (tipo 
arguslab che non dichiara veri e propri limiti predeterminati, 
ma si impossessa di tutto quanto puote, ma che sul mio vecchio 
PC gira da schifo comunque, perché č pensato per macchine piů 
serie).
ciao
soviet_mario


wrote:

La raccomandazione di Stroustrup non ha valenza generale, ma si
riferisce in modo specifico alla libreria standard. Dal momento che i
contenitori standard sono per definizione liberi di spostare i loro
elementi a piacimento, ne consegue che sollevare eccezioni nei
costruttori di copia fa a pugni con l'uso dei suddetti contenitori. Se
non ricordo male in presenza di eccezioni e' garantito uno stato
consistente, ma non necessariamente uno prevedibile.
Ciao,
Nicola

Nicola Musatti ha scritto:

Facendo un esempio, consideriamo un editor che memorizza le righe in un 
contenitore std::list.
Se l'utente inserisce decine di milioni di righe, o se tenta di caricare 
un file contenente decine di milioni di righe, la memoria potrebbe non 
bastare.
Nel caso del caricamento di un file troppo grande, e' ragionevole 
scartare l'intero file, e qui non ci sono problemi di consistenza 
perche' l'intera lista viene distrutta.
Ma supponiamo che l'utente riesca a caricare un file enorme, incominci a 
editarlo, e dopo che ha aggiunto un centinaio di righe la memoria 
risulti insufficiente.
Sarebbe bello consentire all'utente di salvare le proprie modifiche, 
eccetto eventualmente quelle all'ultima riga modificata.
Se la carenza di memoria provoca un deterioramento della struttura dati, 
si corre addirittura il rischio che l'utente (o il programma 
automaticamente) salvi la lista corrotta sul file originale. In tal modo 
non si perderebbero solo le ultime modifiche, ma tutto il file.
Come e' bene prevenire tali disastri?
(Non ditemi "aumentando la memoria", perche' la memoria dei computer non 
la decide lo sviluppatore del software, e perche' qualunque limite di 
memoria puo' essere superato.)
P.S.: L'esempio dell'editor e' solo un esempio, non ditemi che 
normalmente i file di testo non sono troppo grandi.


bastare.
Scusa, ma l'uso delle risorse va sempre in qualche modo ottimizzato e
non è plausibile che un editor mantenga in memoria decine di milioni
di righe. In generale, non ci si dovrebbe mai mettere in condizioni
che la memoria "non basti". Cachare (sì, ammetto che
lessicograficamente non è il massimo)? swappare? 

 Come e' bene prevenire tali disastri?
Mettendo nella lista dei puntatori, normali o smart. In questo modo
prima allochi ed eventualmente gestisci i problemi e poi inserisci
nella struttura dati. Tieni presente che il problema non e' tanto
nell'inserimento di un nuovo elemento, che alla peggio fallisce, ma
nel riordino di strutture, come per esempio la riallocazione di
vettori.
Non dimenticare poi che non e' vietato creare nuove strutture dati.
Avendo l'STL e' stupido farlo senza un motivo piu' che valido, ma se
il motivo c'e'...
Ciao,
Nicola

 ragionevole lanciare un'eccezione bad_alloc in un costruttore di copia?
Perché, secondo me, una eccezione in un costruttore lascia (o potrebbe
lasciare, molto probabilmente) l'oggetto stesso in uno stato
"metastabile": non è costruito se non in parte.

 "metastabile": non è costruito se non in parte.
Non proprio ma quasi: il problema è che per un oggeto parzialmente
costruito il distruttore non è chiamato. Per cui il costruttore deve
essere fatto in modo che se si accartoccia si autoripulisce per i fatti
suoi.
Ad ogni modo non c'è nessuna controindicazione a lanciare un'eccezione
in un costruttore non di copia: una volta che il programmatore ha
scritto il codice in modo exception safe è anzi il modo più furbo per
comunicare al mondo che ci è scoppiato qualcosa in faccia.


 il motivo c'e'...
hash_map... :P
