Il problema:
Da molto tempo, sono preoccupato per il exceptions
meccanismo, perché sento che non risolve davvero ciò che dovrebbe.
RECLAMO: Ci sono lunghi dibattiti al di fuori di questo argomento, e la maggior parte di essi fatica a confrontare exceptions
vs restituire un codice di errore. Questo non è definitivamente l'argomento qui.
Cercando di definire un errore, sarei d'accordo con CppCoreGuidelines, da Bjarne Stroustrup & Herb Sutter
Un errore indica che la funzione non può raggiungere lo scopo pubblicizzato
RECLAMO: il exception
meccanismo è un linguaggio semantico per la gestione degli errori.
RECLAMO: Per me, non c'è "nessuna scusa" per una funzione per non raggiungere un compito: O abbiamo erroneamente definito le condizioni pre / post in modo che la funzione non possa garantire risultati, o qualche caso eccezionale specifico non è considerato abbastanza importante per passare il tempo nello sviluppo una soluzione. Considerando che, IMO, la differenza tra la gestione del codice normale e del codice di errore è (prima dell'implementazione) una linea molto soggettiva.
RECLAMO: L'uso delle eccezioni per indicare quando una condizione pre o post non è mantenuta è un altro scopo del exception
meccanismo, principalmente a scopo di debug. Non mi rivolgo a questo utilizzo di exceptions
qui.
In molti libri, tutorial e altre fonti, tendono a mostrare la gestione degli errori come una scienza abbastanza obiettiva, che è stata risolta exceptions
e che hai solo bisogno di catch
loro per avere un software robusto, in grado di recuperare da qualsiasi situazione. Ma i miei diversi anni come sviluppatore mi fanno vedere il problema da un approccio diverso:
- I programmatori tendono a semplificare il loro compito generando eccezioni quando il caso specifico sembra troppo raro per essere implementato con attenzione. Casi tipici di questo sono: problemi di memoria insufficiente, problemi di disco pieno, problemi di file danneggiati, ecc. Ciò potrebbe essere sufficiente, ma non viene sempre deciso a livello di architettura.
- I programmatori tendono a non leggere attentamente la documentazione relativa alle eccezioni nelle librerie e di solito non sono consapevoli di quale e quando una funzione viene generata. Inoltre, anche quando lo sanno, non li gestiscono davvero.
- I programmatori tendono a non catturare le eccezioni abbastanza presto e, quando lo fanno, è soprattutto per accedere e lanciare ulteriormente. (fare riferimento al primo punto).
Ciò ha due conseguenze:
- Gli errori che si verificano frequentemente vengono rilevati all'inizio dello sviluppo e sottoposti a debug (il che è positivo).
- Le rare eccezioni non vengono gestite e causano l'arresto anomalo del sistema (con un bel messaggio di registro) nella home dell'utente. Alcune volte viene segnalato l'errore, o nemmeno.
Considerato ciò, l'IMO lo scopo principale di un meccanismo di errore dovrebbe essere:
- Rendi visibile nel codice in cui alcuni casi specifici non sono gestiti.
- Comunicare il runtime del problema al codice correlato (almeno il chiamante) quando si verifica questa situazione.
- Fornisce meccanismi di recupero
Il principale difetto della exception
semantica come meccanismo di gestione degli errori è l'IMO: è facile vedere dove si throw
trova a nel codice sorgente, ma non è assolutamente evidente sapere se una funzione specifica potrebbe essere lanciata guardando la dichiarazione. Questo porta tutto il problema che ho introdotto sopra.
Il linguaggio non impone e controlla il codice di errore in modo rigoroso come fa per altri aspetti del linguaggio (ad esempio, tipi di variabili forti)
Un tentativo di soluzione
Con l'intenzione di migliorare questo, ho sviluppato un sistema di gestione degli errori molto semplice, che cerca di mettere la gestione degli errori allo stesso livello di importanza del normale codice.
L'idea è:
- Ogni funzione (rilevante) riceve un riferimento a un
success
oggetto molto leggero e, nel caso, può impostarlo su uno stato di errore. L'oggetto è molto leggero fino a quando non viene salvato un errore con il testo. - Una funzione è incoraggiata a saltare il suo compito se l'oggetto fornito contiene già un errore.
- Un errore non deve mai essere ignorato.
Il design completo ovviamente considera attentamente ogni aspetto (circa 10 pagine), anche come applicarlo a OOP.
Esempio della Success
classe:
class Success
{
public:
enum SuccessStatus
{
ok = 0, // All is fine
error = 1, // Any error has been reached
uninitialized = 2, // Initialization is required
finished = 3, // This object already performed its task and is not useful anymore
unimplemented = 4, // This feature is not implemented already
};
Success(){}
Success( const Success& v);
virtual ~Success() = default;
virtual Success& operator= (const Success& v);
// Comparators
virtual bool operator==( const Success& s)const { return (this->status==s.status && this->stateStr==s.stateStr);}
virtual bool operator!=( const Success& s)const { return (this->status!=s.status || this->stateStr==s.stateStr);}
// Retrieve if the status is not "ok"
virtual bool operator!() const { return status!=ok;}
// Retrieve if the status is "ok"
operator bool() const { return status==ok;}
// Set a new status
virtual Success& set( SuccessStatus status, std::string msg="");
virtual void reset();
virtual std::string toString() const{ return stateStr;}
virtual SuccessStatus getStatus() const { return status; }
virtual operator SuccessStatus() const { return status; }
private:
std::string stateStr;
SuccessStatus status = Success::ok;
};
Uso:
double mySqrt( Success& s, double v)
{
double result = 0.0;
if (!s) ; // do nothing
else if (v<0.0) s.set(Error, "Square root require non-negative input.");
else result = std::sqrt(v);
return result;
}
Success s;
mySqrt(s, 144.0);
otherStuff(s);
saveStuff(s);
if (s) /*All is good*/;
else cout << s << endl;
L'ho usato in molti dei miei (propri) codici e ho costretto il programmatore (io) a riflettere ulteriormente su possibili casi eccezionali e su come risolverli (bene). Tuttavia, ha una curva di apprendimento e non si integra bene con il codice che ora lo utilizza.
La domanda
Vorrei capire meglio le implicazioni dell'uso di un tale paradigma in un progetto:
- La premessa al problema è corretta? o ho perso qualcosa di rilevante?
- La soluzione è una buona idea architettonica? o il prezzo è troppo alto?
MODIFICARE:
Confronto tra metodi:
//Exceptions:
// Incorrect
File f = open("text.txt"); // Could throw but nothing tell it! Will crash
save(f);
// Correct
File f;
try
{
f = open("text.txt");
save(f);
}
catch( ... )
{
// do something
}
//Error code (mixed):
// Incorrect
File f = open("text.txt"); //Nothing tell you it may fail! Will crash
save(f);
// Correct
File f = open("text.txt");
if (f) save(f);
//Error code (pure);
// Incorrect
File f;
open(f, "text.txt"); //Easy to forget the return value! will crash
save(f);
//Correct
File f;
Error er = open(f, "text.txt");
if (!er) save(f);
//Success mechanism:
Success s;
File f;
open(s, "text.txt");
save(s, f); //s cannot be avoided, will never crash.
if (s) ... //optional. If you created s, you probably don't forget it.