Les item-lists

Dernière mise à jour : 25/04/2009 21:40:41

Un forum web "Bien programmer en C" permet de poser des questions sur les articles, le C en général, la programmation etc.

Home

Introduction

Qui n'a jamais été confronté à ce problème :

"Comment faire le lien entre des constantes symboliques et leur représentation textuelle ?"

Le but des item-lists est de résoudre de façon la plus automatique possible ce genre de problème.

Mise en oeuvre

Le principe est de séparer les informations de bases constantes (par exemple, la correspondance entre caractère et chaine morse) dans un fichier indépendant (ni.c, ni.h, on y reviendra), mais que l'on peut inclure (#include) de façon à réaliser une génération automatique de code en fonction de la demande.

Je prends un exemple plus parlant.

Je peux créer une série de constantes qui représentent des fruits

enum fruits
{ BANANE, ORANGE, POMME, FRAISE, KIWI };

Ca peut servir à définir une masse de fruit :

struct dosage_fruit
{
   enum fruits fruit;
   int masse;
};

puis des recettes de fruits mixés :

struct dosage_fruit mix_energie[] =
{
   {BANANE, 100},
   {ORANGE, 150},
   {FRAISE, 80},
};

struct dosage_fruit mix_forme[] =
{
   {BANANE, 50},
   {ORANGE, 50},
   {POMME, 100},
   {KIWI, 50},
   {FRAISE, 50},
};

etc.

Si on veut afficher la recette, il faut un moyen simple pour convertir la constante fruit en une chaine imprimable.

