• Aucun résultat trouvé

Bases de la Programmation Création de types

N/A
N/A
Protected

Academic year: 2022

Partager "Bases de la Programmation Création de types"

Copied!
10
0
0

Texte intégral

(1)

Bases de la Programmation Création de types

V. Padovani, PPS - IRIF

First Law. A robot may not injure a human being or, through inaction, allow a human being to come to harm.

Second Law. A robot must obey the orders given it by human beings except where such orders would conflict with the First Law.

Third Law. A robot must protect its own existence as long as such protection does not conflict with the First or Second Law.

Handbook of Robotics, 56th Edition(2058 A.D.)

— I. Asimov.I, Robot(1950)

Je vous montrerai dans ce chapitre trois manières de créer vos propres types dans l’écriture d’un programme : comment définir uneénumeration, un ensemble de constantes nommées et rassemblées sous un même nom de type ; comment définir un type destructure permettant de rassembler plusieurs données, mêmes de types différents, sous un même nom de variable ; comment définir un typeunionpermettant de créer des variables dont le type de contenu peut changer au cours du temps.

J’illustrerai les éléments de ce chapitre par un unique exemple concret : la manière dont je choisirais les types d’un programme permettant la simulation d’un jeu de plateau.

Les régles exactes du jeu sont sans importance, je ne parlerai que de choix de types, pas d’implémentation – ces choix n’engagent que moi, ils ne sont qu’un exemple :

– Le plateau est carré et comme celui d’un plateau d’échecs. Les deux joueurs, blanc et noir, ne disposent que d’une seule sorte de pions de leurs couleurs.

– Un pion peut se déplacer horizontalement ou verticalement d’un nombre quel- conque de cases, atterrir sur une case vide ou sur un autre pion, qui est alors retiré du plateau. Des règles limitent ces possibilités de déplacement et de prise.

– Un joueur ne peut déplacer que les pions de sa couleur.

Comment modéliser un tel jeu en C ? Le premier point qui me vient à l’esprit est celui-ci.

Il me faudra écrire le mécanisme de gestion de la partie simulée, mais ce point est très secondaire : une simple boucle d’interaction, desprintfet desscanfferont l’affaire. Cette partie du code sera triviale à écrire, et je ne l’écrirai qu’en dernier.

Le vrai problème est ailleurs. Il faudra bien, à un moment ou un autre, écrire une fonctionjouant effectivement un coup sur le plateau, ou plus exactement tentant de jouer une demande de coup de la part d’un des deux joueurs. Cette fonction devra : vérifier si la demande de coup est valide, c’est-à-dire si elle est conforme aux contraintes du jeu ; si elle l’est, modifier en conséquence la représentation du plateau en mémoire ; sinon, signaler à l’appelant – le mécanisme de gestion de la partie – que cette demande de coup est invalide.

Les seules informations dans une demande de coup sont : le plateau, le joueur dont c’est le tour, la position de départ du coup, sa direction, le nombre de cases du déplacement.

Ce sont ces éléments dont je choisirai d’abord la représentation en mémoire, avant même de songer à écrire cette fonction centrale.

(2)

1 Les énumérations

Je m’imposerai ici une règle incontournable : aucune valeur numérique liée à la représen- tation du jeu ne devra apparaître dans le corps des fonctions – à aucun endroit.

Je vous ai montré, dans le chapitre sur les tableaux, comment définir des constantes par #define. Le procédé est commode, mais il est exclu que je perde mon temps à écrire des pages entières de #define. Une demande de coup peut échouer pour plusieurs raisons et idéalement, la fonction centrale du jeu devrait donner une indication précise de son échec ou de sa réussite, c’est-à-dire répondre par exemple :

– position de départ du coup invalide,

– case de départ ne contenant pas un pion du joueur,

– nombre de cases de déplacement faisant sortir le pion du plateau, – déplacement ou prise contraire aux règles du jeu,

– coup valide, sans capture, – coup valide, avec capture.

Plutôt que faire six définitions de constantes, le C permet de définir un nouveau type pour ces réponses : un type énuméré, une énumerations de constantes.

