Mise en oeuvre progressive des warnings de compilation en C/C++

Ces dernières années, les compilateurs ont énormément progressé sur différents aspects : optimisation (génération de code, LTO, …), instrumentation (sanitizers), debug, vectorisation, … mais également concernant un sujet plus immédiat et visible au quotidien : les warnings, que ça soit dans la détection des problèmes ou la manière dont ils sont libellés (précision, suggestion, rappel du nom du warning concerné, …). On peut sans doute saluer la compétition entre gcc (le référent historique) et clang (du projet LLVM) qui a dépassé le stade d’outsider. Ce sont les deux compilateurs que nous considérerons (en focalisant sur les langages C et C++).

Les options de warnings ne font souvent pas l’objet d’une attention particulière et se trouvent sous-utilisées (par facilité, méconnaissance, …). Pourtant elles permettent d’aller très loin dans la recherche de faiblesses dans le code et au plus tôt (à la compilation du code), là où les corrections coûtent peu chères. Le but de cet article est de prendre cette direction, de vous proposer un renforcement progressif des warnings en définissant plusieurs niveaux. L’idée est de se mettre dans la démarche de choisir délibérément un niveau adapté à la situation du moment, en faisant découvrir les options qui existent.

Comme pour d’autres sujets concernant le développement logiciel, que ce soit la gestion de projet ou la stratégie de test par exemple, il est impératif de faire des choix. Le risque est moindre que de ne faire aucun choix. Mais nous allons le voir, comme bien souvent, il ne faut pas être effrayé et s’imaginer une tonne de process ou de questions pièges. On se tiendra loin des règles rigides et arbitraires (et absurdes) comme “on met tout au max pour une meilleure qualité” (ce que l’on voit plus généralement au sujet des règles MISRA ou des options des outils d’analyse statique de code). Rien de tout cela. Je prône l’amélioration continue et le pragmatisme, c’est dans cette démarche que nous allons passer en revue des incréments simples qui bénéficieront au projet et aux développeurs.

Niveau 1 : l’indispensable

Par défaut, si vous ne précisez pas d’option de warning particulière, le compilateur vérifie bien sûr des règles relatives au langage mais cette situation est à bannir complètement : elle passe à côté de faiblesses les plus promptes à causer des bugs. Aussi, s’il ne devait y avoir qu’une option de warning à utiliser, ça serait -Wall … qui est loin de les activer tous même si elle en rassemble plusieurs dizaines, listés dans la page de la documentation de gcc dédiée aux warnings : https://gcc.gnu.org/onlinedocs/gcc/Warning-Options.html. A propos de cette page, vous aurez souvent à vous y référer car j’ai pris le parti de ne pas indiquer la signification de chaque warning évoqué dans cet article. 

Par cette action simple qui doit devenir automatique, vous détecterez ainsi les problèmes les plus importants. Et les plus simples à régler.

Donc si vous en êtes au niveau 0, réagissez ! Ça n’est pas cher et ça peut rapporter gros. A moins que vous ne préfériez subir les bugs. Dans ce cas, vous pouvez vous épargner la lecture de la suite de cet article. Mais ne prétendez pas écrire du code de qualité.

Niveau 2 : le minimum

Disons que “-Wall” était le strict minimum mais si vous l’utilisiez déjà habituellement ou si vous avez activé cette option et corrigé les warnings trouvés (avec dans ce cas une probable satisfaction), vous êtes prêt pour passer la seconde en utilisant le couple -Wall -Wextra.

L’option -Wextra ajoute son lot de warnings (plus d’une quinzaine d’après la documentation de gcc). Tous ne vont pas mettre au jour des bugs manifestes mais vont alerter sur des faiblesses qui peuvent induire un comportement inattendu : -Wsign-compare, -Wtype-limits, -Wmissing-field-initializers …

Vous pourriez rencontrer l’option -W qui est l’ancienne notation de -Wextra. On peut se demander pourquoi elle n’a pas été dépréciée mais bref … renommez-là pour être plus explicite.

Si -Wextra vous amène beaucoup de warnings, vous êtes peut-être dans le cas où, utilisant abondamment les callbacks, ces warnings sont de type “unused parameter”. Vous pouvez trouver des manières de déclarer ces paramètres comme inutilisés dans le code (avec l’attribut unused) ou de les utiliser de manière factice pour satisfaire le compilateur. Ou sinon, désactivez uniquement ce warning avec -Wno-unused-parameter, au moins temporairement. Vous gagnerez en lisibilité pour corriger les warnings restants et adopter une stratégie concernant celui-ci. Quoi qu’il en soit, prenez le temps en premier lieu de parcourir chacun des endroits pointés, cela permettra peut-être de trouver des incohérences : “tiens, la fonction marchait alors qu’on n’utilise pas ce paramètre bien qu’on aurait dû” ou “ah, ce paramètre n’est réellement plus utilisé, on l’enlève”.

Niveau 3 : le recommandé

