Abstraction des données stockées
Paramétrer le type des éléments
Nous voulons adapter nos implémentations de set pour pouvoir stocker n’importe
quel type de données et non pas seulement des int. Nous pourrions par exemple
vouloir stocker, des nombres flottants float, ou des types
structurés, comme struct person (le triplet (day,month,year) représente la date de naissance):
struct person {
char * firstname;
char * lastname;
unsigned int year;
unsigned char month;
unsigned char day;
};
Une solution consiste à avoir plusieurs définitions du type struct set, par
exemple (pour l’implémentation avec sentinelle):
struct int_set {
int t[SET__SIZE];
};
struct person_set {
struct person t[SET__SIZE];
};
Cependant, cela oblige à définir chaque fonction set__* pour chacune des deux structures ci-dessus. Ce faisant, on duplique une grande quantité de code, ce qui n’est pas souhaitable.
Le pointeur polymorphe
Nous allons donc explorer une solution basée sur un type générique qui permet de stocker aussi bien des int que des float ou des struct person dans nos ensembles. Et ceci, avec une unique implémentation des fonctions set__*.
En langage C, le type void * permet de déclarer un pointeur
polymorphe (ou plutôt amorphe). Un pointeur polymorphe est compatible
avec tous les pointeurs typés: int *, float *, struct person *,
etc. Plus précisément, cela signifie que l’on peut affecter une valeur
de type int * (ou float * ou struct person *) dans une variable de type
void *. Inversement, on peut affecter une valeur de type void * dans une
variable de type int * (ou float * ou struct person *). Cela permet ainsi
de stocker une adresse en oubliant le type des valeurs pointées.
Le pointeur void * est notamment le type de retour
et le type des paramètres des fonctions d’allocation dynamique
malloc, realloc, free. L’annexe 1 présente des
exemples d’utilisation d’un tel pointeur (cf. également la norme 2011
du langage C WG14
N1570).
- On souhaite remplacer le type
intdes données parvoid *dans les réalisations deset. Donner les nouveaux prototypes des fonctionsset__add,set__remove,set__findetset_filter. - Écrire une fonction
is_evencompatible avec le nouveau prototype deset__filteret qui indique si son paramètre (supposé pointer sur unint) est pair. - Que se passe-t-il si on utilise la fonction
is_evenci-dessus pour filtrer unstruct setdont les données sont desstruct person? - Quelle contrainte sur les entiers stockés dans les
setne peut-on plus garantir suite à l’utilisation du typevoid *? Pourquoi?
Mise en œuvre
Commencer par poser un tag nommé
withstarsur votre dépôtgit. Dans la suite, nous allons modifier l’API deset. Ce tag permettra de revenir facilement à la version actuelle au besoin (correction de bugs, etc).
Changement du type des données stockées
- Modifier les structures de données pour qu’elles stockent des
valeurs de type
void *plutôt queint. - Modifier également les prototypes des fonctions conformement à
l’exercice précédent. Attention à modifier les prototypes des
fonctions outils
find,shift_left,shift_right. Mettre à jour le code afin de pouvoir compiler (NB:set__printaffiche maintenant les valeurs de l’ensemble avec%pplutôt que%d). - Commentez vos tests structurels (ne conserver qu’une fonction
main()vide). Vous vous reposerez sur les tests structurels fournis sur thor. - Mettre à jour vos tests fonctionnels de manière à pouvoir compiler votre code.
Effets de bords
Le passage du stockage d’une valeur int au stockage d’un pointeur
void* a des conséquences non négligeables. Compiler les programmes
problem1.c,
problem2.c,
problem3.c et
problem4.c avec la mise en œuvre de
set par la liste chaînée.
- Expliquer le comportement de chacun d’eux à l’aide d’un schéma de la mémoire.
- Quelle solution proposez-vous pour résoudre ces problèmes ?
- Pour quels types de données, cette solution est-elle contraignante et pourquoi?
Paramétrer le traitement des données
- Donner les prototypes des 3 fonctions permettant de paramétrer le traitement des données.
- Proposer une mise en œuvre de ces fonctions pour le type
intet une mise en œuvre pour le typestruct person
Rétablir le comportement attendu
On choisit finalement de placer ces fonctions dans le TAD lui-même plutôt que de les passer en paramètre.
- Comment paramétrer le TAD à l’aide de ces fonctions?
- Programmer la solution décrite dans les exercices précédents dans
les trois mises en œuvre de
set(voir l’annexe 2 pour les nouveaux prototypes deset__emptyetset__print) - Adapter vos tests fonctionnels et structurels et vérifier votre
implémentation. Ajouter des tests avec des données de type
struct person.
Annexe 1
Exemple d’utilisation des pointeurs void*:
struct interest {
char * firstname;
char * lastname;
};
void *p = NULL;
int number = 10;
struct interest identity = {"Finch", "Harold"};
struct interest * other;
p = &number;
p = &identity;
other = p;
Il y a des restrictions sur la manipulation de ce pointeur: pas d’arithmétique
sur les pointeurs, pas d’opérateurs d’indirection * et ->, etc.
Annexe 2
Ci-après, les nouveaux prototypes de set__empty et de set__print
pour l’implémentation avec pointeur polymorphe :
struct set * set__empty(int (*cmp)(const void*, const void*),
void * (*copy)(void const *),
void (*del)(void *));
void set__print(const struct set * se, void (*print)(void *));
Le prototype de set__empty permet d’initialiser la structure en
mettant les pointeurs de fonctions à l’intérieur. Le prototype de
set__print choisit de laisser libre la fonction d’affichage. Pour
les autres fonctions de set.h, il est demandé de ne pas leur ajouter
de paramètre fonctionnel.