Il existe de nombreuses philosophies dans différentes disciplines du génie logiciel sur la façon dont les bibliothèques doivent faire face aux erreurs ou à d'autres conditions exceptionnelles. Quelques-uns de ceux que j'ai vus:
- Renvoie un code d'erreur avec le résultat renvoyé par un argument pointeur. C'est ce que fait PETSc.
- Renvoie les erreurs par une valeur sentinelle. Par exemple, malloc retourne NULL s'il ne peut pas allouer de mémoire,
sqrt
retournera NaN si vous passez un nombre négatif, etc. Cette approche est utilisée dans de nombreuses fonctions libc. - Jetez des exceptions. Utilisé dans deal.II, Trilinos, etc.
- Renvoie un type de variante; par exemple, une fonction C ++ qui renvoie un objet de type
Result
s'il s'exécute correctement et utilise un typeError
pour décrire comment il échouerastd::variant<Error, Result>
. - Utilisez assert et crash. Utilisé dans p4est et certaines parties d'igraph.
Problèmes avec chaque approche:
- La vérification de chaque erreur introduit beaucoup de code supplémentaire. Les valeurs dans lesquelles un résultat sera stocké doivent toujours être déclarées en premier, introduisant de nombreuses variables temporaires qui ne peuvent être utilisées qu'une seule fois. Cette approche explique quelle erreur s'est produite, mais il peut être difficile de déterminer pourquoi ou, pour une pile d'appels profonds, où.
- Le cas d'erreur est facile à ignorer. En plus de cela, de nombreuses fonctions ne peuvent même pas avoir de valeur sentinelle significative si toute la gamme de types de sortie est un résultat plausible. Beaucoup des mêmes problèmes que # 1.
- Uniquement possible en C ++, Python, etc., pas en C ou en Fortran. Peut être imité en C en utilisant la sorcellerie setjmp / longjmp ou libunwind .
- Uniquement possible en C ++, Rust, OCaml, etc., pas en C ou en Fortran. Peut être imité en C en utilisant la macro sorcellerie.
- Sans doute le plus informatif. Mais si vous adoptez cette approche pour, disons, une bibliothèque C pour laquelle vous écrivez ensuite un wrapper Python, une erreur idiote comme passer un index hors limites à un tableau plantera l'interpréteur Python.
Une grande partie des conseils sur Internet concernant la gestion des erreurs est écrite du point de vue des systèmes d'exploitation, du développement intégré ou des applications Web. Les accidents sont inacceptables et vous devez vous soucier de la sécurité. Les applications scientifiques n'ont pas ces problèmes à peu près dans la même mesure, voire pas du tout.
Une autre considération est de savoir quels types d'erreurs sont récupérables ou non. Un échec de malloc n'est pas récupérable et, dans tous les cas, le tueur de mémoire insuffisante du système d'exploitation y parviendra avant vous. Un index hors limites pour une taille de tableau n'est pas récupérable non plus. Pour moi en tant qu'utilisateur, la meilleure chose qu'une bibliothèque puisse faire est de planter avec un message d'erreur informatif. D'autre part, l'échec, par exemple, d'un solveur linéaire itératif à converger pourrait être récupéré en utilisant un solveur à factorisation directe.
Comment les bibliothèques scientifiques doivent-elles signaler les erreurs et s'attendre à ce qu'elles soient traitées? Je réalise bien sûr que cela dépend de la langue dans laquelle la bibliothèque est implémentée. Mais pour autant que je sache, pour toute bibliothèque suffisamment utile, les gens voudront l'appeler à partir d'une autre langue que celle dans laquelle elle est implémentée.
Soit dit en passant, je pense que l'approche # 5 peut être considérablement améliorée pour une bibliothèque C si elle définit un pointeur de fonction de gestionnaire d'assertion global dans le cadre de l'API publique. Par défaut, le gestionnaire d'assertion signale le numéro de fichier / ligne et se bloque. Les liaisons C ++ pour cette bibliothèque définiraient un nouveau gestionnaire d'assertion qui lève à la place une exception C ++. De même, les liaisons Python définiraient un gestionnaire d'assertion qui utilise l'API CPython pour lever une exception Python. Mais je ne connais aucun exemple qui adopte cette approche.
Réponses:
Je vais vous donner ma perspective, qui est encodée dans le projet deal.II que vous référencez.
Tout d'abord, il existe deux types de conditions d'erreur: les erreurs récupérables et les erreurs non récupérables.
Le premier est, par exemple, si un fichier d'entrée ne peut pas être lu - par exemple si vous lisez des informations d'un fichier tel que
$HOME/.dealii
celui-ci peut ou non exister. La fonction de lecture doit simplement revenir à la fonction appelante pour que celle-ci sache quoi faire. Il se peut également qu'une ressource ne soit pas disponible pour le moment mais peut l'être à nouveau dans une minute (un système de fichiers monté à distance).Ce dernier est, par exemple, si vous essayez d'ajouter un vecteur de taille 10 à un vecteur de taille 20: Essayez comme vous pourriez, il n'y a rien qui puisse être fait à ce sujet - il y a un bogue dans le code qui a conduit à le point où nous avons essayé de faire l'ajout.
Ces deux conditions doivent être traitées différemment, quel que soit le langage de programmation que vous utilisez:
Dans le deuxième cas, puisqu'il n'y a pas de recours, mettez fin au programme. Vous pouvez le faire en lançant une exception ou en renvoyant un code d'erreur qui indique à l'appelant que rien ne peut être fait, mais vous pourriez aussi bien abandonner le programme immédiatement car cela facilite grandement la tâche du programmeur pour déboguer le problème.
Dans le premier cas, une situation exceptionnelle s'est produite qui pourrait être traitée. Même si C et Fortran n'avaient aucun moyen d'exprimer cela, tous les langages raisonnables qui sont venus plus tard ont incorporé des moyens dans la norme linguistique pour traiter de tels retours "exceptionnels" en fournissant, eh bien, des "exceptions". Utilisez-les - c'est pour cela qu'ils sont là; ils sont également conçus de telle manière que vous ne pouvez pas oublier de les ignorer (si vous le faites, l'exception se propage juste un niveau plus haut).
En d'autres termes, ce que je préconise ici (et ce que fait deal.II) est un mélange de vos stratégies 3 et 5, selon le contexte. Il est vrai que 3 ne fonctionne pas dans des langues comme C ou Fortran - dans ce cas, on peut affirmer que c'est une bonne raison de ne pas utiliser de langues qui rendent difficile l'expression de ce que vous voulez faire.
Je noterai que certains systèmes ne devraient tout simplement pas tomber en panne, même dans les cas où les erreurs ne sont pas récupérables. Par exemple, un ensemble de fonctions est appelé à plusieurs reprises pour un certain nombre de requêtes, par exemple pour évaluer une fonction de vraisemblance pour des entrées données dans un schéma d'échantillonnage statistique. Peut-être que l'évaluateur ne peut pas traiter les valeurs négatives car le problème n'a aucun sens dans cette situation (par exemple, évaluer la rigidité d'une plaque métallique d'épaisseurX ), mais comme l'évaluateur doit être appelé à plusieurs reprises, il ne doit pas simplement planter, mais simplement lever une exception. Dans de tels cas, même si le passage d'une valeur négative n'est pas récupérable, il convient de lever une exception plutôt que d'interrompre le programme. J'étais en désaccord avec cette position il y a quelques années, mais j'ai changé d'avis après que les directives du logiciel de la communauté xSDK ont codé l'exigence selon laquelle les programmes ne devraient jamais planter (ou du moins devraient avoir un moyen de passer du crash à l'exception - alors accord. II a maintenant la possibilité de faire
Assert
lever une exception au lieu d'appelerabort()
.)la source
std::exception
, et celles-ci peuvent être capturées par référence sans connaître le type dérivé.