5. Bien programmer: évitez la redondance

5.1. #define

La redondance est une plaie en programmation. Elle n'est pas génante lorsqu'un écrit le programme, mais devient un véritable danger dès qu'on le modifie. Le meilleur exemple est celui-ci:

char chaine[1024];
int i;

for(i=0; i < 1024, i++) chaine[i] = 0;
			

Voyez-vous le bogue? Non? Vraiment pas? Normal: il n'y en a pas! Mais supposons: on m'a demandé de n'utiliser que 256 caractères pour la chaine: voici ma modification:

char chaine[256];
int i;

for(i=0; i < 1024, i++) chaine[i] = 0;
			

Là, le bogue est flagrant: j'ai oublié de modifier 1024 partout où il apparaissait. J'aurais du effectuer une recherche, mais si elle est manuelle, elle peut mettre un temps énorme suivant la taille du programme, et de plus je peux faire des erreurs. Si elle est automatique, je risque de modifier des 1024 en 256 alors que ces 1024 aurait du rester 1024.

Solution: utiliser l'instruction #define du préprocesseur du C, ou équivalent dans les autres langages.

#define LENGTH_MAX
char chaine[LENGTH_MAX];
int i;

for(i=0; i < LENGTH_MAX, i++) chaine[i] = 0;
			

Dans le cas qui nous intéresse, on peut même se passer du #define. Cela donne:

char chaine[256];
int i;

for(i=0; i < sizeof(chaine), i++) chaine[i] = 0;
			

Notez au passage que sizeof ne sert pas qu'à connaître la taille d'un type, mais aussi la longueur d'un tableau statique. Ceci est bien sur impossible pour un tableau dynamique. Notez que j'ai remis 256. En fait, si cette chaîne est utilisée dans une partie de code très restreinte, cela peut aller. Mais si elle est utilisée à de nombreux endroits ou si la longueur qui conditionne cette chaîne définit la longueur d'autres chaînes, le #define est obligatoire.

5.2. Ne faites pas n'importe quoi avec les constantes

Bien sur, définir des constantes comme ce qui suit est d'apparence ridicule. En fait, c'est pire, c'est dangereux:

#define ONE 1
#define TWO 2
#define CR '\n'
#define UNSET -4
			

En effet, sous ces définitions se cachent à la fois une volonté de bien faire en utilisant des constantes plutôt que des valeurs codées en dur, et une redondance inutile ou très dangereuse suivant ce qu'on en fait. En effet, si pour une raison ou pour une autre j'ai à modifier ONE pour mettre 5, par exemple parce que j'utilisais le champ 1 d'un tableau et que ce champ se retrouve à 5, alors je vais définir ONE avec la valeur 5. Deux effets aussi dangereux l'un que l'autre apparaissent:

  • ONE était utilisé ailleurs: dans ce cas, on va utiliser 5 au lieu de 1. Le programme va avoir un fonctionnement anormal.

  • Lorsqu'on va vouloir mettre 1 en écrivant du nouveau code, on va vouloir faire bien en utilisant la constante ONE, mais cela mettra 5 au lieu du 1 désiré.

Banissez ces définitions de constantes: elles sont inutiles et dangereuses, et comme elles n'ont pas d'utilité, elles rallongent le code, le rendent moins lisible, et accessoirement font perdre quelques microsecondes au préprocesseur.

5.3. Les énumérations

Avec tout cela, l'instruction #define reste à éviter lorsqu'on peut utiliser un enum Ainsi, plutôt que de définir TRUE et FALSE ainsi:

#define FALSE 0
#define TRUE 1
			

il vaut mieux créer une énumération:

typedef enum {
FALSE=0,
TRUE
} my_boolean;
			

Ensuite, plutôt que d'utiliser le type int, on utilisera le type my_boolean ce qui permettra au compilateur d'effectuer une vérification de type plus efficace. D'ailleurs, je suis récemment tombé sur un cas où le gcc m'a surpris:

typedef enum
{
 UNSET = 0,
 USED = 1,
 UNUSED = 2,
 BROKEN = 3
}
my_type;

int
main ()
{
 my_type m = UNSET;
 switch (m)
 {
 case USED:
 break;
 case UNUSED:
 break;
 case BROKEN:
 break;
 }
 return (0);
}
			

J'ai compilé le programme ainsi: gcc -Wall test.c -o t. Voici la réponse du compilateur:

t.c: In function `main':
t.c:22: warning: enumeration value `UNSET' not handled in switch
			

Jamais je n'aurais eu ce warning avec un #define. Ici, pour supprimer le warning, il faut simplement ajouter la clause default: dans le switch.

5.4. Les noms des constantes

Avant de finir avec les définitions de constantes, remarquez qu'il existe deux manière de faire pour créer une variable et lui associer du contenu. L'une, la mauvaise est de donner un nom générique à la variable et un nom explicite à la constante. Cela peut donner:

mode = FILE_NAME_IS_KNOWN;
			

L'autre, bonne, est le contraire: un nom de variable explicite et un nom de constante commun:

is_file_name_known = TRUE;
			

Il faut savoir qu'une variable doit toujours contenir un nom explicite. Cela facilite la compréhension car il lui arrive souvent d'être séparée de sa valeur dans le code: un appel à la fonction my_function montre cela:

my_fonction(mode)
			

est bien moins explicite que

my_fonction(is_file_name_known)
			

Dans le premier cas, on ne sait pas ce qui est mis en paramètre. Dans le second cas, oui.

