Les effets de bord

Il existe un principe de bonne programmation qu'on est en droit de nommer principe de localité, et qui pourrait s'exprimer comme suit:

Cela signifie que, à moins qu'il soit impossible de l'éviter[1], les variables globales seront considérées comme une erreur de programmation et seront sévèrement pénalisées.

Il y a plusieurs raisons à cet interdit. L'une d'entre elles s'illustre d'ailleurs avec une certaine facilité--les variables globales introduisent la possibilité très forte de ce qu'on appelle un effet de bord.

Qu'est-ce qu'un effet de bord?

Un effet de bord est une erreur de logique, souvent difficile à découvrir--et ardue à déverminer--se produisant lorsqu'une variable voit sa valeur altérée à un endroit dans le code où cette altération n'est pas justifiée.

Présumons qu'on veuille produire le programme nous permettant d'obtenir la forme géométrique visible à droite.

Nous pourrions y parvenir avec le programme suivant. Remarquez qu'il fait usage de variables globales, ce qui rend le programme en question fragile face aux erreurs de logique et d'inattention--n'oubliez pas qu'il s'agit ici d'un problème très simple.

*
**
***
****
*****
******
*******
********
*********
**********

#include <iostream>
using namespace std;

const int MAX_LIGNES = 10;
int Cpt_Lignes, // indique la ligne en train d'être dessinée
    Max_Car,    // indique le max. de caractères sur la ligne
    Cpt_Car;    // indique le caractère en train d'être dessiné

void Dessiner_Ligne ();

void main ()
{
   Cpt_Lignes = 1;
   while (Cpt_Lignes <= MAX_LIGNES)
   {
      Dessiner_Ligne ();
      cout << endl;
      Cpt_Lignes ++;
   }
}

void Dessiner_Ligne ()
{
   Max_Car = Cpt_Lignes;
   Cpt_Car = 1;
   while (Cpt_Car <= Max_Car)
   {
      cout << "*";
      Cpt_Car ++;
   }
}

Ce programme fonctionne et donne de bons résultats.

Portez maintenant attention au programme de la page suivante: il est presque identique, mais une simple (et bête) erreur fera en sorte de «déformer la forme».


#include <iostream>
using namespace std;

const int MAX_LIGNES = 10;
int Cpt_Lignes, // indique la ligne en train d'être dessinée
    Max_Car,    // indique le max. de caractères sur la ligne
    Cpt_Car;    // indique le caractère en train d'être dessiné

void Dessiner_Ligne ();

void main ()
{
   Cpt_Lignes = 1;
   while (Cpt_Lignes <= MAX_LIGNES)
   {
      Dessiner_Ligne ();
      cout << endl;
      Cpt_Lignes ++;
   }
}

void Dessiner_Ligne ()
{
   Max_Car = Cpt_Lignes;
   Cpt_Car = 1;
   while (Cpt_Car <= Max_Car)
   {
      cout << "*";
      Cpt_Car ++;
   }
   Cpt_Lignes ++;
} 

La ligne rouge dénote l'erreur de logique dans ce cas (le compteur de ligne est incrémenté à un deuxième endroit, ce qui fait en sorte d'aplatir le triangle et d'en faire disparaître chaque ligne paire). La version à la page suivante est encore plus sournoise...


#include <iostream>
using namespace std;

const int MAX_LIGNES = 10;
int Cpt_Lignes, // indique la ligne en train d'être dessinée
    Max_Car,    // indique le max. de caractères sur la ligne
    Cpt_Car;    // indique le caractère en train d'être dessiné

void Dessiner_Ligne ();

void main ()
{
   Cpt_Lignes = 1;
   while (Cpt_Lignes <= MAX_LIGNES)
   {
      Dessiner_Ligne ();
      cout << endl;
      Cpt_Lignes ++;
   }
}

void Dessiner_Ligne ()
{
   Max_Car = Cpt_Lignes;
   Cpt_Lignes = 1;
   while (Cpt_Car <= Max_Car)
   {
      cout << "*";
      Cpt_Car ++;
   }
}

Pouvez-vous trouver l'erreur? Elle est très vilaine, et cause parfois des boucles infinies n'affichant rien de pertinent (allez ici pour une brève explication).

Un programme ne respectant pas le principe de localité se fragilise de lui-même. On ne sait pas où chaque variable est en droit d'être utilisée, où elle ne devrait pas l'être, et on ne tire pas profit de la force qu'a le compilateur de reconnaître les variables utilisées hors de leur contexte raisonnable.

En informatique, où les programmes sont souvent immenses, on ne peut se permettre de travailler ainsi. C'est trop dangereux, trop imprudent.

Respecter le principe de localité--comment?

