Si l'on prend le cas d'une fonction mathématique (cad "pure" en info) on pourrait dire que le contrat est que les paramètres sont dans le domaine de définition. Ça n'implique pas en soit l'apparition de démons naseaux si l'appelant est en dehors, et il est tout à fait convenable (et même préférable) de réagir par exemple en levant une exception dans une implémentation.
Quand une fonction n'est pas définie pour tout son domaine, il y a plusieurs solutions :
On réduit le domaine de définition, on rendant les types en entrée plus contraints, par exemple, on pourrait imaginer une fonction division avec le prototype suivant int division(int a, nonZeroInt b), où nonZeroInt est un type int qui ne peut pas contenir zéro. Mais cela force l'utilisateur à convertir et finalement cela repousse le problème plus haut dans le code.
On augmente le domaine de sortie, en mettant par exemple un std::optional en valeur de retour, ainsi division deviendrait std::optional<int> division(int a, int b). Mais on repousse le problème plus bas dans le code.
On lève une exception. C'est ce que tu proposes et dans de nombreux cas c'est une bonne solution, mais je ne peux pas m’empêcher de penser qu'un jour cela va péter au moment où on ne s'y attend pas.
C'est un vrai problème et à chaque cas de figure il y a une solution qui est plus ou moins adaptée.
Une solution que j’apprécie vraiment, mais qui est peu utilisé dans la vraie vie c'est l'utilisation de type rafinés. Tu peux ajouter des contraintes sur les types de ta fonction et laisser le compilateur prouver que ces contraintes ne seront jamais violée. Sinon il peut te monter dans quel cas ton code est faux ou simplement te laisser dans le doute en t’annonçant qu'il n'a pas réussis à faire la preuve. En Haskell il y a Liquid Haskell qui est très amusant à utiliser, mais je n'ai pas de retour sur l'impact que cela peut avoir sur un gros projet.
[^] # Re: contrats
Posté par Guillaum (site web personnel) . En réponse au journal Gestion de l'erreur - C++ - std::optional. Évalué à 2.
Belle traduction, j’apprécie ;)
Quand une fonction n'est pas définie pour tout son domaine, il y a plusieurs solutions :
divisionavec le prototype suivantint division(int a, nonZeroInt b), oùnonZeroIntest un type int qui ne peut pas contenir zéro. Mais cela force l'utilisateur à convertir et finalement cela repousse le problème plus haut dans le code.std::optionalen valeur de retour, ainsidivisiondeviendraitstd::optional<int> division(int a, int b). Mais on repousse le problème plus bas dans le code.C'est un vrai problème et à chaque cas de figure il y a une solution qui est plus ou moins adaptée.
Une solution que j’apprécie vraiment, mais qui est peu utilisé dans la vraie vie c'est l'utilisation de type rafinés. Tu peux ajouter des contraintes sur les types de ta fonction et laisser le compilateur prouver que ces contraintes ne seront jamais violée. Sinon il peut te monter dans quel cas ton code est faux ou simplement te laisser dans le doute en t’annonçant qu'il n'a pas réussis à faire la preuve. En Haskell il y a Liquid Haskell qui est très amusant à utiliser, mais je n'ai pas de retour sur l'impact que cela peut avoir sur un gros projet.