PDA

Visualizza la versione completa : try-throw-catch vs set_error_handler?


xnavigator
04-10-2010, 23:02
allora ragazzi proprio non riesco ad entrare nell'ottica di questo try-throw-catch


nel mio framework ho creato una funzione personalizzata per la gestione degli errori (appunto con set_error_handler)

quando voglio generare un errore da me richiamo la funzione trigger_error come spiegato anche nell'esempio:
http://it.php.net/manual/en/function.set-error-handler.php

il codice mi sembra 100 volte più chiaro che ogni volta mettere il codice che potrebbe avere un errore in un blocco try-throw-catch.

voi usate quale dei 2 usate?

e soprattutto quali sono i vantaggi ad usare sto try-catch

xnavigator
04-10-2010, 23:03
ops pensavo stessi nella sezione di PHP, vabbè credo si possa considerare una domanda da programmazione generale cmq :ciauz:

xnavigator
15-10-2010, 13:04
up

MItaly
15-10-2010, 13:31
Una routine di gestione globale degli errori secondo me in generale è una pessima idea, perché dovrebbe saper gestire qualunque errore generato da qualunque parte del programma, compito impossibile se la gestione si limita ad essere qualcosa di più di un "logga l'errore e termina l'applicazione". Il compito di una funzione del genere è quello che avrebbe un catchall impostato a livello di entrypoint in un'applicazione tradizionale (dotata, appunto, di un entrypoint):


#include<iostream>
// ...

