clavier ouvert

Blog d'enseignement d'Adrien Foucart.
Toutes les opinions présentées ici n'engagent que moi. Blog garanti sans pub, sans traqueurs, et 100% rédigé par un humain.

🟨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 !

Commentaires, remarques, erreurs qu'il faut absolument me faire remarquer? Contactez-moi sur Mastodon ou par mail (adrien@adfoucart.be)