Pour respecter le principe de localité, il faut d'abord se poser les questions suivantes, pour chaque variable: pourquoi ai-je déclaré cette variable? Quel est son rôle?

Dans notre problème--fort simple, faut-il le rappeler--le seul sous-programme autre que le programme principal («main()») se nomme «Dessiner_Ligne()», et a pour but de dessiner une ligne. On remarque que c'est dans ce sous-programme, et seulement à cet endroit, que les variables Max_Car et Cpt_Car sont utilisées. On fait donc une utilisation locale de ces variables.

Si l'utilisation de ces variables est locale, il tombe donc pleinement sous le sens de les déclarer localement.

La difficulté qui demeure alors est de s'assurer que les variables utilisées dans plus d'un sous-programme--comme, dans ce cas, Cpt_Lignes--respectent, elles aussi, le principe de localité. C'est à ce moment qu'entre en jeu la notion de paramètre.

#include <iostream>
using namespace std;

const int MAX_LIGNES = 10;
// la ligne en train d'être dessinée
int Cpt_Lignes;
void Dessiner_Ligne ();

void main ()
{
   Cpt_Lignes = 1;
   while (Cpt_Lignes <= MAX_LIGNES)
   {
      Dessiner_Ligne ();
      cout << endl;
      Cpt_Lignes ++;
   }
}

void Dessiner_Ligne ()
{
   // le max. de caractères sur la ligne
   int Max_Car,
   // le caractères en train d'être dessiné
       Cpt_Car;
   Max_Car = Cpt_Lignes;
   Cpt_Car = 1;
   while (Cpt_Car <= Max_Car)
   {
      cout << "*";
      Cpt_Car ++;
   }
}
        

Pour compléter le portrait: les paramètres

Les paramètres représentent le moyen de communication privilégié entre deux sous-programmes distincts.

Lorsqu'un sous-programme en appelle un autre--comme main() le fait avec Dessiner_Ligne() dans notre exemple--on nomme paramètre toute donnée passée de l'appelant (ici: main()) à l'appelé (ici: Dessiner_Ligne()).

Ici, on remarque que l'appelant (main()) se trouve à initialiser, tester et incrémenter la variable Cpt_Lignes. On pourrait dire que main() en est responsable, et qu'il devrait être celui qui déclare cette variable.

Le mécanisme de passage de paramètres devra, pour nous être utile, faire en sorte que l'appelant (main()) puisse passer à l'appelé (Dessiner_Ligne()) la valeur de Cpt_Lignes, mais sans que l'appelé puisse endommager cette variable et causer un effet de bord--car si un effet de bord demeurait possible, notre problème ne serait pas vraiment résolu.

Il se trouve que c'est effectivement le cas: le passage de paramètres en C++ se fait par valeur (voir l'annexe à ce sujet), ce qui protège la valeur de Cpt_Lignes tel que l'appelant (main()) le perçoit.

Une solution complète suit.


#include <iostream>
using namespace std;

const int MAX_LIGNES = 10;

// Nom:
//    Dessiner_Ligne ()
// Intrants:
//    Ligne représente l'index de la ligne à dessiner
// Extrants:
//    Ligne représente l'index de la ligne à dessiner
// But:
//   Dessiner la ligne «Ligne»
void Dessiner_Ligne (int Ligne);

// Nom:
//    main (), le programme principal
// Intrants:
//    aucun
// Extrants:
//    aucun
// But:
//   Dessiner un triangle à angle droit dont chaque ligne
//   croît en largeur d'un et un seul symbole
void main ()
{
   int Cpt_Lignes; // indique la ligne en train d'être dessinée
   Cpt_Lignes = 1;
   while (Cpt_Lignes <= MAX_LIGNES)
   {
      // Appel à Dessiner_Ligne(). Dessinera la Cpt_Lignes-ème
      // ligne du triangle
      Dessiner_Ligne (Cpt_Lignes);
      cout << endl;
      Cpt_Lignes ++;
   }
}

// Nom:
//    Dessiner_Ligne ()
// Intrants:
//    Ligne représente l'index de la ligne à dessiner
// Extrants:
//    Ligne représente l'index de la ligne à dessiner
// But:
//   Dessiner la ligne «Ligne»
void Dessiner_Ligne (int Ligne)
{
   int Max_Car,    // indique le max. de caractères sur la ligne
       Cpt_Car;    // indique le caractères en train d'être dessiné
   Max_Car = Ligne;
   Cpt_Car = 1;
   while (Cpt_Car <= Max_Car)
   {
      cout << "*";
      Cpt_Car ++;
   }
}

[1] ... ce qui ne se produira pas dans ce cours.

[2] la deuxième opération de «Dessiner_Ligne()» réinitialise le compteur de ligne à 1.