Dans le code ci-dessous, qui devra être placé dans le programme après les #include mais avant toute définition de fonction. Le mot enum est ici un mot-clef du langage : enum reponse {

ERR_POS, ERR_PION, ERR_SORTIE, ERR_REGLE, VAL_VIDE, VAL_CAPTURE };

Le typeenum reponseest encore un type entier numérique : sa définition fait ce que ferait une suite de #define associant chaque nom à un entier, en commancçant par zéro et faisant croître les entiers de un en un : ERR_POSvait 0,ERR_CAS vaut 1, etc.

Mais cette définition fait plus : elle crée un nouveau type dans l’environnement de compilation. On peut déclarer des variables de ce type comme des variables ordinaires et leur donner une valeur, pourvu que cette valeur soit l’une des constantes :

enum reponse r;

r = ERR_POS;

Ce type sera, bien sûr, le type de retour de la fonction centrale. J’aurais pu utiliser intet des constantes, mais la création de ce type fait d’avantage : elle permettra de voir dans le code qu’une variable est destinée à contenir une réponse et rien d’autre, même si son nom est neutre. La déclaration suivante ne dit rien sur ce que va contenir r:

int r;

La suivante, si : enum reponse r;

(3)

Notez qu’il n’y a qu’une seule constante associée au non–respect des règles : on pourrait en définir plusieurs, donnant des informations plus précise sur la ou les règles enfreintes.

1.1 L’opérateur typedef

L’écriture répétitive du mot-clef enum à chaque usage du type enum reponse peut devenir rapidement lassante : une réponse est une réponse, pas une enum–réponse. L’opérateur typedef permet de créer unaliaspour un type donné, et le procédé suivant est ordinaire : créer un alias pour enum reponse appelé reponse, sans le enum. immédiatement après la définition du type énuméré.

typedef enum reponse reponse;

Autre procédé courant : définir une énumération anonymeet lui donner à la volée un alias – dans ce cas, le type enum reponse n’existe plus, seul le type reponse est créé :

typedef enum { ERR_POS, ERR_PION, ERR_SORTIE, ERR_CASE, VAL_VIDE, VAL_CAPTURE } reponse;

C’est cette forme de définition dont je me servirai dans les sections suivantes.

1.2 Modélisation du matériel de jeu

Les joueurs, les directions possibles d’un coup et les contenus possibles d’une case du plateau se représentent de manière naturelle par des énumérations supplémentaires : typedef enum {

BLANC, NOIR } joueur;

typedef enum {NORD, SUD, EST, OUEST} direction;

typedef enum {VIDE, PION_BLANC, PION_NOIR} contenu;

Reste le choix de la représentation du plateau. Il est possible que ses dimensions soient variables et soient convenues en début de partie, mais je n’ai pas encore présenté les outils permettant de gérer ce genre de choses – je n’en parlerai qu’au chapitre suivant.

Je ferai ici au plus simple. Le plateau sera de taille définie par une constante definie par un#define, mais avec une contrainte forte, la valeur de cette constante devra pouvoir être librement modifiée, sans que la compilation ou l’exécution du programme recompilé échouent :

#define TAILLE 8

(4)

Le choix de la représentation du plateau dans ce cas immédiat : une matrice de taille TAILLE×TAILLEd’éléments de typecontenu. Quelque part dans l’une des fonctions gérant le début du jeu, il me faudra déclarer le plateau de la manière suivante :

contenu plateau[TAILLE][TAILLE];

Compléments

Certains noms que j’ai choisis pour les énumérations pourraient être abrégés sans nuire à la lisibilité du code : rep,dir, contplutôt quereponse, direction,contenu, par exemple.

À moins que vous ne souhaitiez participer l’International Obfuscated C Code Contest1, il vaut mieux éviter de réduire un nom de type à moins de trois lettres, et il vaut mieux finir un nom abrégé sur une consonne, ce qui est plus facile à compléter mentalement. Rien ne vous empêche par ailleurs d’écrire votre code en anglais, ce qui est une bonne habitude.