Cependant, les constantes ont tendance à servir à tout. Elles doivent soit avoir un nom extrèmement général, comme TRUE et FALSE, soit avoir un nom très spécifique, avec un préfixe, par exemple:

typedef enum
{
FILE_NAME_KNOWN_UNSET,
FILE_NAME_KNOWN_TRUE,
FILE_NAME_KNOWN_FALSE
} File_Name_Known;
			

5.5. Encapsulation ou pseudo-héritage de structures

La redondance est connue pour apparaître dans les fonctions. Si deux fonctions effectuent la même séquence d'instructions, il est nécessaire d'extraire ce code, de créer une nouvelle fonction avec ce code, et d'appeler la nouvelle fonction dans les deux autres fonctions. De même, il faut éviter d'avoir des fonctions en double: si on corrige un bogue dans l'une, on risque d'oublier de le corriger dans l'autre. Cela est habituel pour un programmeur. Ce qui l'est moins, c'est la redondance dans la structure de données. Elle peut apparaître de deux manières: dans l'utilisation de la structure et dans la définition. Le premier cas consiste à avoir deux fois les mêmes données en mémoire. Cela est signe d'une mauvaise conception de l'application ou d'une mauvaise connaissance de son code qui pousse à créer une nouvelle représentation d'une données alors qu'elle existe déjà. Dans certains cas, un pointeur vers la donnée existante résoud le problème. Dans les autres cas, il faut complètement revoir la structure de données, et par effet de bord indésirable mais prévisible, réécrire le code.

Le cas où la définition de la structure de données duplique explicitement les données doit être évité dès la conception de l'application. Voici un exemple de travail:

typedef struct
{
 int a;
 int b;
}
petit;

typedef struct
{
 int a;
 int b;
 int c;
}
grand;

petit p;
grand g;
p.a=0;
g.a=0;
			

Les langages à objets proposent tous la même solution pour éviter cette redondance: l'héritage. En C, avant de parler d'héritage, on peut déjà penser aux pointeurs:

typedef struct
{
 int a;
 int b;
}
petit;

typedef struct
{
 petit *p;
 int c;
}
grand;

grand g;
g.p = (petit*)malloc(sizeof(petit));
g.p-< a=0;
			

Cette méthode nécessite un pointeur et beaucoup de traitement d'erreur:

  • Et si malloc renvoit NULL?

  • Et si g.p n'a pas été initialisé?

  • Et si g est un pointeur qu'un libère, a-t-on pensé à libérer g->p?

  • ...

Une méthode plus élégante est d'encapsuler la structure petit dans la structure grand.

typedef struct
{
 int a;
 int b;
}
petit;

typedef struct
{
 petit p;
 int c;
}
grand;

grand g;
g.a=0;
			

Cela ressemble plus a de l'héritage. En effet, toutes les fonctions qui prennent en argument une structure petit peuvent prendre une structure grand, à la seule condition de ne pas libérer la mémoire de la structure car certains éléments de grand qui ne font pas partie de petit peuvent pointer sur de la mémoire allouée qu'il faut libérer avant de libérer la mémoire de la structure grand. C'est ce mécanisme qui est utilisé dans glib et dans gtk+ qui permet d'avoir par exemple les boutons dériver des gtk_widget par exemple. Ainsi, gtk_widget_show marche pour tous les widgets de gtk car tous dérivent de gtk_widget.

5.6. Le copier/coller est dangereux

Le copier/coller est de la pure génération de redondance. En effet, copier/coller un bloc d'instructions consiste à avoir deux exemplaires du même algorithme ou du même sous-algorithme. J'ai indiqué plus haut pourquoi cela était génant. Plutôt que de faire du copier/coller, il vaut mieux faire comme plus haut, à savoir extraire le code à copier/coller et en faire une nouvelle fonction. Ainsi, à la place de faire le copier/coller, il suffit de faire appel à la nouvelle fonction.

Un copier/coller est souvent signe qu'un bloc de code exécute un algorithme élémentaire. Créer une fonction pour cet algorithme est donc une chose aisée car si on veut l'utiliser plusieurs fois, c'est qu'on dispose de données à traiter dans une même structure de données, et dont le résultat est aussi dans une même structure de données. L'isolement consiste dans ce cas à donner un nom à la nouvelle fonction, puis à mettre en argument la structure de données en entrée, et à renvoyer à la fin la structure de données en sortie. Une première compilation indiquera via les erreurs les variables locales à déclarer, et un bon compilateur comme gcc avec l'option -Wall indiquera les variables inutiles là où a été extrait le code de la nouvelle fonction. Une erreur courante dans cette opération est d'oublier de faire appel à la nouvelle fonction là ou se trouvait le code déplacé.

Un copier/coller est dangereux aussi parce que souvent, il y a des petites modifications à apporter au code collé. Le fait qu'il compile avant fait qu'il risque de compiler après, même si des modifications ont été oubliées. Cela est donc dangereux à cause du risque d'oubli.

Enfin, si un copier/coller se justifie pour accélérer le code (par exemple exécuter plusieurs fois la même instruction plutôt que de la mettre dans une boucle), il reste préférable de faire appel au pré-processeur qui se chargera de dupliquer lui-même la ligne. On évite ainsi la redondance, ou plutôt, on la signale explicitement avec les instructions du préprocesseur.

Réfléchissez dont toujours avant de copier/coller un bout de code: est-ce nécessaire ou y a-t-il moyen de faire autrement? Un couper/coller est bien moins dangereux car il n'y a pas de génération de code redondant. C'est du déplacement de code.

création est mise à disposition sous un contrat Creative Commons