Bonnes pratiques C++ (3/n)

Ce billet appartient à une série consacrée aux bonnes pratiques de développement C++. Certains sont toutefois applicables à d'autres langages. Aujourd’hui : les compilateurs et les analyseurs de code.

Compilez sans warning

Le projet doit compiler sans warning avec le niveau de warning configuré au plus élevé. (Ou le niveau juste avant le niveau le plus élevé pour le compilateur MSVC, qui au niveau maximal a tendance à être excessivement pédant et à émettre des warnings pour des constructions tout à fait valides.)

  • Un warning indique un problème : ne le négligez pas. Le code fonctionne chez vous actuellement, mais peut-être qu’il ne compilera pas ou se comportera différemment sur une autre plateforme ou avec un autre compilateur, ou bien peut-être que vous utilisez une construction ambiguë ou suspecte. (Exemple fréquent : une affectation dans une expression conditionnelle. C’est parfaitement légal mais facile à comprendre de travers à la relecture. Et puis il peut s’agir d’une faute de frappe. À la place de if (x = a + 1), écrivez donc if ((x = a + 1) != 0), cela rendra l’intention beaucoup plus claire et vous évitera un warning.)
  • Comme déjà évoqué plus haut, la compilation doit être aussi peu verbeuse que possible. Si elle génère des milliers de warnings que vous considérez sans importance, le jour où se présentera un warning indiquant un problème grave, il sera noyé dans la masse et vous passerez à côté.

Beaucoup de warnings concernent la portabilité. Il s’agit typiquement de constructions qui risquent de fonctionner différemment selon la taille des entiers ou des pointeurs. Ne pensez pas que cela ne vous concerne pas au prétexte que votre projet n’est censé fonctionner que sur une seule plateforme : la plupart des projets ont des durées de vie supérieures à celles des machines, des systèmes ou des compilateurs. Personnellement, en trente ans de carrière, j’ai été confronté au problème six fois, soit au moins cinq fois de trop pour considérer que c’est anecdotique : sous Windows quand on est passé de Windows 3.1 (16 bits) à Windows NT (32 bits), puis à nouveau récemment avec l’arrivée de Windows 64 bits ; sur la plateforme Palm OS, lorsqu’elle est passée du processeur 68000 (big endian) au processeur ARM (little endian) ; sur la plateforme Mac OS X, lorsqu’elle est passée du processeur Power PC (big endian 32 bits) aux processeurs Intel (little endian 64 bits), puis une nouvelle fois récemment lorsqu’elle est passée aux processeurs ARM64 ; et enfin sur la plateforme iOS, lorsqu’elle est passée de ARM à ARM64. Plus vous aurez à l’esprit d’écrire du code portable en toute circonstance, plus ces transitions forcées se feront sans douleur. Pour cela, prêtez particulièrement attention à tous les warnings qui concernent la taille des entiers ou des pointeurs : conversions de type, affectations d’un pointeur à un entier et inversement (très fréquent dans le code C historique), chaînes de format ne correspondant pas aux arguments passés à printf, etc.

Il est fréquent que de nouveaux warnings apparaissent à l’occasion de la mise à jour du compilateur. Il peut s’agir d’appels à des fonctions qui ont été dépréciées depuis la version précédente, ou bien de constructions dorénavant considérées comme dangereuses et ne devant plus être utilisées. (Par exemple : assigner une chaîne littérale à une variable non const a longtemps été une pratique banale, mais c’est désormais considéré comme une mauvaise pratique et cela provoque un avertissement sur les compilateurs récents.) Prenez le temps nécessaire pour corriger ces nouveaux warnings. Si vous ne le faites pas, ils seront de plus en plus nombreux au fur et à mesure de l’évolution du projet et des outils de build et après quelques années, il y en aura tellement que plus personne n’aura le temps ni le courage de s’attaquer au problème.

Exceptionnellement, il peut être nécessaire de désactiver un warning, par exemple pour compiler du code C ou C++ un peu bancal généré par un outil externe tel que lex, yacc, ou protobuf. Dans ce cas, procédez de la façon la plus chirurgicale possible. Ne désactivez que le warning qui pose problème, et uniquement pour le fichier, voire si possible uniquement pour les quelques lignes qui posent problème. Tous les compilateurs proposent une directive #pragma permettant de réaliser cette opération.

Utilisez un analyseur de code statique

Il existe un assez grand nombre d’outils pour analyser du code C++ et repérer les erreurs potentielles qui s’y cachent. Citons par exemple SonarQube, Klocwork, cpplint, cppcheck, et clang-tidy. Il y a également un analyseur inclus dans la plupart des IDE tels que Xcode, Visual Studio ou Qt Creator. Je ne peux qu’en recommander l’utilisation régulière.

Ces outils ne sont pas infaillibles. Ils passent à côté de nombreux de problèmes, surtout si votre code contient beaucoup de variables globales ou utilise de l’aliasing (le fait d’accéder à une même variable à travers plusieurs pointeurs). De plus, ils sont assez lents et génèrent des faux positifs. On ne peut donc pas les inclure dans la chaîne de compilation habituelle et encore moins bloquer la livraison à la moindre alerte. En revanche, vous pouvez vous fixer comme règle de faire une analyse statique après le développement de chaque nouvelle fonctionnalité, ou bien si vous travaillez en scrum, à la fin de chaque sprint.

Les problèmes que ces outils remontent sont de plusieurs ordres :

  • Le style. Il est possible de configurer des alertes pour repérer les noms de variable, de fonction ou de classe trop courts ou ne respectant pas un schéma fixé à l’avance, les fonctions trop longues, les blocs mal indentés, le manque de commentaires, etc. C’est très utile dans une grosse équipe pour assurer l’uniformité du code source.
  • Les bonnes pratiques. Il s’agit de repérer les endroits où le code utilise des idiomes historiques alors qu’un équivalent moderne est recommandé : calculs sur des pointeurs, allocations de buffers nus au lieu d’utiliser std::array ou std::vector, utilisation de chaînes « à l’ancienne » au lieu de std::string, appels à strcpy, malloc, printf et autres vieilleries dangereuses, etc. Il s’agit aussi de proposer des améliorations, par exemple de repérer les endroits où un paramètre est passé par valeur alors qu’il vaudrait mieux le passer par référence, les fonctions membres qui pourraient être statiques, les boucles où l’indice entier gagnerait à être remplacé par un itérateur C++, ou encore certains patterns de code fréquents pour lesquels il existe une alternative plus sûre. Par exemple, cppcheck est capable de repérer une boucle servant à rechercher un élément dans un container et propose d’utiliser std::find à la place.
  • Les problèmes de logique. Il s’agit de détecter les variables utilisées avant d’être initialisées, les objets jamais détruits, les tests toujours vrais ou toujours faux, le code mort, les divisions par zéro, les déréférencements de pointeurs nuls, etc. C’est évidemment sur ce point que les analyseurs sont le moins performants et émettent le plus de fausses alertes.

Là encore, il faut utiliser ce genre d’outil dès le début du projet. Si vous le mettez en place alors que votre application compte déjà des milliers de lignes de code et que l’équipe a déjà pris ses habitudes de développement, vous allez vous retrouver face à des centaines d’anomalies que vous n’aurez jamais le temps de résoudre.