Mokona Guu Center

Contournement et solution

Publié le

En programmation, il arrive parfois que quelque chose « tombe en marche ».Rien ne fonctionnait ou fonctionnait mal et soudain, après une manipulation incertaine, un petit changement, ça marche. À ce point, certains crient victoire et annoncent que le problème est résolu, d'autres, plus prudents, se mettent une petite note dans un coin et restent discret. Mais dans les deux cas, ce qui a été trouvé est un contournement, pas une solution.

La différence entre le contournement et la solution se situe dans deux contextes. L'un est volontaire et réagit à un contexte local provoquant un bug pour traiter un cas particulier 1. L'autre est involontaire et fait suite à un changement qui a fait disparaître le bug d'une manière non comprise.

Dans les deux cas, le contournement se diffère d'une solution par la compréhension du problème. Cela rend le contournement dangereux : il est là car le programmeur n'a pas compris où était son problème. La loi de Murphy nous averti : ça se retournera contre nous au pire moment.

En effet, puisque le contournement est la conséquence de quelque chose de non maîtrisé il est tout à fait probable (et cela se vérifie dans la pratique àpeu près tout le temps) que la vraie raison du problème entraîne d'autres conséquences plus tard.

Le pointeur magique

Prenons un exemple malheureusement souvent vu (qui est aussi le contournement qui tient le moins longtemps) : le cas spécial du pointeur magique.

Dans ce type de contournement, un pointeur qui ne correspond à rien est reçu dans une fonction (je parle de pointeur, mais cela peut-être un identifiant,une clé dans un dictionnaire, un résultat de hash non valide,...). Cette valeur ne correspondant à rien provoque un plantage ou une exception. La solution serait bien entendu de comprendre pourquoi cette valeur est reçue. Le contournement consiste à tester si la valeur reçue et l'ignorer si c'est celle qui nous ennuie.

Rien n'indique que cette valeur folle va rester la même ou même si d'autres ne vont pas apparaître. La pratique enseigne le contraire.

Un dérivé un peu plus subtile est de tester si le pointeur est null et de ne pas traiter le cas. Le cas est plus subtile car la nature du test dépend de la spécification de la méthode. Si celle-ci est censée accepter la valeurnull alors le test est valide. Si elle n'est pas censée l'accepter, alors c'est un contournement.

Si ce contournement est accompagné systématiquement d'un changement de la spécification des méthodes pour qu'elles acceptent les valeurs nulles, il faut commencer à s'inquiéter.

Le printf() magique.

À nouveau un grand classique. Je le nomme printf mais il peut s'agir den'importe quelle fonction ajoutée pour obtenir une trace du déroulement d'unprogramme et dont l'ajout occulte l'erreur.

Le déroulement est une scénario très connu : un crash un peu pénible car assez rare entraîne la volonté d'avoir un log de ce qu'il se passe au niveau d'une méthode. On ajoute une sortie texte. On relance. Le bug a disparu !

C'est étrange, mais puisque le bug a disparu, la sortie texte est enlevée. Le bug se produit à nouveau !

Le contournement consiste alors à laisser la sortie texte en place. La solution consiste à comprendre pourquoi l'ajout d'une ligne dans le programme« résout » le problème.

Bien entendu, il ne résout rien du tout. Il cache le problème.

Le printf() magique est généralement causé par un écrasement, le plus souvent sur la pile 2. Il peut aussi être du, surtout avec des traitement asynchrones (dont multithread) à une légère différence de timing, une sortie texte étant généralement longue et provoquant souvent un changement de contexte.

Dans le premier cas, vérifiez bien vos accès à des tableaux. Dans le second cas, vérifiez que vous n'accédez pas à des ressources partagées. Dans les deux cas, cela peut-être dans la méthode elle-même ou bien dans des appels qu'elle fait.

Une variante de l'écrasement de pile peut-être obtenue avec l'ajout de variable temporaire pour faire des vérifications qui, en décalant la place des variables dans la pile, cachent le bug.

Dans ce cas comme dans le précédent, un changement d'option dans le compilateur ou bien un changement de compilateur peut aussi contourner l'erreur obtenue. Je l'ai vu faire. Ce n'est pas une solution.

Les différences d'implémentation

Une bibliothèque est souvent (normalement, elle est toujours) accompagnée d'une documentation contenant la spécification de chacune de ces composantes(classes, méthodes, fonctions,...). Parfois, une même spécification peut servir à plusieurs implémentations de bibliothèques, ou bien une implémentation d'une même bibliothèque peut varier d'une version à une autre.

Un exemple de la spécification aux multiples implémentations est laSTL. On peut aussi penser à Mono par rapport à.Net.

Les spécifications laissent généralement une certaine liberté aux implémentations. La spécification porte souvent sur les résultats obtenus en fonction des entrées, moins souvent sur la manière dont la fonction s'y prend.

Du coup, un bug ou une fonctionnement qui semble étrange peut tout simplement venir d'une implémentation particulière. Il y a deux cas : soit, effectivement l'implémentation est mauvaise et il est peut-être intéressant de contourner en attendant une nouvelle version 3 ou changer d'implémentation ; soit il s'agit d'une mauvaise lecture des spécifications. Ce dernier cas n'est pas si rare et pas toujours évident à détecter.

Je prends un exemple concret récent : l'utilisation d'une std::list (en C++donc) dans un algorithme semblait être la bonne solution et fonctionnait parfaitement. Jusqu'au jour où la masse des données traitées par la liste est devenue très importante et que l'algorithme s'est écroulé : des heures et des heures de traitement.

Après avoir cherché dans des opérations potentiellement lourdes del'algorithme, un contournement a été adopté : utiliser un std::deque. Le programme a retrouvé une vitesse correcte.

Mais il s'agissait bien d'un contournement, pas d'une solution. En effet, le pourquoi de ce changement dans l'efficacité de l'algorithme n'était pas compris. Et utiliser un std::deque alors que l'on voulait initialement une std::list n'était pas satisfaisant.

Le problème venait en fait de l'utilisation de size() sur une std::list.Étrange n'est-ce pas ? En fait, la spécification par SGI de std::list indique bien que l'on peut s'attendre à ce que size() soit de complexité O(n), avec n le nombre d'éléments de la liste. On peut s'attendre, mais ça n'est pas forcé.

En étudiant les sources de différentes STL, on s'aperçoit que certaines implémentent size() par un parcours d'itérateur entre begin() et end() et d'autres maintiennent le taille en tant que membre du container.

Puisque size() était utilité pour savoir si la liste était vide, la solution véritable était donc d'utiliser empty() qui lui est assuré d'une complexité constante.

Une petite note au passage : la prochaine norme du C++ à cette date demande une complexité constante pour std::list::size().

Contournement et solution

Parfois, dans l'urgence, il est inévitable d'avoir recours à un contournement.Ne le laissez jamais s'installer ! Notez clairement dans le programme, et pas sur un bout de papier volant, la nature du contournement, pourquoi il a été mis là et quand. Lors de la recherche du problème, vous avez amassez uncertain nombre d'informations, n'hésitez pas à les notez aussi.

Le principe de chercher une solution plutôt que de contourner peut s'appliquer à bien d'autres domaines que la programmation ou l'informatique. Cela demande parfois un peu plus de travail mais on y est le plus souvent gagnant.


  1. aussi nommé « anti-bug » 

  2. dans des langages non managés. 

  3. ou de corriger, si on le peut, la bibliothèque.