Musée des horreurs – Répétitives

Quelques raccourcis :

Les répétitives sont un terreau fertile pour l'expression de diverses petite (et moins petites) horreurs...

Entrée 00 – La répétitive mal choisie

La situation : supposons qu'on veuille écrire une fonction qui lise un entier jusqu'à ce que cet entier soit strictement positif, puis retourne l'entier lu. Au préalable, l'usager doit être invité à entrer un entier strictement positif. Si un nombre nom conforme aux exigences est entré, un message intimant l'usager de se conformer doit être affiché et la lecture doit être tentée à nouveau, et ce tant et aussi longtemps qu'une entrée conforme n'aura pas été réalisée.

L'horreur : les cas considérés ici ne sont pas des horreurs au sens strict du terme. Il s'agit dans chaque cas de solutions opérationnelles mais déficientes en forme ou en style (qui ne sont pas non plus des canons d'élégance – certaines sont carrément inacceptables parce que trop difficiles à valider). Voyons une première version :

Horreur 00.0
int lire_entier_positif() {
   using namespace std;
   int val;
   cout << "Entrez un entier strictement positif: ";
   do {
      cin >> val;
      if (val > 0) {
         return val;
      }
      cout << "Strictement positif s.v.p.! Recommencez: ";
   } while(true);
   return 0; // bidon, point jamais atteint
}

Le cas Horreur 00.0 est un cas type de ce qu'on nomme une fonction spaghetti – vous pouvez en faire la démonstration en essayant d'en tracer le morphogramme. On y remarque plusieurs fautes programmatiques graves :

Un autre cas qui n'est pas vraiment mieux :

Horreur 00.1
int lire_entier_positif() {
   using namespace std;
   int val;
   cout << "Entrez un entier strictement positif: ";
   do {
      cin >> val;
      if (val > 0) {
         break;
      }
      cout << "Strictement positif s.v.p.! Recommencez: ";
   } while(true);
   return val;
}

Cette version a de bon de n'avoir qu'un seul point de sortie à la fin de la fonction, contrairement à celle présentée au cas Horreur 00.0 qui en avait deux dont un purement bidon, et de placer ce point de sortie en fin de fonction, ce qui évite bien des accidents et est clairement la pratique la plus recommandable qui soit.

Par contre, elle a en commun avec le cas Horreur 00.0 le défaut de procéder à deux tests par itération, et le défaut structurel sérieux de briser brutalement le cycle de la répétitive par un break dans l'alternative interne à la boucle. Contrairement à ce que certains peuvent penser, ce genre de manoeuvre ne donne pas du code plus rapide (au contraire!) et entraîne des vices de maintenance.

Une amélioration structurelle importante serait d'avoir une répétitive qui n'est pas infinie et qui se conclut normalement une fois que la condition de poursuite ne sera plus rencontrée :

Horreur 00.2
int lire_entier_positif() {
   using namespace std;
   int val;
   cout << "Entrez un entier strictement positif: ";
   do {
      cin >>val;
      if (val <= 0) {
         cout << "Strictement positif s.v.p.! Recommencez: ";
      }
   } while(val <= 0);
   return val;
}

Le défaut de faire deux tests par itération perdure ici. On constate d'ailleurs que les deux tests sont identiques, et que le test interne (l'alternative) ne sert qu'à afficher un message d'erreur lors d'une entrée invalide.

De manière équivalente, avec des qualités et des défauts similaires, on aurait la version suivante :

Horreur 00.3
int lire_entier_positif() {
   using namespace std;
   int val = -1; // Bidon
   cout << "Entrez un entier strictement positif: ";
   while(val <= 0) {
      cin >> val;
      if (val <= 0) {
         cout << "Strictement positif s.v.p.! Recommencez: ";
      }
   }
   return val;
}

Celle-ci utilise une initialisation bidon de la variable val, cette variable servant au contrôle de la répétitive, pour simuler une lecture erronnée antérieure à la répétitive et forcer une première entrée dans la boucle. Cela fonctionne, mais on sent bien en regardant le tout que l'élégance et la rapidité attendues ne sont pas encore au rendez-vous.

Le bon usage : reconnaître qu'il y a deux types de lecture distinctes à faire dans ce problème, soit la première, qui doit nécessairement être faite, et toutes les autres, qui ne doivent être faites que si la lecture précédente était erronnée.

La forme attendue pour cet algorithme devrait donc être :

Traduit en code C++, on obtient :

Entrée 00 – Usage en bonne et due forme
int lire_entier_positif() {
   using namespace std;
   int val;
   // Faire une première lecture
   cout << "Entrez un entier strictement positif: ";
   cin >> val;
   while (val <= 0) {
      // Faire une nouvelle lecture
      cout << "Strictement positif s.v.p.! Recommencez: ";
      cin >> val;
   }
   return val;
}