On va donc utiliser un tableau de chaines construit sur le même modèle que le enum (même ordre, c'est primordial, car l'enum sert d'indice au tableau):

static char const *chaines_fruits[] =
{
   "banane",
   "orange",
   "pomme",
   "fraise",
   "kiwi",
};

ce qui permet d'afficher la composition :

void afficher_composition (char const *nom, struct dosage_fruit const *a,
                           size_t n)
{
   size_t i;
   printf ("%s\n", nom);
   for (i = 0; i < n; i++)
   {
      struct dosage_fruit const *p = a + i;
      printf ("%d g de %s\n", p->masse, chaines_fruits[p->fruit]);
   }
   printf ("\n");
}

que l'on appelle comme ceci :

#define N(a) (sizeof(a) / sizeof *(a))

{
   afficher_composition ("Mix energie", mix_energie, N(mix_energie));
   afficher_composition ("Mix forme", mix_forme, N(mix_forme));

   etc.

Ce qui donne

Mix energie
100 g de banane
150 g de orange
80 g de fraise

Mix forme
50 g de banane
50 g de orange
100 g de pomme
50 g de kiwi
50 g de fraise


Press ENTER to continue.

(je laisse au programmeur malin le soin d'écrire "d'orange" au lieu de "de orange"...)

Maintenant, patatras, nouvelle recette à la carte :

struct dosage_fruit mix_tropical[] =
{
   {BANANE, 50},
   {ORANGE, 80},
   {MANGUE, 100},
   {ANANAS, 50},
};

Quelles sont les conséquences sur le programme :

2 modifications :

  • l'enum :
    
    enum fruits
    { BANANE, ORANGE, POMME, FRAISE, KIWI, MANGUE, ANANAS };
    
  • la liste des chaines 'associées' (manuellement, pour le moment)
    static char const *chaines_fruits[] =
    {
       "banane",
       "orange",
       "pomme",
       "fraise",
       "kiwi",
       "mangue",
       "ananas",
    };
    

Si j'inverse ou que j'en oubli une, c'est la catastrophe. Idem, et c'est beaucoup plus sournois, si j'oublie une ','.

Donc, après quelques sueurs froides, ça marche, et on obtient bien :

Mix energie
100 g de banane
150 g de orange
80 g de fraise

Mix forme
50 g de banane
50 g de orange
100 g de pomme
50 g de kiwi
50 g de fraise

Mix tropical
50 g de banane
80 g de orange
100 g de mangue
50 g de ananas


Press ENTER to continue.

Mais c'est déjà beaucoup de stress dans un petit programme comme ça. Dans l'industrie, la moyenne, c'est 1 000 000 de lignes... Pas question de se stresser comme ça si, pour ajouter une valeur dans la liste, il faut modifier dans 10 fichiers différents... (Lesquels ? On n'est pas des robots...)

Par contre, on peut utiliser des techniques de programmations qui font que la maintenance est centralisée en un seul fichier. On le modifie, on recompile tout le projet, et les modifications sont automatiquement reportées dans tout le code.

Pour ça, on va faire travailler la machine à partir d'un fichier unique, pour qu'elle produise ce qu'on veut. C'est toute la puissance qu'offre le préprocesseur bien maitrisé.

Je vais montrer les différentes étapes pour expliquer le principe, mais dans la pratique, on ne fait que la dernière évidemment.

Dans notre exemple, Les deux éléments à "synchroniser" sont :


enum fruits
{ BANANE, ORANGE, POMME, FRAISE, KIWI, MANGUE, ANANAS };

et la liste des chaines 'associées' (manuellement, pour le moment)

static char const *chaines_fruits[] =
{
   "banane",
   "orange",
   "pomme",
   "fraise",
   "kiwi",
   "mangue",
   "ananas",
};

Si on observe bien ces deux éléments, on constate qu'ils se ressemblent. Pour être plus parlant, on va les réorganiser en colonnes :

enum fruits
{
   BANANE,
   ORANGE,
   POMME,
   FRAISE,
   KIWI,
   MANGUE,
   ANANAS
};

et la liste des chaines 'associées' (manuellement, pour le moment)

static char const *chaines_fruits[] =
{
   "banane",
   "orange",
   "pomme",
   "fraise",
   "kiwi",
   "mangue",
   "ananas",
};

On voit que le schéma est similaire :

  • une entête
  • une liste (chaque élément est terminé par une ,)
  • une fin particulière.

On voit que la liste des enum présente une petite irrégularité : le dernier élément n'est pas terminé par une ','. C'est normal (et obligatoire) en C90 (en C99, la virgule est acceptée).

Petite astuce : on ajoute un élément à la liste, qui sert à terminer la liste sans virgule. Il ne fait pas partie de la liste. C'est soit un 'dummy' (inutile), soit, comme ici où les valeurs sont automatiques, une constante qui exprime le nombre d'éléments de la liste, ce qui, à priori, n'est pas complètement inutile....

Modification :

enum fruits
{
   BANANE,
   ORANGE,
   POMME,
   FRAISE,
   KIWI,
   MANGUE,
   ANANAS,
   NB_FRUITS
};

Afin de faciliter la maintenance, il faudrait disposer d'une liste 'double' comme ceci :

BANANE "banane"
ORANGE "orange"
POMME  "pomme"
FRAISE "fraise"
KIWI   "kiwi"
MANGUE "mangue"
ANANAS "ananas"

Comment faire comprendre ça à un programme C ?

C'est la qu'intervient la puissance du préprocesseur.

Il suffit d'écrire une liste de macros avec deux paramètres. Chaque macro représentant un groupe cohérent d'informations ou 'item' :

ITEM (BANANE, "banane")
ITEM (ORANGE, "orange")
ITEM (POMME , "pomme" )
ITEM (FRAISE, "fraise")
ITEM (KIWI  , "kiwi"  )
ITEM (MANGUE, "mangue")
ITEM (ANANAS, "ananas")

Il suffit ensuite d'écrire la définition de ITEM qui correspond à l'usage qu'on en fait :

enum fruits
{
#define ITEM(id, chaine)\
   id,

ITEM (BANANE, "banane")
ITEM (ORANGE, "orange")
ITEM (POMME , "pomme" )
ITEM (FRAISE, "fraise")
ITEM (KIWI  , "kiwi"  )
ITEM (MANGUE, "mangue")
ITEM (ANANAS, "ananas")

#undef ITEM

   NB_FRUITS
};

ce qui va produire automatiquement la bonne liste.

On fait pareil avec l'autre liste :

static char const *chaines_fruits[] =
{
#define ITEM(id, chaine)\
   chaine,

ITEM (BANANE, "banane")
ITEM (ORANGE, "orange")
ITEM (POMME , "pomme" )
ITEM (FRAISE, "fraise")
ITEM (KIWI  , "kiwi"  )
ITEM (MANGUE, "mangue")
ITEM (ANANAS, "ananas")

#undef ITEM
};

Le #undef permet le 'recyclage' du nom de la macro qui est invariablement ITEM.

L'étape ultime consiste à inclure la liste à partir d'un fichier exterieur auquel je donne l'extension .itm (par exemple : fruits.itm semble approprié).

Je conseille de placer un commentaire dans ce fichier qui rappelle son nom et le début de la définition de la macro avec la signification des champs.

/* fruits.itm

#define ITEM(id, chaine)\

*/
ITEM (BANANE, "banane")
ITEM (ORANGE, "orange")
ITEM (POMME , "pomme" )
ITEM (FRAISE, "fraise")
ITEM (KIWI  , "kiwi"  )
ITEM (MANGUE, "mangue")
ITEM (ANANAS, "ananas")

La maintenance de ce fichier est extrêmement simple. Elle est "visuelle".

Les deux définitions deviennent alors :

enum fruits
{
#define ITEM(id, chaine)\
   id,
#include "fruits.itm"
#undef ITEM

   NB_FRUITS
};

et

static char const *chaines_fruits[] =
{
#define ITEM(id, chaine)\
   chaine,
#include "fruits.itm"
#undef ITEM
};

Ce qui allège considérablement le code source et rend la maintenance automatique. (Le C, c'est Bien)

Il peut y avoir des centaines de lignes dans un .itm. Idem pour le nombre de champs. Ici, il y en a 2 champs, mais on aurait pu en avoir qu'un seul. En effet, une macro sait transformer un paramètre symbolique (ORANGE) en une chaine ("ORANGE") avec #. Mais elle ne sait pas modifier la casse des caractères, d'où mon choix de mettre 2 champs.

Exemples de fichiers itm que j'utilise

(Simple) : gestion des erreurs

(Complexe) : tables de caractères

Je laisse au lecteur le soin d'écrire l'ensemble de l'exemple et de faire les tests nécessaires. Tous les éléments sont là.


Multi-fruits

Multi-fruits


Valid XHTML 1.0! Valid CSS! Get Firefox!  Use OpenOffice.org Club d'entraide des développeurs francophones Code::Blocks
© Emmanuel Delahaye 2006-2009 | emmanuel dot delahaye at gmail dot com | Prev | Home | Forum | Livre d'or