Autre remarque : j’ai mentionné à la fin des cours sur les tableaux et sur les pointeurs la possibilité d’appeler une fonction sur un nom de tableau à une dimension, mais pas sur le nom d’une matrice. C’est évidemment possible, et trois formes équivalentes existent pour le paramètre correspondant. Une fonction prenant un plateau en argument peut être de l’une ou l’autre des formes suivantes :

// forme avec tailles explicites

/* type et nom */ (contenu p[TAILLE][TAILLE], /* autres parametres */) { /* instructions */

}

// forme avec taille en premiere dimension non specifiee

/* type et nom */ (contenu p[][TAILLE], /* autres parametres */) { /* instructions */

}

// forme reelle compilee, de type pointeur

/* type et nom */ (contenu (*p)[TAILLE], /* autres parametres */) { /* instructions */

}

A la compilation, les deux premières formes seront toujours traduites en la troisième, qui est le type réel du nom d’une matrice représentant le plateau après sa conversion en pointeur – vous pouvez utiliser la première ou la seconde forme si elle vous semble plus claire intuitivement, mais gardez en tête que ce qui est passé à la fonction n’est pas une

“valeur de matrice” mais bien un pointeur, et que l’usage de p[i][j] dans la fonction accèdera bien au contenu de la matrice dont le nom est passé en argument.

La raison de ce typage pourpest la suivante : la variable plateau ci-dessus est en fait un tableau de tableaux dont les éléments sont des tableaux à TAILLE éléments. Le type des éléments deplateau est donccontenu[TAILLE], et la conversion automatique du nom du plateau produira un pointeur vers contenu[TAILLE], de type contenu(*)[TAILLE], qui est aussi le type de p. Notez que seule la taille en la seconde dimension du plateau laisse une trace dans ce type, ce qui explique la possiblité de ne pas spécifier cette taille dans

1. www.ioccc.org. Ça vaut le détour.

(5)

la seconde forme – dans la première, elle n’est spécifiée que pour le confort de lecture du programmeur, en soulignant le fait que la taille en nombre de lignes des matrices manipulées sera toujours égale à la constante écrite.

Des formes similaires existent pour le passage à une fonction d’un nom de tableau à une seule dimension : on peut écrire int t[] plutôt que int *t, ou mêmeint t[TAILLE]

pour insister sur le fait que la taille des tableaux dont le nom sera passé à une fonction sera toujours TAILLE. Je n’ai pas voulu m’en servir dans ce cours, car elles me semblent d’avantage semer la confusion qu’autre chose.

2 Les structures

Certaines des informations qu’il faudra transmettre à la fonction centrale sont clairement liées entre elles. Par exemple, la position d’un coup est spécifiée par un numéro de ligne et un numéro de colonne. Il serait logique de rassembler ces deux informations en une seule – après tout, une position est une position, ou ne serait-ce que pour éviter une suite de paramètres trop longue dans les fonctions travaillant sur des positions.

C’est précisément ce que permettent de faire les structures : rassembler en une variable unique plusieurs données, même lorsqu’elles sont de type distinct, et sans même les limites imposées aux tableaux : les variables de structures sont librement réaffectables. Avant de pouvoir se servir de telles variables, il faut définir leur type à l’aide d’une définition de type de structure.

2.1 Modélisation des positions

Le code ci-dessous doit encore être placé dans le programme après les #include, avant toute définition de fonction.