Bravo, en ayant franchi le niveau 2, vous avez intégré le fait que désormais -Wall ne sera plus jamais seul car toujours accompagné de son inséparable -Wextra (et l’utilisation des deux semble unanimement indiquée). Vous avez également déjà gagné en confiance, en réalisant quels bugs auraient pu se produire ou en constatant un changement de comportement de l’applicatif.

Plus on augmente en niveau, plus les warnings nouveaux portent sur des considérations précises … et a priori une dangerosité (ou une probabilité) moindre. Aussi, faites en fonction du niveau technique de l’équipe, du temps disponible à consacrer à cela (même si éviter un bug coûte souvent moins cher que de le corriger après qu’il se soit manifesté). Nous souhaitons ici apprendre à tirer le meilleur du compilateur, ce qui implique de mettre un pied dans la documentation des options de warnings et de comprendre le rôle de chacune. C’est essentiel puisqu’il s’agit de faire du sur-mesure désormais.

Vous constaterez que cette approche par les warnings va soulever des problématiques techniques qui vous feront progresser sur de nombreux aspects : lisibilité et maintenabilité du code, sécurité, gestion des pointeurs et des types, connaissance du compilateur, etc. 

Voici donc la suite d’options que je vous propose d’adopter :

-Wall -Wextra -Wshadow -Wpointer-arith -Wundef -Wcast-align -Wswitch-default -Wwrite-strings -Wbad-function-cast

Niveau 4 : l’exigeant

A ce stade, vous devenez plus fin connaisseur des options du compilateur et vous en comprenez la subtilité. Vous pouvez encore en ajouter quelques-unes :

-Wformat=2 -Wcast-qual -Wconversion -Wlogical-op -Wstrict-prototypes -Wmissing-prototypes -Wmissing-declarations

Ne prenez pas peur. Faites connaissance avec chacune au fur et à mesure. Et adaptez si besoin, selon votre code ou votre jugement. Et dites-vous que cette liste est dans les propriétés du projet, un makefile, … donc ça ne change pas vraiment grand chose qu’il y ait 10 options ou 17.

Ensuite, comme toujours, veillez à ne pas rendre les choses complexes (comment ça, c’est trop tard ?). Les options de warnings que vous rajouteriez pourront trouver des défauts potentiels mais en moins grand nombre. Ne mettez pas non plus tous vos œufs dans le même panier, c’est à dire qu’il y a sans doute d’autres points d’amélioration à trouver concernant vos projets : architecture, tests, intégration continue, satisfaction des besoins clients, support, correction de bugs …

Pour parachever ce niveau, je vous soumets une suggestion : utilisez de manière alternative un autre compilateur. Typiquement, si par défaut vous utilisez gcc, alors mettez en place clang (issu du projet LLVM qui contient d’autres outils incontournables). C’est toujours intéressant. Et pour des mêmes options de warnings, leur traitement interne sera légèrement différent. Cet autre compilateur lèvera donc quelques warnings inconnus du premier. Une manière de polir son build et son code.

Niveau 5 : le sur-mesure

Pour donner un retour d’expérience et être tout à fait honnête, je ne crois pas avoir vu de projet dépassant l’équivalent du niveau 3. Alors me direz-vous, pourquoi faire du zèle et s’acharner ? Et bien pour apprendre, et découvrir ce que les compilateurs peuvent faire pour vous sur ces problématiques. Ils sont capables d’aller trouver des faiblesses sans avoir besoin d’autres outils. Je pense en premier lieu aux outils d’analyse statique de code, terriblement puissants mais qui demandent d’investir financièrement (les outils en vue sont très coûteux, sinon essayez clang-analyze) et dans la configuration, la maintenance et correction des défauts (qui parfois amènent à complexifier le code !). Et comme je l’ai déjà vu, le côté pratique n’est pas toujours au rendez-vous quand l’analyse et les résultats sont produits sur un serveur.

Bien, restons-en donc avec notre compilateur. Le fait de rajouter des options de warnings permettra d’améliorer votre code (et de trouver encore des problèmes) mais au niveau atteint, il peut déjà y en avoir beaucoup. Il ne faudrait donc pas tomber dans l’indigestion. Il est important de mettre cela en perspective dans votre stratégie d’amélioration continue.

Dans cette stratégie, une des approches concernant l’ajout de nouveaux warnings pourrait être de ne pas les activer dans un contexte de développement mais de les ajouter dans un job de compilation supplémentaire dans votre intégration continue, par exemple.

En consultant la documentation, vous découvrirez que des options peuvent être très utiles mais dans des contextes particuliers : utilisation de flottants (-Wfloat-equal et -Wdouble-promotion), embarqué contraint en mémoire (-Wframe-larger-than et -Wstack-usage-len), sécurité sur le formatage de chaînes (-Wformat-truncation=2 et -Wformat-signedness), propreté (-Wdeclaration-after-statement, -Wredundant-decls, -Wmissing-include-dirs, -Wunused-macros), code conditionnel if/else (-Wduplicated-cond, -Wduplicated-branches), types (-Waggregate-return et -Wshift-overflow=2) …