Ce code est à la fois plus élégant et plus rapide que tous les prédécesseurs, sans pour cela être plus compliqué (en fait, il est probablement plus simple que tous les cas problèmes listés plus haut dans cette rubrique). Il est plus élégant parce que découpé de manière à faire son travail et rien d'autre, et ce à l'aide d'une répétitive classique, documentée et standardisée. Il est plus rapide parce qu'on n'entre dans la répétitive que si c'est vraiment nécessaire, et que chaque itération dans la répétitive n'implique qu'une seule vérification de condition (plutôt que les deux dans chacun des cas problèmes).

Après tout, pourquoi se compliquer la vie?

Entrée 00a – Corollaire

Il ne faut pas lire dans Entrée 00 plus que ce qui y apparaît vraiment. On y passe d'une répétitive faire...tant que à une répétitive tant que...faire parce que c'est une meilleure structure de répétitive pour ce problème, ce qui ne signifie pas qu'il s'agisse d'un meilleur format de répétitive pour tous les problèmes. Il y a un pas ici qu'il vaut mieux ne pas franchir.

Si on considère un problème similaire à celui de Entrée 00, soit celui d'écrire une fonction qui lise un entier jusqu'à ce que cet entier soit strictement positif, puis retourne l'entier lu, sans demander d'afficher de message particulier lors d'une entrée incorrecte. On pourrait être tenté d'utiliser une boucle tant que...faire comme suggéré plus haut :

Horreur 00a.0
int lire_entier_positif() {
   using namespace std;
   int val;
   cout << "Entrez un entier strictement positif: ";
   cin >> val;
   while (val <= 0) {
      cin >> val;
   }
   return val;
}

Dans ce cas, on a une solution opérationnelle et correcte, mais pas idéale. La différence entre ce problème et le problème cité dans Entrée 00 est qu'ici, le code avant la répétitive est exactement le même que le code dans la répétitive (à l'affichage initial près). La clé de la simplicité est donc ici, sans perte de vitesse ou d'élégance, un faire...tant que :

Entrée 00a – Usage en bonne et due forme
int lire_entier_positif () {
   using namespace std;
   int val;
   cout << "Entrez un entier strictement positif: ";
   do {
      cin >> val;
   } while (val <= 0);
   return val;
}

Il faut éviter de prendre intégralement une seule et même stratégie pour résoudre tous les problèmes.

Entrée 01 – Compliquer les choses simples

La situation : on veut le premier élément d'un tableau en C++ ou d'une Collection en VB.NET. On présume que la personne programmant ici connaisse les répétitives For Each en VB.NET et for en C++.

L'horreur : en VB.NET, prendre le marteau doré de la répétitive For Each et l'appliquer à un cas qui ne demande pas le moins du monde d'itérer à travers une séquence :

Horreur 01.0a
Dim Coll As New Collection
' ...
' remplir la Collection
' ...
If Coll.Count > 0 Then
   For Each ObjVille In Coll
         Exit For
   Next
   ' Utiliser ObjVille, dernier objet ayant référé
   ' à un élément de la collection... donc le premier
   ' élément de la Collection dans ce cas-ci!
   Cmb_Ville.Text = ObjVille.Get_Ville
   Lbl_Tmp_Ville.Text = ObjVille.Get_Temperature()
End if

En C++, la même horreur donnerait à peu près ceci :

Horreur 01.0b
//
// using ...
//
vector<Ville> v;
// ...
// remplir le vecteur
// ...
if (!v.empty()) {
   vector<Ville>::size_type i;
   for (i = 0; i < v.size(); i++) {
      break;
   }
   // Utiliser v[i], dernier objet ayant été référé
   // dans le vecteur... donc le premier élément du
   // vecteur dans ce cas-ci!
   cout << "Ville: " << v[i].Get_Ville()
        << ", température: " << v[i].Get_Temperature()
        << endl;
}

