Le problème:
Depuis longtemps, je m'inquiète du exceptions
mécanisme, car je pense qu'il ne résout pas vraiment ce qu'il devrait.
RÉCLAMATION: Il y a de longs débats à l'extérieur sur ce sujet, et la plupart d'entre eux ont du mal à comparer exceptions
vs renvoyer un code d'erreur. Ce n'est définitivement pas le sujet ici.
Essayer de définir une erreur, je suis d'accord avec CppCoreGuidelines, de Bjarne Stroustrup & Herb Sutter
Une erreur signifie que la fonction ne peut pas atteindre son objectif annoncé
RÉCLAMATION: Le exception
mécanisme est un langage sémantique pour gérer les erreurs.
RÉCLAMATION: Pour moi, il n'y a "aucune excuse" à une fonction pour ne pas accomplir une tâche: Soit nous avons mal défini les conditions pré / post afin que la fonction ne puisse pas garantir des résultats, soit un cas exceptionnel spécifique n'est pas considéré comme suffisamment important pour passer du temps à développer une solution. Considérant que, l'OMI, la différence entre le code normal et la gestion du code d'erreur est (avant la mise en œuvre) une ligne très subjective.
RÉCLAMATION: L'utilisation d'exceptions pour indiquer quand une condition pré ou post n'est pas conservée est un autre objectif du exception
mécanisme, principalement à des fins de débogage. Je ne cible pas cette utilisation d' exceptions
ici.
Dans de nombreux livres, tutoriels et autres sources, ils ont tendance à montrer que la gestion des erreurs est une science assez objective, qui est résolue exceptions
et que vous avez juste besoin d' catch
eux pour avoir un logiciel robuste, capable de récupérer de n'importe quelle situation. Mais mes quelques années en tant que développeur me font voir le problème d'une approche différente:
- Les programmeurs ont tendance à simplifier leur tâche en lançant des exceptions lorsque le cas spécifique semble trop rare pour être mis en œuvre avec soin. Les cas typiques sont les suivants: problèmes de mémoire insuffisante, problèmes de saturation de disque, problèmes de fichiers corrompus, etc. Cela peut être suffisant, mais n'est pas toujours décidé au niveau architectural.
- Les programmeurs ont tendance à ne pas lire attentivement la documentation sur les exceptions dans les bibliothèques, et ne savent généralement pas à quel moment et quand une fonction est lancée. De plus, même quand ils le savent, ils ne les gèrent pas vraiment.
- Les programmeurs ont tendance à ne pas détecter les exceptions assez tôt, et lorsqu'ils le font, c'est surtout pour se connecter et lancer plus loin. (se référer au premier point).
Cela a deux conséquences:
- Les erreurs qui se produisent fréquemment sont détectées tôt dans le développement et déboguées (ce qui est bien).
- De rares exceptions ne sont pas gérées et font planter le système (avec un joli message de log) au domicile de l'utilisateur. Parfois, l'erreur est signalée, ou même pas.
Considérant que l'OMI, l'objectif principal d'un mécanisme d'erreur devrait être:
- Rendre visible dans le code où certains cas spécifiques ne sont pas gérés.
- Communiquez le runtime du problème au code associé (au moins à l'appelant) lorsque cette situation se produit.
- Fournit des mécanismes de récupération
L' exception
IMO est le principal défaut de la sémantique en tant que mécanisme de gestion des erreurs: il est facile de voir où se throw
trouve a dans le code source, mais il n'est absolument pas évident de savoir si une fonction spécifique pourrait se lancer en regardant la déclaration. Cela apporte tout le problème que j'ai présenté ci-dessus.
Le langage n'applique pas et ne vérifie pas le code d'erreur aussi strictement qu'il le fait pour d'autres aspects du langage (par exemple, des types de variables forts)
Un essai de solution
Dans le but d'améliorer cela, j'ai développé un système de gestion des erreurs très simple, qui essaie de mettre la gestion des erreurs au même niveau d'importance que le code normal.
L'idée est:
- Chaque fonction (pertinente) reçoit une référence à un
success
objet très léger et peut lui attribuer un état d'erreur au cas où. L'objet est très léger jusqu'à ce qu'une erreur de texte soit enregistrée. - Une fonction est encouragée à ignorer sa tâche si l'objet fourni contient déjà une erreur.
- Une erreur ne doit jamais être annulée.
Le design complet prend évidemment en considération chaque aspect (environ 10 pages), ainsi que la façon de l'appliquer à la POO.
Exemple de 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;
};
Usage:
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;
J'ai utilisé cela dans beaucoup de mon (propre) code et cela oblige le programmeur (moi) à réfléchir davantage aux cas exceptionnels possibles et à la façon de les résoudre (bien). Cependant, il a une courbe d'apprentissage et ne s'intègre pas bien avec le code qui l'utilise maintenant.
La question
J'aimerais mieux comprendre les implications de l'utilisation d'un tel paradigme dans un projet:
- La prémisse du problème est-elle correcte? ou ai-je raté quelque chose de pertinent?
- La solution est-elle une bonne idée architecturale? ou le prix est trop élevé?
MODIFIER:
Comparaison entre les méthodes:
//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.