Voilà pour le sur-mesure commun aux deux compilateurs cités. Mais sachez que clang propose des options bien à lui, qui pourraient vous être utiles, comme : -Wheader-guard, -Wloop-analysis, -Wstring-conversion, -Wunique-enum …

A vous de jouer désormais, en suivant la trame donnée à travers ces 5 niveaux et en adaptant suivant vos besoins, vos souhaits et vos ressources.

Utiliser ou pas l’option -Werror ?

Voilà qui fait débat. Cette option permet de considérer les warnings comme des erreurs, la conséquence directe étant l’arrêt de la compilation à la première occurrence.

Aussi, une fois atteint le niveau de warnings choisi et que tous ont été éradiqués, l’utilisation de cette option peut être vue comme un rempart contre une nouvelle prolifération insidieuse, lente mais continue, de nouveaux warnings. Ces derniers sont corrigés dès leur apparition et ainsi, le code fourni est garanti exempt de warnings. Cela semble donc plus rigoureux.

Mon avis semble être minoritaire sur la question mais je ne suis pas particulièrement favorable à cette option. Tout d’abord, l’émission de warnings dépend du compilateur et de sa version, il est donc pénalisant quand on arrive sur un projet par exemple, de voir la compilation échouer. De même, il peut arriver qu’en intégrant une application dans un système de build comme Yocto (qui aurait par défaut des options plus rigoureuses), cela empêche carrément de générer l’image finale.

D’autre part, de nombreux warnings vont demander du temps et des discussions parce que leur correction ne sera pas immédiate et qu’elle pourrait nécessiter des changements plus profonds ou la rédaction de règles communes de programmation.

Un autre cas concret pénible arrive quand on est en train de modifier du code (qui ne va parfois pas rester) et d’échouer sur un problème inoffensif comme l’ajout de la déclaration d’une variable au milieu du code avec -Wdeclaration-after-statement …

Plus on pousse les options, plus il sera facile de causer de nouveaux warnings … et donc de se prendre les pieds dans le tapis avec -Werror. Tous les warnings ne se valent pas en terme de gravité. L’utilisation de -Werror peut aussi dépendre du niveau de warning : il peut s’avérer d’autant plus utile que le niveau des warnings est faible. Avec un niveau élevé, cela devient gênant, voire pénalisant.

Ce qui est déterminant, c’est que les warnings ne soient pas oubliés ! Pourquoi ne pas faire en sorte d’ajouter cette option sur compilation de pré-commit ou dans l’intégration continue (où rendre visible les warnings est déjà très instructif et visuel) ?

Emparez-vous de cette question, débattez-en … et choisissez.

Conclusion

Les compilateurs (et les warnings) sont nos amis. Nous les utilisons tous les jours mais ils ne demandent qu’à s’exprimer davantage. On a tout à gagner à mieux connaître les options et à les utiliser. Cela demande un effort mais cet investissement sera vite rentabilisé.

Concernant les options de warnings, nous avons présenté une approche qui permet de renforcer leur usage de manière progressive, en 5 niveaux, au cours desquels une liste croissante d’options a été proposée. Ces suggestions sont à adapter en fonction du contexte du projet dans lequel elles seront mises en œuvre. Quoi qu’il en soit, vous avez dû trouver des éléments pour vous guider dans cette progression, de la prise de conscience à la maîtrise des warnings. Vous saurez donc décider en toute connaissance de cause (sinon, je peux bien sûr vous accompagner sur ces sujets).

Pour trouver des faiblesses et bugs dans vos logiciels, le sujet traité n’en est qu’un parmi d’autres. Les compilateurs proposent d’autres fonctionnalités comme les sanitizers qui instrumentent le code pour détecter des erreurs à l’exécution, des options de durcissement (-fstack-protector-strong,  -D_FORTIFY_SOURCE=2, etc.) … D’autres outils vous permettront d’avoir du code plus lisible, mieux formaté (clang-format) et plus conforme (votre IDE, clang-tidy …).

Au-delà des compilateurs, l’amélioration des logiciels repose aussi sur d’autres outils et pratiques : tests, intégration continue, revue de code, gestion des tâches … Nous sommes allés loin dans les warnings mais pensez bien à l’équilibre de tous ces sujets dans la vie de vos projets et ajustez les curseurs en conséquence. Rien ne sert d’avoir du code qui compile sans un warning après avoir ajouté des dizaines d’options si par exemple il ne répond pas correctement aux besoins spécifiés.

Avancez progressivement, ayez une vision globale, soyez pragmatique. Mais considérez les warnings à leur juste valeur, et incluez les dans votre stratégie de développement, sans vous habituer à leur présence en disant “on verra plus tard, ils ont toujours été là et tout fonctionne bien”.