Pour certains langages (par exemple C++), la fuite de ressources ne devrait pas être une raison.
Le C++ est basé sur le RAII.
Si votre code est susceptible d'échouer, de retourner ou de lancer (c'est-à-dire la plupart des codes normaux), votre pointeur doit être enveloppé dans un pointeur intelligent (en supposant que vous ayez une balise très bon raison de ne pas créer votre objet sur la pile).
Les codes de retour sont plus verbeux
Elles sont verbeuses et ont tendance à se transformer en quelque chose comme.. :
if(doSomething())
{
if(doSomethingElse())
{
if(doSomethingElseAgain())
{
// etc.
}
else
{
// react to failure of doSomethingElseAgain
}
}
else
{
// react to failure of doSomethingElse
}
}
else
{
// react to failure of doSomething
}
En fin de compte, votre code est une collection d'instructions identifiées (j'ai vu ce type de code dans le code de production).
Ce code pourrait être traduit par :
try
{
doSomething() ;
doSomethingElse() ;
doSomethingElseAgain() ;
}
catch(const SomethingException & e)
{
// react to failure of doSomething
}
catch(const SomethingElseException & e)
{
// react to failure of doSomethingElse
}
catch(const SomethingElseAgainException & e)
{
// react to failure of doSomethingElseAgain
}
qui séparent proprement le traitement du code et des erreurs, qui puede soit un bon chose.
Les codes de retour sont plus fragiles
S'il ne s'agit pas d'un obscur avertissement d'un compilateur (voir le commentaire de "phjr"), ils peuvent facilement être ignorés.
Avec les exemples ci-dessus, supposons que quelqu'un oublie de gérer son erreur éventuelle (cela arrive...). L'erreur est ignorée lorsqu'elle est "retournée", et risque d'exploser plus tard (par exemple, un pointeur NULL). Le même problème ne se produira pas avec une exception.
L'erreur ne sera pas ignorée. Parfois, vous voulez qu'elle n'explose pas, cependant... Il faut donc choisir avec soin.
Les codes de retour doivent parfois être traduits
Supposons que nous ayons les fonctions suivantes :
- doSomething, qui peut retourner un int appelé NOT_FOUND_ERROR
- doSomethingElse, qui peut retourner un bool "false" (en cas d'échec)
- doSomethingElseAgain, qui peut renvoyer un objet Error (avec les variables __LINE__, __FILE__ et la moitié de la pile).
- doTryToDoSomethingWithAllThisMess qui, eh bien... Utiliser les fonctions ci-dessus, et renvoyer un code d'erreur de type...
Quel est le type de retour de doTryToDoSomethingWithAllThisMess si l'une de ses fonctions appelées échoue ?
Les codes de retour ne sont pas une solution universelle
Les opérateurs ne peuvent pas renvoyer de code d'erreur. Les constructeurs C++ ne le peuvent pas non plus.
Les codes de retour signifient que vous ne pouvez pas enchaîner les expressions.
Le corollaire du point précédent. Et si je veux écrire :
CMyType o = add(a, multiply(b, c)) ;
Je ne peux pas, car la valeur de retour est déjà utilisée (et parfois, elle ne peut pas être modifiée). La valeur de retour devient donc le premier paramètre, envoyé en tant que référence... Ou pas.
Les exceptions sont typées
Vous pouvez envoyer des classes différentes pour chaque type d'exception. Les exceptions relatives aux ressources (c'est-à-dire la perte de mémoire) doivent être légères, mais toutes les autres peuvent être aussi lourdes que nécessaire (j'aime bien que l'exception Java me donne toute la pile).
Chaque prise peut ensuite être spécialisée.
N'utilisez jamais catch(...) sans relancer la procédure
En général, vous ne devez pas cacher une erreur. Si vous ne relancez pas l'erreur, au moins, enregistrez l'erreur dans un fichier, ouvrez une boîte de message, etc...
Les exceptions sont... NUKE
Le problème avec les exceptions est qu'une utilisation excessive produit un code plein de try/catches. Mais le problème est ailleurs : Qui fait des try/catch dans son code en utilisant un conteneur STL ? Pourtant, ces conteneurs peuvent envoyer une exception.
Bien sûr, en C++, il ne faut jamais laisser une exception sortir d'un destructeur.
Les exceptions sont... synchrones
Assurez-vous de les attraper avant qu'ils ne mettent votre fil d'exécution à genoux ou qu'ils ne se propagent dans votre boucle de messages Windows.
La solution pourrait être de les mélanger ?
Je suppose donc que la solution consiste à lancer un message lorsque quelque chose devrait no se produisent. Et lorsque quelque chose peut se produire, il faut utiliser un code de retour ou un paramètre pour permettre à l'utilisateur de réagir.
La seule question qui se pose est donc la suivante : "Qu'est-ce qui ne devrait pas se produire ?".
Cela dépend du contrat de votre fonction. Si la fonction accepte un pointeur, mais spécifie que le pointeur doit être non-NULL, alors il est normal de lancer une exception lorsque l'utilisateur envoie un pointeur NULL (la question étant, en C++, quand l'auteur de la fonction n'a-t-il pas utilisé des références au lieu de pointeurs, mais...).
Une autre solution consisterait à afficher l'erreur
Parfois, votre problème est que vous ne voulez pas d'erreurs. L'utilisation d'exceptions ou de codes de retour d'erreur est intéressante, mais... Vous voulez le savoir.
Dans mon travail, nous utilisons une sorte de "Assert". Il va, en fonction des valeurs d'un fichier de configuration, peu importe les options de compilation debug/release :
- enregistrer l'erreur
- ouvrir une boîte de message avec un "Hey, you have a problem" (Hé, vous avez un problème)
- ouvrir une boîte de message avec un "Hey, you have a problem, do you want to debug" (Hé, vous avez un problème, voulez-vous déboguer)
Dans le cadre du développement et des tests, cela permet à l'utilisateur de localiser le problème exactement au moment où il est détecté, et non après (lorsqu'un code se préoccupe de la valeur de retour, ou à l'intérieur d'un code de rattrapage).
Il est facile de l'ajouter au code existant. Par exemple :
void doSomething(CMyObject * p, int iRandomData)
{
// etc.
}
conduit à une sorte de code similaire à :
void doSomething(CMyObject * p, int iRandomData)
{
if(iRandomData < 32)
{
MY_RAISE_ERROR("Hey, iRandomData " << iRandomData << " is lesser than 32. Aborting processing") ;
return ;
}
if(p == NULL)
{
MY_RAISE_ERROR("Hey, p is NULL !\niRandomData is equal to " << iRandomData << ". Will throw.") ;
throw std::some_exception() ;
}
if(! p.is Ok())
{
MY_RAISE_ERROR("Hey, p is NOT Ok!\np is equal to " << p->toString() << ". Will try to continue anyway") ;
}
// etc.
}
(J'ai des macros similaires qui ne sont actives qu'en cas de débogage).
Notez qu'en production, le fichier de configuration n'existe pas, donc le client ne voit jamais le résultat de cette macro... Mais il est facile de l'activer en cas de besoin.
Conclusion
Lorsque vous codez en utilisant des codes de retour, vous vous préparez à l'échec et vous espérez que votre forteresse de tests est suffisamment sûre.
Lorsque vous codez en utilisant des exceptions, vous savez que votre code peut échouer, et vous placez généralement des contre-feux à des endroits stratégiques choisis dans votre code. Mais en général, votre code est plus axé sur "ce qu'il doit faire" que sur "ce que je crains qu'il ne se produise".
Mais quand on code, il faut utiliser le meilleur outil à sa disposition, et parfois, c'est "Ne jamais cacher une erreur, et la montrer dès que possible". La macro dont j'ai parlé plus haut suit cette philosophie.
1 votes
Un problème de poule ou d'œuf dans le monde du logiciel... éternellement discutable :)
0 votes
Je m'en excuse, mais j'espère que le fait d'avoir une variété d'opinions aidera les gens (moi y compris) à faire un choix approprié.