struct position {

int i // numero de ligne int j // numero de colonne };

Cette définition ne réserve aucun espace en mémoire, elle se contente de créer un nouveau type, de nomstruct position. On peut ensuite déclarer dans toute fonction des variables d’un type de structure défini, plus simplement appelées des “structures” :

struct position pos;

Comme pour les énumérations, il est courant d’utiliser un typedef pour s’épargner l’écri- ture répétitive du mot-clef struct, en définissant un type de structure anonyme auquel on donne immédiatement un nom :

typedef struct { int i;

int j;

} position;

Chaque variable de type position a deux “compartiments” appelés ses champs. Ces champs sont typés et nommés sur le modèle fourni par la définition du type de cette variable. Les champs iet jde chaque variable de type positionsont ici spécifiés comme

(6)

devant être de type int. On accède à l’un des champs en faisant suivre le nom de la variable du nom du champ et en les séparant par un point :

position pos;

// ...

pos.i = 1;

// ...

pos.j = pos.i + 1;

On peut initialiser les champs d’une structure au moment de sa déclaration, il suffit de fournir les valeurs d’initialisation de ses champs, dans le même ordre d’énumération que dans la définition de son type :

position pos = {3, 4}; // pos.i == 3, pos.j == 4

Comme avec les tableaux, l’initialisation commence par le premier champ, et tout ce qui n’est pas fourni reçoit une valeur nulle.

2.2 Modélisation des coups

La position d’un coup, sa direction, son nombre de cases de déplacement et le joueur souhaitant jouer ce coup sont des informations de natures différentes, mais elles sont aussi liées – là encore, un coup est un coup, et il serait commode de rassembler ces informations sous un type commun.

Le code ci-dessous doit encore être placé dans le programme après les#include, avant toute définition de fonction, mais après les definitions de position etdirection.

typedef struct { position pos;

direction dir;

int nbr;

joueur camp;

} coup;

Notez qu’il s’agit d’une structure dont le premier champ est lui-même d’un type de struc- ture. L’accès à ses données internes nécessite de doubler les noms de champs :

coup c = {{1, 2}, EST, 6, NOIR};

// ...

c.pos.i = 3;

c.pos.j = 4;

2.3 Propriétés des structures, pointeurs vers structures

Les structures se manipulent presque comme des variables ordinaires. La seule restriction est l’impossibilité de les comparer par == ou != et cette restriction est la seule. Comme pour les types de base, le langage manipule des valeurs de structures, copiables d’une variable vers une autre de même type :

coup c = {{3, 4}, SUD, 2, BLANC};

coup d = c;

(7)

On peut aussi déclarer les paramètres d’une fonction comme attendant une valeur de structure, une fonction peut renvoyer une valeur de structure, etc. Mais tout comme l’usage d’affectations, ces procédés restent limités aux “petites” structures : la copie en mémoire d’une valeur de structure est d’autant plus coûteuse qu’elle occupe d’espace en mémoire. Avec une structure de taille très grande, il serait illucide de vouloir utiliser partout dans le code des passages de valeurs de structures ou des retours de valeurs de structures.

A moins qu’il ne s’agisse d’une structure de taille très réduite (e.g. une position), l’usage est de passer à une fonction un pointeur vers une structure plutôt que la valeur de cette structure, à l’aide d’un paramètre de la forme coup *pc. Cet usage est si fréquent qu’il existe un raccourci d’écriture, celui à utiliser, pour accéder à la structure pointée. Si pc pointe vers une structure de type coup, les instructions suivantes sont équivalentes : // notation classique : (1) dereferencement, (2) acces au champ

(*pc).camp = NOIR;

(*pc).pos.i = 3;

// notation abregee pc -> camp = NOIR;

pc -> pos.i = 3;

2.4 Protoype de la fonction centrale

Toutes ces définitions permettront au final de réduire à deux paramètres la forme de la fonction gérant les demandes de coups : un plateau sous forme de (pointeur après conversion d’un nom de) matrice, un pointeur vers la structure décrivant le coup souhaité.

resultat jouer (contenu p[TAILLE][TAILLE], coup *pc);

Une dernière remarque importante. Si les règles du jeu le permettent, il sera possible d’abstraire la direction du coup dans l’écriture de cette fonction, c’est-à-dire de ne jamais avoir à tenir compte dans le traitement écrit de sa valeur exacte.

Si par exemple le test du respect des règles n’impose que d’explorer virtuellement la position cible ainsi que toutes les positions intermédiaires entre l’origine et cette position cible, il suffit de déléguer le calcul de toutes ces positions à une fonction auxiliaire :

– prenant en entrée une valeur de position, une valeur de direction, un nombre, – renvoyant la valeur de position atteinte à partir de la position donnée, dans la

direction donnée en se déplaçant du nombre de cases donné.

Sans cette fonction auxiliaire, réduite à quelques lignes, il faudrait écrire dans la fonction jouerquatre fois le même code (ou presque), une fois par direction, ce qui serait une erreur d’implémentation très élémentaire2. Cette technique est un exemple de “factorisation” du code : elle est à utiliser partout où elle permet d’éviter de programmer par copier-coller.

2. J’ai vu pire dans un projet : deux couleurs de pièces se déplaçant dans toutes les directions etseize fois le même code, une fois par couleur et par direction. Inutile de dire que la note n’était pas brillante.

(8)

3 Les unions

Supposons qu’il y ait plusieurs sortes de coups, par exemple deux (trois, quatre. . .) : la précédente ; déposer un pion de sa couleur sur un case vide du plateau.

Plutôt que de disperser la fonction jouer en deux (trois, quatre. . .) versions, il est à mon avis plus satisfaisant de conserver un type commun coup pour toutes les sortes de coups, tout en préservant la centralité de la fonction jouer – après tout, un coup est un coup, et jouer c’est jouer – quitte à ce que cette fonction se résume à un simple aiguillage vers des traitements plus spécialisés.

Le problème est que les informations liées à un déplacement et une pose sont de natures très différentes. On pourrait songer à multiplier les champs du type coup, y ajouter une information précisant la nature du coup souhaité, ignorer les champs qui n’ont pas de signification pour le coup joué, etc., mais cette solution est artificielle, le langage permet de faire mieux.

Un langage à objets serait parfait pour implémenter cette option de manière très naturelle et élégante, mais il faudra en C se limiter à la solution suivante. Le langage permet effectivement de conserver ce type coupcommun à l’aide d’unedéfinition d’union.

3.1 Modélisation de plusieurs sortes de coups, version basique

Les définitions suivantes créent, comme les précédentes, de nouveaux types. Le dernier créé dans ce code–source est le type union coup, un type d’union.

typedef struct { position pos;

direction dir;

int nbr;

joueur camp;

} depl;

typedef struct { position pos;

joueur camp;

} pose;

union coup {

depl info_depl;

pose info_pose;

};

Une union n’est pas une structure, elle ne peut contenir qu’une seule donnée à la fois.

Mais une variable cde type union coup pourra contenir à chaque instant soit une valeur de type depl, soit une valeur de type pose.

La taille d’allocation d’une telle variable est suffisamment grande pour contenir l’une ou l’autre de ces valeurs. Les noms de champs permettent de spécifier le type de la valeur stockée lors d’une affectation. Ce type doitêtre celui du type associé à ce nom de champ, sous peine de stockage d’une valeur indéfinie :

(9)

union coup c;

depl d = {{1, 2}, SUD, 3, NOIR};

c.info_depl = d;

pose p = {{3, 4}, BLANC};

c.info_pose = p;

Le même nom de champ permet de relire la valeur stockée dans une union, pourvu que le type associé à ce nom de champ soit bien celui de la valeur stockée, là encore sous peine de lecture d’une valeur indéfinie.

union coup c;

depl d = {{1, 2}, SUD, 3, NOIR};

c.info_depl = d; // c.info_depl de type depl, egal a d direction cd = c.info_depl.dir; // SUD == d.dir

int ci = c.info_depl.pos.i; // 1 == d.pos.i

pose p = {{3, 4}, BLANC};

c.info_pose = p; // c.info_depl de type pose, egal a p joueur cc = c.info_pos.camp; // BLANC == p.camp

int cj = c.info_pos.pos.j; // 4 == p.pos.j

Les affectations c.info_depl = d et c.info_pose = p copient respectivement les valeurs de d etp dans l’union c. Chaque valeur se relit, avec son type d’origine, à l’aide du nom de champ utilisé pour son stockage. Comme pour struct, on définit souvent une union à l’aide d’un alias, cette définition créant alors un nouveau type sans le mot-clef union : union {

depl info_depl;

pose info_pose;

} coup;

Le raccourci d’écriture des pointeurs vers structures est encore valide pour les pointeurs vers unions. Si pu pointe vers une union de typecoup, on peut écrire :

pu -> info_depl.dir = SUD;

3.2 Modélisation de plusieurs sortes de coups, version améliorée

Le principe de la définition du type coup précédente pose problème. La fonction jouer ne pourra pas a priori savoir quelle est la nature du coup dont l’adresse lui est passée en argument : cette information n’est inscrite nulle part dans l’union.

Il est cependant possible d’inscrire effectivement cette information dans une valeur d’union, en tirant parti de la manière dont les structures sont stockées en mémoire. Une structure est divisée en champs, mais stockée en un seul segment d’unités-mémoire. La norme du langage garantit que le tout premier champ de la structure sera stocké au tout début de ce segment, quel que soit le type de cette structure.

(10)

Si l’on ajoute aux deux définitions de types de structuresdepl etpose un premier champ d’un type commun et spécifiant la nature du coup joué, ce champ sera donc au début des espaces alloués aussi bien pour les variables de type depl que pour celles de type pose, même si ces types ne sont pas de même taille :

typedef enum {DEPL, POSE} type_coup;

typedef struct {

type_coup info_coup;

position pos;

direction dir;

int nbr;

joueur camp;

} depl;

typedef struct {

type_coup info_coup;

position pos;

joueur camp;

} pose;

Lors de l’affectation d’une union par une valeur de structure de l’un ou l’autre de ces types, le premier champ de la structure sera également au début du segment alloué pour l’union.

Rien n’interdit de lire la valeur de ce champ commun aux deux types de structures, il suffit d’ajouter à l’union un champ uniquement dédié à la lecture de cette valeur : typedef union {

type_coup info_coup;

depl info_depl;

pose info_pose;

} coup;

Il faudra toutefois respecter deux contraintes pour que cette information soit consistante avec le type de la valeur stockée dans une union :

1. le champinfo_coupd’une valeur de typedeplstockée dans une union de typecoup devra toujours être égal à DEPL,

2. le champinfo_coupd’une valeur de typeposestockée dans une union de typecoup devra toujours être égal à POSE.

Si cette convention est respectée on poura alors écrire dans la fonction jouer : resultat jouer (contenu p[TAILLE][TAILLE], coup *pc) {

switch (pc -> info_coup) { case DEPL :

// extraction et gestion de pc -> info_depl, de type depl // ...

case POSE :

// extraction et gestion de pc -> info_pose, de type pose // ...

// ...

} //..

}

Références

Documents relatifs

aires constantes entre deux points d’une même sphère; et celui de la détermination des congruences à foyers associés équidistants d’une droite fixe, à celui de

CIRCUIT ELECTRIQUE EN DERIVATION. Circuit avec une dérivation. Une installation domestique classique est constituée d’appareils en dérivation. Réaliser un montage avec une

La fission nucléaire est l’éclatement d’un noyau lourd et instable en deux noyaux plus légers et quelques neutrons. Cet éclatement s’accompagne d’un dégagement de

Ecrire une fonction qui, à partir d’une chaîne de caractères (qui peut être aussi longue que l’on veut, comme une encyclopédie), renvoie un dictionnaire dont chaque item a

Ecrire une fonction qui, à partir d’une chaîne de caractères (qui peut être aussi longue que l’on veut, comme une encyclopédie), renvoie un dictionnaire dont chaque item a pour

Pour exécuter le programme par exemple pour e = 0,2, taper terme(0.2) dans l’écran de calculatrice puis valider. Ceci évite d’entrer dans le programme une ligne du type Input

create type t_vehicule as object(numveh integer, tarif float, dateprochrev date, gere_par ref t_agence, revision t_set_ref_revision ) not final;b. create

Elle n'est accessible que depuis l'intérieur du bloc ou elle est déclarée, mais elle est créée au début du programme et elle existe aussi longtemps que dure l'exécution de