int main()
{
try
{
// qui inizia il programma
// ...
}
catch(std::exception & ex)
{
// Se l'eccezione ha risalito lo stack ed è arrivata fin qui c'è davvero poco da fare
std::cerr<<"Eccezione non gestita, morte totale."<<std::endl;
std::cerr<<ex.what()<<std::endl;
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}

Un error handler globale invece in un ambiente in cui le eccezioni sono usate anche per segnalare errori "normali" (non solo catastrofi inevitabili) è pessimo, dato che non consente di gestire eccezioni che altrimenti sarebbero gestibili o persino attese.
Supponi di avere delle stringhe da cui estrarre dei numeri recuperati, ad esempio, da delle caselle di testo (per cui potenzialmente ci potrebbe essere dentro di tutto): se la conversione è effettuata da un componente che usa le eccezioni per segnalare errori di parsing, senza try ... catch sei fregato, perché l'errore arriverebbe fino all'handler globale, che non farebbe altro che terminare l'applicazione, mentre gestire subito l'eccezione ti consente di prendere contromisure:


std::vector<std::string> in;
std::vector<int> output ;
try
{
for(auto it=in.begin(); it!=in.end(); it++)
output.push_back(boost::lexical_cast<int>(in));
}
catch(boost::bad_lexical_cast & ex)
{
// una delle stringhe contiene spazzatura; segnala il fatto all'utente

}

Nota che tutte le altre eventuali eccezioni che non sappiamo gestire vengono lasciate andare, e risalgono lo stack finché non incontrano un blocco catch che le sappia gestire (al massimo arrivano al catchall piazzato nell'entrypoint, o, se questo non c'è, be', va in crash il programma :stordita: ).

xnavigator
15-10-2010, 13:35
ma perchè non sarebbe meglio fare cosi:



for(auto it=in.begin(); it!=in.end(); it++) {
if (contieneSpazzatura(in))
//segnala
else
output.push_back(boost::lexical_cast<int>(in));
}



cioè io non oso immaginare quanta "complessità" possa aggiungere ogni volta questo try catch se lo usaimo spesso in un pezzo di codice =/

thx per i chiarimetni

MItaly
15-10-2010, 14:11
Perché è una seccatura e in linea di massima è uno spreco. :)
Spesso l'unico metodo per capire se è possibile fare qualcosa è provare; in casi quello dell'esempio, bisognerebbe di fatto effettuare il parsing due volte, il che è uno spreco in termini di performance.
In altri casi non è proprio possibile distinguere le due operazioni: pensa all'apertura di un file: l'unico modo per sapere se è possibile farlo è provare ad aprirlo e vedere se si riesce oppure se il sistema operativo risponde picche. Chiedere prima al SO "posso aprirlo?" e, se si ottiene una riposta positiva, effettuare l'operazione aspettandosi che non fallisca non è una buona idea, dato, che nei sistemi operativi attuali (multitasking preemptive) il tuo programma può essere interrotto a qualunque momento per lasciare spazio ad un altro processo, il quale può, ad esempio, cambiare i permessi sul file in questione tra il tuo controllo e l'apertura.
Si crea cioè una race condition tra il momento del controllo e quello dell'apertura, per cui, se ci si può aspettare che l'apertura fallisca anche se si è controllato prima, tanto vale evitare il controllo e gestire il problema al momento dell'apertura.

La principale alternativa alle eccezioni è piuttosto la gestione degli errori a "return code" (tipica del C e dei linguaggi procedurali), che tuttavia è una gran rottura di scatole, dal momento che ogni funzione può avere solo un valore restituito, per cui se questo se ne va per la segnalazione di eventuali errori bisogna ricorrere a parametri passati per riferimento per ottenere il "vero" valore che la funzione restituisce, senza contare che bisognerebbe controllare *ogni* valore restituito. Se ad esempio il lexical_cast funzionasse in quella maniera, il codice in questione sarebbe molto meno leggibile:


for(auto it=in.begin(); it!=in.end(); it++)
{
int temp;
if(boost::lexical_cast<int>(in, temp)) // supponendo che accetti un secondo parametro reference per il risultato e restituisca false in caso di conversione fallita
output.push_back(temp);
else
{
// gestisci l'errore
}
}

O ancora, se ci fossero una serie di operazioni che possono fallire, confronta l'idioma ad eccezioni:


try
{
operazione1();
operazione2();
operazione3();
operazione4();
operazione5();
}
catch(std::exception & ex)
{
std::cout<<"Si è verificato un errore tragico; impossibile brematurare l'antani."<<std::endl
<<"Dettagli errore: "<<ex.what()<<std::endl;
}

con quello a codici restituiti:


if(!operazione1())
{
// messaggio di errore
// salto alla fine del blocco
}
if(!operazione2())
{
// idem
}
// ... eccetera ...

o anche


if(operazione1())
{
if(operazione2())
{
if(operazione3())
{
// eccetera
}
else
{
// messaggio di errore
}
}
else
{
// messaggio di errore
}
}
else
{
// messaggio di errore
}

o anche


// questo può funzionare *solo* se abbiamo *solo* chiamate a funzioni senza nulla in mezzo e tutte possono essere riportate ad un bool
if(!(operazione1() && operazione2() && operazione3() && operazione4()&&operazione5()))
{
// messaggio di errore
}

o (spesso presente in C):


#define MUST_SUCCEED(x) if(!(x)) goto error:

/* ... */
MUST_SUCCEED(operazione1());
MUST_SUCCEED(operazione2());
MUST_SUCCEED(operazione3());
MUST_SUCCEED(operazione4());
MUST_SUCCEED(operazione5());
goto end:
error:
/* messaggio di errore */
end:


Inoltre le eccezioni risalgono lo stack automaticamente andandosi a cercare un gestore che le sappia gestire, e, nei linguaggi che conosco, risalendo lo stack vengono distrutti correttamente gli oggetti su di esso allocati (è ciò che sta alla base dell'idioma RAII (http://en.wikipedia.org/wiki/Resource_Acquisition_Is_Initialization) in C++, e in linguaggi tipo C# esso è implementato tramite i blocchi using). Questo consente di evitare leak di risorse, qualunque percorso la funzione prenda nell'uscire: considera una funzione di questo genere:


function UnaFunzioneACaso()
{
$handle=fopen(/* ... */);
// ...
if(UnaCondizioneACaso)
{
// C'è un errore per cui non si può continuare
return FALSE; // <-- ooops non abbiamo chiuso $handle, questo resterà aperto per tutta la durata dello script
}
fclose($handle);
return TRUE;
}

Se iniziano ad esserci più punti d'uscita la questione si fa spinosa, e infatti spesso in linguaggi come il C si finisce con fare accrocchi orribili basati su goto (spesso nascosti in macro) e altre cose malvagie per fare sì che, se si verifica un errore, venga eseguito sempre il codice di cleanup prima di uscire. Incapsulare le risorse in oggetti allocati sullo stack (che vengono distrutti correttamente in qualunque maniera si esca dalla funzione) consente di evitare questo genere di resource leak.

In ogni caso, le eccezioni, almeno in C++, sono state introdotte perché i costruttori delle classi non possono restituire alcunché (il che in effetti è sensato, almeno dal punto di vista della sintassi), per cui, anche volendo, non è possibile segnalare errori tramite valore restituito. Esiste un vecchio idioma che prevede che il costruttore non possa fallire, mentre l'inizializzazione avviene in un qualche metodo init che può restituire un codice di errore, ma perché tutto questo funzioni correttamente bisogna mantenere un qualche flag interno alla classe che segnali se la classe è stata costruita correttamente, il che è una gran rottura di scatole sia per l'implementatore che per l'utilizzatore. L'uso di eccezioni direttamente nel costruttore consente di fare sì che, se c'è un errore, l'oggetto non esista nemmeno più (esce sicuramente di scope), mentre se le cose vanno bene l'oggetto si trova sicuramente in uno stato consistente.

L'idea di fondo delle eccezioni comunque è che la notifica degli errori non sia un opt-in (=devo controllare esplicitamente i valori restituiti, e nel 90% dei casi non lo faccio perché sono pigro :) => vedi ad esempio questo (http://forum.html.it/forum/showthread.php?s=&postid=13124656#post13124656) post, dove non si controlla neanche una volta il povero hr :D ), ma un opt-out (di base se non gestisco gli errori esplode tutto, se proprio voglio ignorarne uno devo mettere appositamente un blocco catch vuoto); questo in linea di massima dovrebbe aiutare a costruire programmi più robusti, evitando che gli errori passino inosservati. Non a caso tutti i linguaggi più moderni (vedi ad esempio Java, tutti i linguaggi .NET, Python, Ruby, in una certa misura PHP5, ...) basano la gestione degli errori sulle eccezioni.

Nota che tra l'altro set_error_handler e il try...catch in PHP assolvono funzioni differenti: set_error_handler gestisce errori generati da trigger_error (che, come meccanismo, ricorda vagamente la gestione degli errori del BASIC e di VB classico), mentre i try ... catch gestiscono gli errori generati tramite throw. Non a caso vedo che è previsto (http://it.php.net/manual/en/class.errorexception.php) che si possano trasformare gli errori "vecchio stile" in eccezioni.

xnavigator
15-10-2010, 14:13
panico dammi 1min leggo e dammene altri 10 per rispondere lol


thx intanto

MItaly
15-10-2010, 14:22
Originariamente inviato da xnavigator
panico dammi 1min leggo e dammene altri 10 per rispondere lol

:D (tra l'altro ho editato)


thx intanto
Figurati. :)

MItaly
15-10-2010, 14:43
Per quanto concerne il "costo" delle eccezioni, in linea di massima se nessuna eccezione viene sollevata l'unico svantaggio che si ha è in termini di dimensioni dell'eseguibile leggermente superiori, poiché è necessario aggiungere informazioni sui tipi delle eccezioni impiegati e su cosa ciascun blocco catch gestisce (parlo di C++, non so come vengano implementate in PHP).
Non viene infatti effettuato un controllo ad ogni momento per vedere se qualcuno ha generato un'eccezione, ma, nel momento in cui viene sollevata un'eccezione (tramite throw) una routine della CRT usando magia nera risale lo stack, alla ricerca del più vicino blocco try che la può gestire. Questa operazione può non essere veloce come un semplice return+if (specialmente se il codice in questione non è ancora caricato in memoria), ma credo che nella stragrande maggioranza dei casi sia praticamente impercettibile (inoltre il compilatore è solitamente in grado di ottimizzare diversi casi in cui le eccezioni sono usate in maniera "semplice").
A questo proposito, per quanto concerne C++ ti segnalo due link: http://stackoverflow.com/questions/307610/how-do-exceptions-work-behind-the-scenes-in-c e http://www.codeproject.com/KB/cpp/exceptionhandler.aspx?fid=3666&df=90&mpp=25&noise=3&sort=Position&view=Quick&fr=51

xnavigator
15-10-2010, 15:34
Originariamente inviato da MItaly

O ancora, se ci fossero una serie di operazioni che possono fallire, confronta l'idioma ad eccezioni:


try
{
operazione1();
operazione2();
operazione3();
operazione4();
operazione5();
}
catch(std::exception & ex)
{
std::cout<<"Si è verificato un errore tragico; impossibile brematurare l'antani."<<std::endl
<<"Dettagli errore: "<<ex.what()<<std::endl;
}

con quello a codici restituiti:


if(!operazione1())
{
// messaggio di errore
// salto alla fine del blocco
}
if(!operazione2())
{
// idem
}
// ... eccetera ...




questo sopra secondo me è l'esempio da prendere in considerzione... io come puoi capire utilizzo il secondo metodo, che mi permette di gestire anche in maniera opportuna operazioni particolari per ciascun errore...

ora nel caso per esempio usando try & catch e la funzione operazione4 mi restituisce un errore come faccio nel blocco catch ad eseguire una particolare istruzione che voglio eseguire solo nel caso in cui operazione4 fallisce (fallisce nel senso restituisce false o un valore < 0 per distinguere l'errore)?

non so se mi sono spiegato :D

per quanto riguarda lo scheduler preempitve allora li riesco ad inutire l'utilità del try catch (controllare se esiste un file -> poi effettuare operazione di scrittura) però questo è un caso abbastanza limite credo, quindi lasciamolo fuori

Loading