Mokona Guu Center

Asserter n'est pas jouer

Publié le

La fonction assert(), dans la bibliothèque C++, aussi présente dans d'autres langages, comme le C# ou le Java, est une fonction qui vérifie une condition et qui arrête le programme“dans son comportement initial, souvent redéfini.” si la condition n'est pas évaluée à « vrai », avec un message.

Cette fonction est une version appauvrie des pré/post conditions et invariants qui se trouvent dans d'autres langages de programmation (Eiffel, Caml, D,...). Appauvrie car sans cadre d'utilisation, et, utilisée par mimétisme par beaucoup de programmeurs, d'utilisation souvent mauvaise. Les assert() minent le terrain, on les trouve à chaque recoin de code. Parfois pour le mieux, souvent pour le pire.

    int addValue(int value)
    {
        assert(value > 0); // Vraiment ?
        ...
    }

Les pré/posts conditions et invariants font partie de la programmation par contrat. La programmation par contrat consiste à poser des conditions à l'utilisation d'une fonction. L'exécution de telle fonction n'est valide que si les paramètres d'entrée sont tels ou tels, la sortie est de telle forme et que telles et telles valeurs respectent d'autres conditions pendant le déroulement (au moins à l'entrée et à la sortie de la fonction).

Un exemple classique est une fonction racine carrée dont la condition de sortie (post-condition) pourrait être que la valeur de retour au carré est égale à la valeur d'entrée.

    float sqrt(float entry)
    {
        ... // Calcul de la racine carrée
        assert(root * root == entry); // Probablement que l'on vérifierait plutôt à un espilon près
        return root;
    }

Mais doit-on aussi avoir une contrainte qui arrêterait le programme si la valeur d'entrée était négative ?

    float sqrt(float entry)
    {
        // La fonction ne renvoie que des résultats réels.
        assert(entry >= 0);
        ... // Calcul de la racine carrée\
        return root;
    }

Dans ce cas-ci, c'est probable.

Dans le cas général que l'on trouve malheureusement beaucoup dans du code de production, comme le premier exemple de l'article, non !

Un assert() est fait pour vérifier la logique interne d'une fonction ou d'un ensemble de fonctions (que je nommerai « système » dans la suite de l'article). À partir de données valides en entrée de ce système, les contrats (les asserts) sont là pour vérifier que la logique du programmeur est respectée.

La protection qu'offre les assert() est bien une protection contre le programmeur : j'écris une fonction avec une domaine de validité en tête. Mon cheminement me semble bon. Cependant, je laisse un assert() en tant que pré/post condition ou invariant (c'est-à-dire exclusivement en début ou en fin de fonction) pour être certain d'être prévenu si ma logique se révélait en pratique ne pas être complètement bonne.

Cette pratique vient en supplément des tests unitaires, avec lesquels on a pu oublier un test aux limites par exemple.

Je remets ici la partie importante : les données en entrées du système sont valides !

Le rôle d'un assert() n'est pas de vérifier les données externes. Celles-ci doivent être vérifiées, filtrées, refusées, récupérées,... depuis la source externe avant l'injection dans le système. Arrivées aux portes du système, elles sont valides.

Imaginons en prenant un peu d'altitude que The Gimp vérifie la validité d'un fichier ouvert avec des asserts. Cela signifierait qu'au lieu d'une boite d'alerte qui afficherait que le fichier est dans un format non reconnu ou corrompu (suivant l'erreur trouvée), The Gimp s'arrêterait sur une erreur obscure, voire s'arrêterait brutalement.

C'est exactement ce qui est fait à plus ou moins grande échelle lorsqu'un système est protégée de données externes par des assert(). Ce système peut être un programme complet et les données externes les données sérialisées. Ce système peut aussi être une partie autonome d'un programme dont les données externes sont les données reçues des autres systèmes du même programme.

Un assert() protège le code de programmeurs. Un assert() n'est pas fait pour protéger le code des données reçues.

« J'ai protégé cette section par un assert »

Avez-vous déjà entendu cette phrase révélatrice ? Cette phrase qui indique qu'un système est protégé des données reçues par un assert ? Sûrement. Un signal d'alarme devrait retentir lors d'une revue de code où cette phrase est prononcée.

Souvent, cela signifie qu'il manque un système de remontée et/ou récupération des erreurs à l'utilisateur, ou bien un système de validation des données et notification à un système appelant, suivant la profondeur du code dans l'application.

Si l'assert() est censé protéger de données qui sont lues depuis un autre système, sans que cet autre système soit au courant, c'est encore pire. L'autre système n'ayant pas de contrat avec le notre, il n'a pas de raison de générer des données valides pour nous. Poser un assert() sur ces données, c'est écrire un contrat unilatéral. C'est aberrant.

Comment est-ce que l'utilisation des assert() est-il arrivé à ce point de dévoiement ?

Le point initial est le manque d'une politique de remontée des erreurs. Le système existe souvent pour les erreurs à remonter à l'utilisateur, mais il n'est pas utilisé, ou peu, ou mal. Il n'existe par contre pas toujours pour les systèmes internes ; il doit parfois être écrit spécifiquement.

Dans les deux cas, écrire une bonne gestion des erreurs est longue, parfois complexe. Et à partir du moment où du code est écrit en faisant fi d'une bonne gestion d'erreurs, le code suivant est de plus en plus complexe à gérer.

Tout comme le premier manquement au « const correctness » en C++ peut rapidement devenir un enfer pour son maintient, le premier manquement à une bonne gestion d'erreur rend encore plus complexe la gestion de nouvelles erreurs.

Au final, l'assert() est la méthode la plus simple pour s'en sortir. Il est redéfini dans le framework utilisé sous une forme plus complexe, permettant de remonter plus d'information et pouvant être ignoré. Il peut s'écrire en ligne, mêlant par la même occasion les responsabilités d'une fonction. Et il devient le standard de facto pour dialoguer avec l'utilisateur.

La méthode la plus simple pour s'en sortir, cela reste un pléonasme pour : ne pas faire les choses correctement.

Bonus

Pour terminer, voici un lien vers un passage de la keynote de Walter Bright à propos lors de la DConf 2013, où il est brièvement question de programmation par contrat.