La question de l'indentation

Sujet quasi-religieux, s'il en est un... L'indentation est le principe selon lequel la disposition du code doit refléter sa structure. Les programmes étant des entités complexes comprenant souvent des millions de lignes de texte, l'indentation est une nécessité pour qui souhaite être en mesure de lire, comprendre et – bien sûr – assurer l'entretien des programmes.

Ce que je fais, personnellement

J'utilise souvent des langages à accolades (C, C++, Java, C#) pour exprimer de manière programmatique mes idées. Pour cette raison, je joins disposition des accolades et disposition générale du texte dans cette rubrique.

Voici donc ma pratique personnelle d'indentation quand je programme pour mes propres fins. Quand je programme dans une équipe ou quand je produis quelque chose qui ne soit pas destiné à moi ou à mes étudiant(e)s, je me conforme aux usages du groupe, qui sont plus importants que mon confort personnel. De même, quand j'utilise un langage comme Go ou JavaScript ou la position des accolades (ouvrante, typiquement) influence la compréhension qu'a le compilateur de notre code source, je respecte bien sûr les usages.

  Je faisais... ...je fais maintenant

Règle générale, pour les instructions sur plus d'une ligne, j'utilise des accolades alignées l'une vis à vis l'autre et situées à la hauteur de la séquence en cours (le style Allman selon ce Wiki), mais je n'en fais pas une religion... sauf pour mes étudiant(e)s lorsqu'elles ou ils débutent; dans ce cas, il faut être rigoureux, à la limite sévère, pour réduire dans leur vie les bogues qui font inutilement perdre du temps.

