La question des commentaires

« Imagine what you would need to tell to someone who is reading your code, if you were sitting next to them. This is what you put in comments » – Jonathan Boccara (lien)

« Imagine comments didn't exist. This is how you should write your code » – Brittany Friedman (lien)

Une question récurrente pour qui met la main à la pâte du côté de la programmation : il faut commenter le code, mais... Comment? Combien de commentaires mettre? Où commenter? Quoi exprimer? Quelle devrait être la part de texte et de code autodocumenté dans la clarification du propos dégagé par un programme?

Quelques réflexions suivent :

J'ai vu en 2015 sur Twitter cette perle que je me permets de paraphraser, et qui a trait aux formes que peuvent prendre les commentaires en C++, C# ou Java :

// signifie: "He, en passant..."
/* signifie: "assieds-toi, faut qu'on bavarde..."
Version commentée Version non-commentée
/*
SuisJeCommenteOuPas();
//*/
//*
SuisJeCommenteOuPas();
//*/

Ce que je fais, personnellement

Comme toujours, l'essentiel est de respecter les standards en place dans votre milieu de travail. Pour le reste, ce qui suit doit être lu avec attention et discernement – la clé du code de qualité est la clarté, et du code mal documenté ou trop obscur mène à du code difficile à entretenir ou à optimiser.

Quand je travaille pour moi-même, je commente très peu, préférant le code autodocumenté. Cela ne veut absolument pas dire « Pat ne commente pas, je vais faire comme lui! » (je vous en prie, ne faites pas ça!); cela veut dire que je vise du code tellement clair que les commentaires y deviendraient superflus. En fait, je suis de ceux qui pensent que le meilleur commentaire est un commentaire à même le code; l'autodocumentation du code est, selon moi, nettement supérieure à toute annotation destinée aux programmeuses et aux programmeurs, du fait que le compilateur (ou l'outil de génération de code que vous privilégiez, s'il s'agit d'un interpréteur ou d'un hybride) lui-même est en mesure de l'analyser et de nous protéger contre nos propres bêtises. D'autres ont une position semblable (par exemple http://leonardo-m.livejournal.com/99562.html qui recommande de transformer le plus possible les commentaires en code).

J'utilise les commentaires pour ce qui échappe au compilateur, pour ce qui est strictement destiné aux humains, ou encore – quand l'outil s'y prête – pour insérer des métadonnées servant à des outils automatisant des tâches comme la génération de la documentation ou la génération de code de validation et de tests (les outils comme Javadoc, Doxygen ou les trucs tels que les commentaires XML dans les programmes C#). Pour le reste, si je peux indiquer dans le code ce qui me préoccupe, alors ce sera mon premier choix.

 

Avec commentaires seulement

À même le code

Par exemple, pour tester les préconditions d'une fonction, un commentaire est utile au sens où les programmeuses et les programmeurs sont informés de la marche à suivre, mais les humains tendent à manquer ces indications qui leur sont destinées. Le compilateur, lui, est en général beaucoup plus alerte.

L'exemple à droite, bien que limité, le démontre. Si les commentaires sont pertinents (et parfois essentiels : par exemple, ici, il n'est pas possible de valider à même le code les préconditions quant aux caractéristiques des zones pointées a priori par les paramètres dest et src), ils ne suffisent pas en pratique et des validations à même le code sont très nettement préférables.

