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:
codice:
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:
codice:
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:
codice:
if(!operazione1())
{
// messaggio di errore
// salto alla fine del blocco
}
if(!operazione2())
{
// idem
}
// ... eccetera ...
o anche
codice:
if(operazione1())
{
if(operazione2())
{
if(operazione3())
{
// eccetera
}
else
{
// messaggio di errore
}
}
else
{
// messaggio di errore
}
}
else
{
// messaggio di errore
}
o anche
codice:
// 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):
codice:
#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 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:
Codice PHP:
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 post, dove non si controlla neanche una volta il povero hr ), 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 che si possano trasformare gli errori "vecchio stile" in eccezioni.