🟨2026.04.15
C
Orienté Objet | 4
Pour faire une classe “générique” en C, on a besoin de macros. Je vais largement me baser sur l’article de Ian Fisher, Type-safe generic data structures in C, qui explique assez bien l’idée de base, sur laquelle on va pouvoir construire notre propre solution.
Cas de base
Prenons un programme très simple avec notre structure-bidon
Object:
#include <stdio.h>
#include <stdlib.h>
typedef struct Object {
int value;
} Object;
int main(){
Object* o = malloc(sizeof(Object));
o->value = 10;
printf("%d\n", o->value);
free(o);
}Pour rendre Object générique, on voudrait que
les int puissent être remplacés par ce qu’on veut.
On va créer une macro sur base de notre
typedef:
#define GEN_OBJECT(type, name)\
typedef struct {\
type value;\
} name;C’est assez dégueulasse à lire, avec les lignes qui doivent
se terminer par \, mais globalement, les
type et name qu’on a mis en paramètre
de notre macro GEN_OBJECT vont être remplacés
partout par les valeurs qu’on veut, à la compilation. Ce qui
veut dire que je peux générer à la volée des structures avec les
types que je veux.
Par exemple:
#include <stdio.h>
#include <stdlib.h>
#define GEN_OBJECT(type, name)\
typedef struct {\
type value;\
} name;
GEN_OBJECT(int, IntObject);
GEN_OBJECT(char, CharObject);
int main(){
IntObject* o = malloc(sizeof(IntObject));
o->value = 10;
printf("%d\n", o->value);
CharObject* o2 = malloc(sizeof(CharObject));
o2->value = 'd';
printf("%c\n", o2->value);
free(o);
free(o2);
}Les:
GEN_OBJECT(int, IntObject);
GEN_OBJECT(char, CharObject);Vont être remplacés par:
typedef struct IntObject {
int value;
} IntObject;Et:
typedef struct CharObject {
char value;
} CharObject;Dans le reste du programme, on peut donc faire “comme si” on les avait définit. Pratique.
Petite subtilité qui rend la lecture du code encore plus
immonde: si on veut faire une fonction qui inclut le nom de
notre “variable de macro”, on doit utiliser ## pour
le “coller” avec le reste du nom. Par exemple:
#define GEN_OBJECT(type, name)\
typedef struct {\
type value;\
} name;\
\
name* name##_make(type value){\
name* o = malloc(sizeof(name));\
o->value = value;\
return o;\
}Me permettrait de faire ensuite:
IntObject* o = IntObject_make(10);
printf("%d\n", o->value);
CharObject* o2 = CharObject_make('d');
printf("%c\n", o2->value);Retour à l’ensemble
On doit maintenant récupérer tout ce qu’on avait dans notre
SetObject de la dernière fois, et transformer ce
Object en un paramètre. Je vais créer deux macros:
une pour la déclaration de l’interface publique, et une pour
l’implémentation. Ainsi, je peux garder le fait de cacher
l’implémentation aux utilisateurs de la classe.
Je met la déclaration en entier:
#define DECL_SET(Object)\
typedef struct Set##Object Set##Object;\
Set##Object* Set##Object##_make();\
void Set##Object##_destroy(Set##Object* set);\
bool Set##Object##_add(Set##Object* set, Object* o);\
bool Set##Object##_remove(Set##Object* set, Object* o);\
bool Set##Object##_contains(Set##Object* set, Object* o);\
int Set##Object##_size(Set##Object* set);Et quelques morceaux choisis de l’implémentation. Tout se
trouve dans un seul fichier, genericSet.h:
#define IMPL_SET(Object)\
typedef struct Node##Object Node##Object;\
struct Node##Object {\
Object* obj;\
Node##Object* next;\
};\
\ //...
struct Set##Object {\
LinkedList##Object** table;\
int capacity;\
int size;\
};\
\ // ...
Set##Object* Set##Object##_make() {\
Set##Object* set = malloc(sizeof(Set##Object));\
set->capacity = 16;\
set->size = 0;\
set->table = malloc(sizeof(LinkedList##Object*)*set->capacity);\
for (int i=0; i < set->capacity; i++){\
set->table[i] = LinkedList##Object##_make();\
}\
\
return set;\
}\
\ // ...
bool LinkedList##Object##_contains(LinkedList##Object* ll, Object* o){\
if (ll->size == 0){\
return false;\
} else {\
Node##Object* node = ll->first;\
while (node != NULL){\
if (Object##_equals(node->obj, o)){\
return true;\
}\
node = node->next;\
}\
return false;\
}\
}\
// ...Toute cette complexité n’est pas en vain, cependant, car elle
me permet maintenant dans set.h de faire:
#include <stdbool.h>
#include "genericSet.h"
typedef struct Object Object;
Object* Object_make(int value);
void Object_destroy(Object* o);
int Object_hash(Object* o);
int Object_equals(Object* o, Object* other);
DECL_SET(Object)Et dans set.c de faire:
#include <stdbool.h>
#include <stdlib.h>
#include <stdio.h>
#include "set.h"
// for testing
struct Object {
int value;
};
Object* Object_make(int value){
Object* o = malloc(sizeof(Object));
o->value = value;
return o;
}
void Object_destroy(Object* o){ free(o); }
int Object_hash(Object* o){ return o->value; }
int Object_equals(Object* o, Object* other){ return o->value == other->value; }
IMPL_SET(Object)Pour que tous mes tests précédents fonctionnent. Dans mon
fichier de test, je peux inclure set.h et avoir
accès à toutes les méthodes de SetObject.
Un set de personnes?
Voyons si on peut grâce à tout ce travail se faire, enfin, un
ensemble de Person. J’aurai besoin de créer une
méthode Person_hash pour que ça fonctionne. Vu que
Person est composé de chaînes de caractères, je
vais utiliser une méthode de hachage appropriée. J’ai trouvé cet
article qui traduit un article d’un site russe que mon
navigateur me déconseille d’ouvrir, donc ça doit être de la
qualité. On nous dit que, pour hacher une chaîne de caractères,
on doit faire:
s[0] + s[1]*p + s[2]*(p^2) + ... + s[n-1]*(p^{n-1}) % m
Avec une valeur de p conseillée de
31. OK, essayons. Modulo une valeur arbitraire.
L’article propose un “nombre premier large”, comme
10^9 + 9. Why not.
On a:
// person.c
#define HASH_P 31
#define HASH_MOD pow(10, 9)+9
// ...
int Person_hash(Person* p) {
int h = 0;
for (int i = 0; i < strlen(p->first_name); i++){
h += p->first_name[i]*pow(HASH_P, i);
}
for (int i = 0; i < strlen(p->last_name); i++){
h += (p->first_name[i]+strlen(p->first_name))*pow(HASH_P, i);
}
return h % HASH_MOD;
}Rajoutons dans notre fichier de test un test très simple:
bool test_person_hash(){
Person* p = Person_make("abcd", "efgh");
Person* p2 = Person_make("efgh", "abcd");
Person* p3 = Person_make("abcd", "efgh");
bool result = !(Person_hash(p)==Person_hash(p2)) &&
(Person_hash(p)==Person_hash(p3));
}Je m’attends à des erreurs de typage, je ne me suis pas trop préoccupé des castings dans toutes mes opérations. J’en ai une, sur le modulo.
// ...
return h % ((int) HASH_MOD);Le test passe. Je ne suis pas sûr que tout est en ordre, mais ce sera suffisant pour le moment.
L’important, c’est que je devrais pouvoir créer un
personSet.h avec:
#include <stdlib.h>
#include "set.h"
#include "person.h"
DECL_SET(Person)Et un fichier personSet.c avec:
#include "personsSet.h"
IMPL_SET(Person);Et rajouter un test:
#include "personsSet.h"
// ...
bool test_person_set(){
SetPerson* set = SetPerson_make();
Person* p = Person_make("abcd", "efgh");
Person* p2 = Person_make("efgh", "abcd");
SetPerson_add(set, p);
bool result = SetPerson_contains(set, p) && !SetPerson_contains(set, p2);
SetPerson_destroy(set);
Person_destroy(p);
Person_destroy(p2);
}Et, mis à part le fait que je me suis un peu emmêlé les
pinceaux dans mes conventions de nommage et qu’il va falloir que
je change un peu tout ça entre mes SetPerson et mes
personSet… ça fonctionne!
Je suis assez impressionné, je dois dire.
Renommons donc personSet.h et
personSet.c en setPerson, et on arrive
à quelque chose de tout à fait fonctionnel. Je m’attendais à
beaucoup plus devoir me casser la tête. Tant mieux !
Conclusion
Je pense que je vais m’arrêter là, pour le moment en tout cas, dans cette exploration du C orienté objet. On pourrait évidemment aller beaucoup plus loin. On n’a pas ici d’héritage, pas de polymorphisme, pas de pleins de choses que les langages orientés objets aiment faire. Mais on pourrait y arriver. Après tout, Python permet de faire de l’orienté objet, et le langage est écrit en C. Il faut bien que ces fonctionnalités viennent de quelque part !