Tarnyko's website
Tarnyko's website
about

C11, listes variantes et le turfu

2024-05-13

Dans la foulée des excellents articles de pulkomandy sur les évolutions du langage C, j'ai décidé de m'intéresser d'abord à l'étape suivante : la norme C11.

Et cela fait déjà 12 ans, mais son adoption reste relativement récente

En tant que développeur C++, revenir aux "bases" m'a fait du bien. Pas de références universelles à transmettre à travers des couches de templates, pas d'instanciation de générique avec un type privé, pas de surcharge-surprise des opérateurs…

Je te propose donc un petit composant, pas" optimisé" mais cependant fonctionnel et totalement thread-safe :

variant_list (C11)

qui est une liste dynamique d'éléments hétérogènes, proche d'un std::vector<std::variant> en C++.
Elle supporte de base : Entiers, Booleéns, Flottants, Chaînes de caractères.

Elle utilise au moins 5 nouveautés intéressantes de C11. Les voici, avec 1-2 autres dignes d'intérêt :


1) macros _Generic

La principale innovation, un début de polymorphisme en C ! Léger, car amené à travers des macros...
Ce code :

#define list_add(L, V) _Generic((V), \
    int:    list_add_int, \
    bool:   list_add_bool, \
    double: list_add_float, \
    char*:  list_add_string)(L, V)

nous permet d'écrire :

list_add(l, 42);     // entier
list_add(l, true);   // booléen
list_add(l, 3.14);   // flottant
list_add(l, "Toto"); // chaîne

avec toujours derrière, cependant, autant de fonctions "classiques" à écrire que de types supportés.

2) <threads.h>

On a enfin une API cross-platform pour les threads ! Qui s'inspire massivement de pthreads et ramène 2 concepts liés :

Les sémaphores (mutex: mtx_t)

Qui dit "threads" dit "accès concurrent aux variables". On utilise des mutex pour les sécuriser :

#include <threads.h>

typedef struct
{
    mtx_t locked;         // C11
    size_t count;
    ...
} List;

mtx_init(&list->locked, mtx_recursive|mtx_timed);
 ...
mtx_lock(&list->locked);
list->count++;
mtx_unlock(&list->locked);

La modification de "list->count" temporisera, puis échouera, si un autre thread est en train d'accéder simultanément à la variable.

Les threads (thrd_t)

Quant aux threads eux-mêmes :

#include <threads.h>

static int my_callback(void* userdata)
{
    ...
    printf("End of thread [ID=%lu].\n", thrd_current());
    return 42;
}

int res;
thrd_t thread;

if (thrd_create(&thread, my_callback, NULL) == thrd_success)
{
    printf("Thread [ID=%lu] successfully created.", thread);
    ...
    // wait until thread callback returns
    thrd_join(thread, &res);  // res == 42
}

Tout simplement !

3) <stdatomic.h>: _Atomic

Une manière plus simple de rendre des variables thread-safe est de les déclarer "atomiques". Il s'agit en fait de variables intégrant nativement un mutex :

#include <stdatomic.h>

typedef struct
{
    _Atomic size_t count;
    ...
} List;

list->count++;

Cela ne fonctionne que sur des types élémentaires (listés ici) et est bien sûr sub-optimal si, comme moi dans la bibliothèque, on modifie de nombreuses variables du même contexte dans un thread.

4) <time.h>: struct timespec

Vous avez peut-être remarqué que, quand j'ai parlé des sémaphores, j'ai dit que la modification pouvait "temporiser". C'est parce que l'API pthreads ramène une autre structure dans son sac :

#include <time.h>

struct timespec ts;                 // C11
timespec_get(&ts, TIME_UTC);
ts.tv_sec = ts.tv_sec + 2;
                                    // bloque 2 secondes si échec initial,
mtx_timedlock(&list->locked, &ts);  // puis réussit/échoue

Cette structure timespec n'existait auparavant que sous GCC et Clang, mais le résultat est là :
On dispose enfin d'une "API temps" cross-platform ET facile à utiliser en C !

Pour faire un chronomètre actif p.ex. :

int timeout = 10;

struct timespec prev_time, curr_time = {0};
timespec_get(&prev_time, TIME_UTC);

while(true) {
    ....
    timespec_get(&curr_time, TIME_UTC);
    if (curr_time.tv_sec - prev_time.tv_sec > timeout) {
        fprintf(stderr, "Timeout of %d seconds expired!\n", timeout);
        break;
    }
}

(sous Windows avec MinGW-w64, j'ai eu des erreurs en utilisant un autre paramètre que "TIME_UTC" ; c'est probablement le cas aussi sous MSVC...
... mais pour l'usage le plus courant, c'est déjà largement suffisant !)

5) unions anonymes

C'est un "sucre syntaxique" qui répond à un usage fréquent : une union se trouve dans une autre union ou struct, et on ne s'en sert que pour fournir une alternative de stockage.

Ne pas la nommer permet d'adresser son contenu directement :

struct Value {
    union { int i; bool b; double f; char* s; }; // C11: anonyme
};

struct Value val;
val.i = 42;      // comme si "i" était directement dans "val"

Notre bibliothèque variant_list en fait bien sûr grand usage 😉.

6) <assert.h>: _Static_assert

De code d'assertion exécuté à la compilation !

#include <assert.h>

#if defined(__x86_64__) && !defined(_WIN64)
  _Static_assert(sizeof(int) == sizeof(int64_t)), "Unhandled 64-bit environment!");
#else
  _Static_assert(sizeof(int) == sizeof(int32_t)), "Works only in 32/64-bit environments!");
#endif

(C23 fournit désormais un équivalent "static_assert()" comme en C++11)


Voilàtou !
J'ai également écrit une version bibliothèque du composant sous LGPLv3.

Je pense éventuellement le faire évoluer pour rajouter les quantités d'arguments variables ("varargs") :

list_add(list, 42, true, 3.14, "Toto"); // autant d'arguments qu'on veut !

et peut-être même, qui sait, de l'optimisation 😉.