J'indente avec des espaces. J'utilise un nombre impair d'espaces, typiquement trois (à cinq, je trouve que c'est excessif). Cela fait un espace pour l'accolade et deux pour la disposition. J'utilise toujours une police non proportionnelle (un truc que j'ai appris tôt dans ma carrière), justement pour améliorer l'impact de l'indentation.

Pour des instructions sur une seule ligne, je tends à indenter tout simplement et à omettre les accolades, sauf dans les cas où je me doute très fort que je devrai en ajouter de toute manière éventuellement.

J'ai longtemps pesté contre les fonctions à plusieurs points de sortie, mais avec le temps, j'ai fini par les intégrer à ma pratique, essentiellement pour la gestion des cas d'erreurs ou d'exceptions. Dans ces cas, il m'arrive même (il y a des exemples à droite) d'insérer le test et le traitement d'erreur ou d'exception sur une même ligne. Ce n'est pas un style convenable pour des débutants, cependant (trop facile de faire des erreurs).

Je définis mes constantes et mes variables aussi localement que possible (le code à droite est un exemple insuffisamment modularisé; un type Age connaissant ses bornes serait avantageux. En pratique, je manipule rarement des primitifs directement comme cet exemple le fait).

J'ai changé mes pratiques d'indentation récemment en 2016 pour ce qui est du positionnement des accolades. La raison pour ce changement est simple : l'adaptation aux outils pédagogiques.

De plus en plus, les écoles où j'enseigne et les lieux où je suis appelé à donner des présentations offrent des écrans plutôt que des ardoises – il faut savoir que je suis un « gars de craie », pas un « gars de diapositives électroniques »; j'aime bien le dynamisme que permettent les ardoises, où il est possible de changer d'approche avec agilité.

Sur les écrans, pour faciliter la vie des étudiantes et des étudiants au fond de la salle, j'utilise des polices de caractères plus grandes que ce que j'utiliserais en temps normal. Une conséquence de ce changement est que si je place les accolades ouvrantes sous le mot qui introduit un bloc, cela tend à occuper un espace vertical important sans livrer d'information visuelle utile. Comparez vous-mêmes :

//
// alternatives imbriquées
//
if (cond)
{
   f();
}
else if (autre_cond)
{
   g();
}
else
{
   h();
}
//
// alternatives imbriquées
//
if (cond) {
   f();
} else if (autre_cond) {
   g();
} else {
   h();
}
//
// gestion d'exceptions
//
try
{
   auto resultat = fct_a_risque();
   utiliser(resultat);
}
catch(exc_type_A&)
{
   gerer_A();
}
catch(exc_type_B&)
{
   gerer_B();
}
catch(...)
{
   gerer_mystere();
}
//
// gestion d'exceptions
//
try {
   auto resultat = fct_a_risque();
   utiliser(resultat);
} catch(exc_type_A&) {
   gerer_A();
} catch(exc_type_B&) {
   gerer_B();
} catch(...) {
   gerer_mystere();
}

Ceci me permet de maximiser l'utilisation de l'écran et l'utilité de l'information présentée. Notez que c'est aussi un retour à mes pratiques du milieu des années '90 alors que je travaillais à CAE Électronique Ltée.

#include <string>
#include <iostream>
//
// Cas d'exceptions possibles
//
class hors_bornes {};
class erreur_lecture {};
template <class T>
   bool est_entre_inclusif (const T &val, const T &minval, const T &maxval)
   {
      return minval <= val && val <= maxval;
   }
short lire_age()
{
   const short AGE_MIN = 0,
               AGE_MAX = 140; // estimé
   using std::cin;
   short age;
   if (!(cin >> age)) throw erreur_lecture{};
   if(!est_entre_inclusif(age, AGE_MIN, AGE_MAX))
      throw hors_bornes{};
   return age;
}
//
// Une fonction affchant des services aux personnes
// du troisième âge (définie ailleurs)
//
void afficher_service(std::ostream&);
int main()
{
   using namespace std;
   cout << "Entrez votre nom: ";
   string nom;
   if (cin >> nom)
   {
      cout << "Coucou, " << nom << endl;
      cout << "Et votre age est...? ";
      try
      {
         const short AGE_ADULTE = 18,
                     AGE_RETRAITE = 65;
         short age = lire_age();
         if(age < AGE_ADULTE)
            cout << "La jeunesse, c'est important!" << endl;
         else if (age < AGE_RETRAITE)
            cout << "La force de l'âge!" << endl;
         else
         {
            cout << "Voici nos services: " << endl;
            afficher_services(cout);
         }
      }
      catch(hors_bornes&)
      {
      }
      catch(erreur_lecture&)
      {
         cerr << "Erreur de lecture" << endl;
      }
   }
   else
      cerr << "Erreur de lecture" << endl;
}
#include <string>
#include <iostream>
//
// Cas d'exceptions possibles
//
class hors_bornes {};
class erreur_lecture {};
template <class T>
   bool est_entre_inclusif (const T &val, const T &minval, const T &maxval) {
      return minval <= val && val <= maxval;
   }
short lire_age() {
   const short AGE_MIN = 0,
               AGE_MAX = 140; // estimé
   using std::cin;
   short age;
   if (!(cin >> age)) throw erreur_lecture{};
   if(!est_entre_inclusif(age, AGE_MIN, AGE_MAX))
      throw hors_bornes{};
   return age;
}
//
// Une fonction affchant des services aux personnes
// du troisième âge (définie ailleurs)
//
void afficher_service(std::ostream&);
int main() {
   using namespace std;
   cout << "Entrez votre nom: ";
   string nom;
   if (cin >> nom) {
      cout << "Coucou, " << nom << endl;
      cout << "Et votre age est...? ";
      try {
         const short AGE_ADULTE = 18,
                     AGE_RETRAITE = 65;
         short age = lire_age();
         if(age < AGE_ADULTE)
            cout << "La jeunesse, c'est important!" << endl;
         else if (age < AGE_RETRAITE)
            cout << "La force de l'âge!" << endl;
         else {
            cout << "Voici nos services: " << endl;
            afficher_services(cout);
         }
      } catch(hors_bornes&) {
      } catch(erreur_lecture&) {
         cerr << "Erreur de lecture" << endl;
      }
   } else
      cerr << "Erreur de lecture" << endl;
}

Dans le cas de petites fonctions sur une seule ligne, il m'arrive d'utiliser le format proposé à droite (indentation, accolade, code, accolade), surtout sur la ligne est courte. Ceci se produit surtout dans la définition inline (à même la déclaration) de méthodes. Le fait que j'aie fréquemment recours à de la programmation générique a pour conséquence (entre autres choses) que j'écris beaucoup, beaucoup de très petites fonctions faciles à optimiser comme celle-ci.

double carre(double x)
   { return x * x; }
double carre(double x) {
   return x * x;
}

Pour une classe, ça donne quelque chose comme ce que vous pouvez voir à droite.

Vous remarquerez que la plupart de mes services sont sur une seule ligne (j'écris rarement des fonctions de plus d'une ligne, en pratique; à droite, le seul cas de plus d'une ligne est l'opérateur de consommation d'un flux, parce qu'il implique de la validation). Cela fait une forme relativement compacte et qui, à mon avis, se lit quant même bien.

class entier
{
public:
   using value_type = int;
private:
   value_type valeur_;
public:
   constexpr entier(value_type valeur = {})
      : valeur_{valeur}
   {
   }
   //
   // Sainte-trinité implicitement correcte
   //
   constexpr bool operator==(const entier &e) const noexcept
      { return valeur() == e.valeur(); }
   constexpr bool operator!=(const entier &e) const noexcept
      { return !(*this == e); }
   constexpr bool operator<(const entier &e) const noexcept
      { return valeur() < e.valeur(); }
   // ...autres opérateurs (omis par économie)...
   constexpr value_type valeur() const noexcept
      { return valeur_; }
   friend std::ostream& operator<<(std::ostream &os, const entier &e)
   {
      return os << e.valeur();
   }
   friend std::istream& operator>>(std::istream &is, entier &e)
   {
      if(!is) return is;
      entier::value_type valeur;
      if (is >> valeur)
         e = entier{valeur};
      return is;
   }
};
class entier {
public:
   using value_type = int;
private:
   value_type valeur_;
public:
   constexpr entier(value_type valeur = {})
      : valeur_{valeur}
   {
   }
   //
   // Sainte-trinité implicitement correcte
   //
   constexpr bool operator==(const entier &e) const noexcept {
      return valeur() == e.valeur();
   }
   constexpr bool operator!=(const entier &e) const noexcept {
      return !(*this == e);
   }
   constexpr bool operator<(const entier &e) const noexcept {
      return valeur() < e.valeur();
   }
   // ...autres opérateurs (omis par économie)...
   constexpr value_type valeur() const noexcept {
      return valeur_;
   }
   friend std::ostream& operator<<(std::ostream &os, const entier &e) {
      return os << e.valeur();
   }
   friend std::istream& operator>>(std::istream &is, entier &e) {
      if(!is) return is;
      entier::value_type valeur;
      if (is >> valeur)
         e = entier{valeur};
      return is;
   }
};

Note pragmatique

En pratique, déboguer prend du temps et, même avec les meilleures pratiques de programmation, il est inévitable que l'on se trouve, au moins sur une base occasionnelle, à fourbir les armes et à recourir au débogueur.

Certains débogueurs gèrent moins bien les fonctions où les accolades apparaissent sur une seule ligne, accompagnées de la définition de la fonction. Dans de tels cas, pour me faciliter l'existence, il m'arrive de faire des retouches temporaires pour remplacer une fonction indentée comme suit :

double carre(double x)
   { return x * x; }

... par son équivalent indenté comme cela :

double carre(double x)
{
   return x * x;
}

... ou encore, plus souvent aujourd'hui pour les raisons mentionnées plus haut :

double carre(double x) {
   return x * x;
}

Devoir reformater le code est un irritant, alors ces adaptations sont pour moi des situations temporaires. Cela dit, si vous déboguez beaucoup, il se peut que vos choix d'indentation soient influencés par les forces et les faiblesses de vos outils.


Valid XHTML 1.0 Transitional

CSS Valide !