... ou, en utilisant un itérateur (ce qui serait plus près de l'usage en VB.NET) :

Horreur 01.0c
//
// using ...
//
vector<Ville> v;
// ...
// remplir le vecteur
// ...
if (!v.empty()) {
   vector<Ville>::iterator it;
   for (it = v.begin(); it != v.end(); ++it) {
      break;
   }
   // Utiliser it, itérateur sur le dernier objet ayant été référé dans
   // le vecteur... donc le premier élément du vecteur dans ce cas-ci!
   cout << "Ville: " << it->Get_Ville()
        << ", température: " << it->Get_Temperature()
        << endl;
}

Le bon usage : n'utiliser une répétitive que si on désire faire quelque chose de répétitif, évidemment. Dans ce cas bien précis, si on ne compte pas itérer à travers la Collection ou le vecteur, il est nettement préférable, suir le plan de l'efficacité comme sur le plan de la compréhension et de la facilité d'entretien ultérieure du programme, d'aller accéder au premier élément (celui désiré) par les canaux prévus à cet effet :

Entrée 01.0a
Dim Coll As New Collection
' ...
' remplir la Collection
' ...
If Coll.Count > 0 Then
   ' Utiliser le premier élément de la Collection
   Cmb_Ville.Text = Coll.Item(1).Get_Ville
   Lbl_Tmp_Ville.Text = Coll.Item(1).Get_Temperature()
End if

En C++, avec un vecteur, plusieurs options sont possibles pour arriver au même résultat :

Entrée 01.0b0 – Accéder au 1er élément par son indice
//
// using ...
//
vector<Ville> v;
// ...
// remplir le vecteur
// ...
if (!v.empty()) {
   // Utiliser v[0], premier élément du vecteur
   cout << "Ville: " << v[0].Get_Ville()
        << ", température: " << v[0].Get_Temperature()
        << endl;
}
Entrée 01.0b1 – Accéder au 1er élément à travers une indirection sur un itérateur
//
// using ...
//
vector<Ville> v;
// ...
// remplir le vecteur
// ...
if (!v.empty()) {
   // Utiliser le premier élément du vecteur
   cout << "Ville: " << v.begin()->Get_Ville()
        << ", température: " << v.begin()->Get_Temperature()
        << endl;
}
Entrée 01.0b2 – Accéder au 1er élément à travers une indirection sur un itérateur
//
// using ...
//
vector<Ville> v;
// ...
// remplir le vecteur
// ...
if (!v.empty()) {
   // Utiliser le premier élément du vecteur
   cout << "Ville: " << *(v.begin()).Get_Ville()
        << ", température: " << *(v.begin()).Get_Temperature()
        << endl;
}
Entrée 01.0b3 – Accéder au 1er élément à travers une méthode spécialisée
//
// using ...
//
vector<Ville> v;
// ...
// remplir le vecteur
// ...
if (!v.empty()) {
   // Utiliser le premier élément du vecteur
   cout << "Ville: " << v.front().Get_Ville()
        << ", température: " << v.front().Get_Temperature()
        << endl;
}

Il serait plus léger encore d'utiliser une variable temporaire plutôt que de solliciter la méthode front() à plusieurs reprises :

//
// using ...
//
vector<Ville> v;
// ...
// remplir le vecteur
// ...
if (!v.empty()) {
   // Utiliser le premier élément du vecteur
   const auto &elem = v.front();
   cout << "Ville: " << elem.Get_Ville()
        << ", température: " << elem.Get_Temperature()
        << endl;
}

À retenir : une maxime à retenir serait c'est pas parce que ça fonctionne que c'est une bonne idée. Gardez les répétitives pour les tâches répétitives et ne vous satisfaisez pas de trucs qui fonctionnent même s'ils ont l'air suspects. Visez une solution à la fois opérationnelle, efficace et élégante. Et souvenez-vous qu'en informatique, le temps où on entretient une solution est beaucoup, beaucoup plus long que le temps requis pour la produire. Prenez soin d'être clair(e) et de faire des choix qui seront simples à comprendre et à entretenir sur une longue période!

Entrée 02 – le Loop-Switch

La situation : un programme doit franchir diverses étapes en séquence. Pour les besoins de la cause, nommons ces étapes etapeA(), etapeB(), etapeC() et etapeD().

L'horreur : implémenter ce séquencement pas une sélective inscrite dans une répétitive :

Horreur 02.0
void franchir_etapes() {
   const int NB_ETAPES = 4;
   for(int i = 0; i != NB_ETAPES; ++i) {
      switch(i) {
      case 0:
         etapeA();
         break;
      case 1:
         etapeB();
         break;
      case 2:
         etapeC();
         break;
      case 3:
         etapeD();
         break;
      }
   }
}

Notez qu'une sélective dans une répétitive peut être convenable si les étapes ne sont pas franchies dans un ordre prédterminé, comme par exemple dans le cas où elles seraient dictées par des valeurs dans une séquence :

// dans ce cas, c'est Ok
template <class It>
   void franchir_etapes(It debut, It fin) {
      for(; debut != fin; ++debut) {
         switch(*debut) {
         case 0:
            etapeA();
            break;
         case 1:
            etapeB();
            break;
         case 2:
            etapeC();
            break;
         case 3:
            etapeD();
            break;
         }
      }
   }

Le bon usage : si les étapes doivent être franchies en séquence... alors, la structure de contrôle souhaitée est une séquence!

Entrée 02.0
void franchir_etapes() {
   etapeA();
   etapeB();
   etapeC();
   etapeD();
}

Voir aussi :


Valid XHTML 1.0 Transitional

CSS Valide !