Plusieurs autres éléments de programmation se prêtent à une documentation à même le code plutôt que par des commentaires (le respect des invariants d'une classe, par exemple, ou le respect par une fonction ou par une méthode de ses propres postconditions). Si possible, la documentation et la validation de ces caractéristiques se fera de maniàre statique, par exemple à l'aide de techniques de métaprogrammation.

//
// memcpy(dest,src, n)
// Preconditions:
// - dest != 0
// - src != 0
// - src pointe vers au moins n bytes valides
// - dest pointe vers une zone d'une capacite d'au
//   moins n bytes
//
void *memcpy(void *dest, const void *src, size_t n) {
   auto d = dest;
   auto s = src;
   for(; *d ++ = *s ++; )
      ;
   return d;
}
#include <cassert>
//
// memcpy(dest,src, n)
// Preconditions:
// - dest != 0
// - src != 0
// - src pointe vers au moins n bytes valides
// - dest pointe vers une zone d'une capacite d'au
//   moins n bytes
//
void *memcpy(void *dest, const void *src, size_t n) {
   assert(dest && src);
   auto d = dest;
   auto s = src;
   for(; *d ++ = *s ++; )
      ;
   return d;
}

L'idée générale derrière les commentaires

Un commentaire, essentiellement, sert d'indication pour les programmeuses et les programmeurs quant à la nature ou au comportement d'éléments dans le code, qu'il s'agisse d'un fichier ou d'un module, d'un type, d'une fonction ou de quoi que ce soit d'autre.

Un commentaire utile enrichit la description naturelle du code, tout en demeurant léger et (surtout!) véridique. Ainsi, il faut à tout prix éviter des horreurs comme :

int i = 1; // affecter la valeur un à la variable i
//
// intialise le compteur i à 0. Parcourt les éléments de tab un à
// un et affiche chacun sur cout, une ligne à la fois
//
template<class T>
   void afficher_elements(const T tab[], int nelems)
   {
      for (int i = 0; i < nelems; ++i)
         cout << tab[i] << ' ';
   }

Ces commentaires, en effet, n'apportent rien à la lecture du code et vont trop loin (dans le cas de la description verbale de l'algorithme d'afficher_elements(), on peut supposer que le commentaire erroné tient du fait que le séparateur utilisé à l'origine a changé au fil du temps, mais que le commentaire n'a pas suivi). Un commentaire trompeur nuit à la lisibilité du code; vous n'en voulez pas, et vos collègues non plus.

Règle générale, un commentaire sert à guider les gens qui utilisent votre code. On veut savoir :

Un exemple de fonction convenablement documentée serait :

//
// trouver_si(debut,fin,pred) retourne un itérateur sur la première occurrence
// i dans l'intervalle à demi-ouvert (debut..fin( pour laquelle pred(*i) s'avère
//
// Préconditions: debut et fin constituent un intervalle à demi-ouvert valide
//                Pred est un prédicat applicable au type pointé
// Postconditions: mêmes que pred()
// Complexité: linéaire, donc O(n) si n == distance(debut,fin) présumant que
//             pred() soit O(1)
//
template <class Itt, class Pred>
   Itt trouver_si(Itt debut, Itt fin, Pred pred)
   {
      for (; debut != fin; ++debut)
         if (pred(*debut)) return debut;
      return fin;
   }

Remarquez aussi qu'il arrive que les commentaires puissent être remplacés par du code, particulièrement par des expressions statiques, donc évaluées à la compilation. Par exemple, ceci :

//
// Transforme val en int par une manipulation d'adresse pas très propre.
// ATTENTION: il faut éviter que la taille de val ne dépasse la taille
// d'un int, sinon seule une partie de val sera « transformée »
//
template <class T>
   int cacher_dans_entier(T val) {
      return *reinterpret_cast<int *>(&val);
   }

...peut être remplacé (très avantageusement!) par cela (voir ici pour des détails) :

template <class T>
   int cacher_dans_entier(T val) {
      static_assert(sizeof(T) <= sizeof(int), "T est trop gros pour un int");
      return *reinterpret_cast<int *>(&val);
   }

En effet, dans le premier cas, le programmeur doit être attentif au commentaire et s'auto-discipliner. Dans le second cas, le compilateur veille et intercepte les dérogations à la règle dès l'étape de génération du code (il n'y a aucun coût à l'exécution).

Le rôle des commentaires dans la conception d'un algorithme

Quand je travaille, je laisse des commentaires d'abord pour moi-même. Souvent, quand une pièce de code me semble perfectible, je débute mon commentaire par ICI: pour que le repérage ultérieur des zones à problème soit plus simple. Par exemple :

Noeud* prochain(Noeud *p) {
   return p->succ; // ICI: valider p au préalable
}
//
// ICI: version récursive (simple mais terriblement inefficace). Raffiner
//
long fibonacci(int n) {
   return n == 0 || n == 1? n : fibonacci(n-1) + fibonacci(n-2);
}

Évidemment, dans du code de production, ces commentaires devraient pour l'essentiel disparaître. Un exemple de version plus sérieuse de ces fonctions (et des commentaires qui me semblent appropriés dans chaque cas) serait :

class PointeurInvalide {};
Noeud* prochain(Noeud *p) { // throw(PointeurInvalide)
{
   if(!p) throw PointeurInvalide{};
   return p->succ;
}
//
// Complexité O(n) en temps et en espace (on peut
// réduire la complexité en espace à O(1) ici, est-
// ce que vous voyez comment y arriver?)
//
long fibonacci(int n) {
   assert(n >= 0);
   vector<long> v(n+1);
   v[0] = 0; v[1] = 1;
   for(vector<long>::size_type i = 2; i <= n; ++i)
      v[i] = v[i-1] + v[i-1];
   return v[n];
}

Remarquez que je n'ai pas commenté le fait que prochain(Noeud *p) retourne le prochain Noeud à partir de p, puisque cela me semble implicite dans la signature et dans les noms utilisés.

Je n'ai pas non plus expliqué la suite de Fibonacci, mais j'ai indiqué la complexité de l'algorithme implémenté (ce qui peut être utile au code client). J'ai tenu pour acquis, ce faisant, que la suite de Fibonacci est de la culture générale pour les scientifiques, mais si j'avais eu un doute, j'aurais sans doute laissé un lien vers un site expliquant le tout. Une autre notice qui aurait pu être indiquée en commentaire ici aurait été le recours à de la programmation dynamique (probablement pas pour accompagner le prototype de la fonction mais peut-être pour accompagner sa définition).

J'ai laissé des commentaires pour indiquer au code client le type d'exception pouvant être levé, dans le cas de prochain(), puisqu'il s'agit d'une information que le programmeur peut mieux gérer que le compilateur.

J'utilise aussi des commentaires pour décrire mon algorithme avant de le coder. C'est ce que je veux exprimer quand j'explique à mes étudiant(e)s débutant(e)s que je commente avant de coder : tant que je ne peux pas exprimer clairement en mots mon intention, il est probable que je ne comprenne pas assez le problème pour essayer de le résoudre.

Un exemple banal de cette démarche irait comme suit :

template <class It>
   void trier(It debut, It fin) {
      //
      // si distance(debut,fin) est petit, faire un tri à bulles
      // sinon,
      //    trouver le centre de (debut..fin(
      //    trier (debut..centre(
      //    trier (centre..fin(
      //    fusionner les deux sous-séquences triées
      //
   }

Remarquez le niveau de détail, très général. Implémenté concrètement, le code résultant ressemble souvent aux commentaires initiaux, d'ailleurs, comme le montre l'extrait ci-dessous :

template <class It>
   void trier(It debut, It fin) {
      using namespace std;
      enum { SEUIL_BULLES = 10 };
      auto n = distance(debut, fin);
      if (n <= SEUIL_BULLES)
         tri_bulles(debut, fin);
      else {
         auto centre = debut;
         advance(centre, n / 2);
         trier(debut, centre);
         trier(centre, fin);
         fusionner(debut, centre, fin);
      }
   }

Il faut évidemment écrire fusionner() avec prudence dans un cas comme celui-ci, ou mieux encore, utiliser du code d'expert.


Valid XHTML 1.0 Transitional

CSS Valide !