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
int
des données parvoid *
dans les réalisations deset
. Donner les nouveaux prototypes des fonctionsset__add
,set__remove
,set__find
etset_filter
. - Écrire une fonction
is_even
compatible avec le nouveau prototype deset__filter
et qui indique si son paramètre (supposé pointer sur unint
) est pair. - Que se passe-t-il si on utilise la fonction
is_even
ci-dessus pour filtrer unstruct set
dont les données sont desstruct person
? - Quelle contrainte sur les entiers stockés dans les
set
ne peut-on plus garantir suite à l’utilisation du typevoid *
? Pourquoi?
Mise en œuvre
Commencer par poser un tag nommé
withstar
sur 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__print
affiche maintenant les valeurs de l’ensemble avec%p
plutô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
int
et 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__empty
etset__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.