7 recettes pour aller plus loin avec le préprocesseur C

(Nhat Minh Lê (rz0) @ 2010-03-15 02:12:59)

Ce soir, j’ai décidé d’être paresseux, et comme beaucoup de blogueurs, je vous sers ici un peu de précuit, un peu de réchauffé : une liste de trucs et astuces !

Mais pas n’importe laquelle ! Récemment, j’ai twitté sur le triste état des ressources disponibles sur le Web français apparaissant dans des recherches telles que « généricité C ». Il y a beaucoup trop de cours et autres tutos superficiels, destinés à inculquer aux débutants quelques bases du langage ou de la programmation. J’ai donc décidé de réagir, à mon échelle, en publiant sur mon modeste blog des articles sur le C pour les bons ! :-° Voici donc le premier : le préprocesseur C, pour les bons !

Je ne saurais être tenu responsable de l’utilisation que vous faites des techniques décrites ici. En particulier, si vous vous faites insulter pour code illisible ou quelque chose comme cela, il ne faudra pas venir vous plaindre. :]

Si en lisant ce qui suit, vous ne vous sentez pas très à l’aise, ou ne comprenez pas quelque chose, il vous manque peut-être quelques notions de base ; mais pas de panique, il vous suffit d’aller lire un cours quelconque, comme par exemple ce tuto dédié au préprocesseur, sur le SdZ.

La plupart des astuces que je présente ici sont illustrées dans des frameworks tels que COS. En moins violent, et plus usité, les en-têtes BSD sys/queue.h et sys/tree.h, dont je reparlerai probablement bientôt, sont également de bonnes illustrations de certaines techniques présentées ci-dessous. Citons aussi SGLIB, dans la même veine que sys/queue.h et sys/tree.h, mais poussant le concept un peu plus loin.

J’ai classé les astuces par ordre croissant de degré d’aliénation requis pour accepter de les utiliser. :-° Prêts ? Let’s rock!

Générez des séquences : opérateurs ,, ?:, && et ||

On vous a toujours dit que l’opérateur ,, c’était mal, que c’était Le Mal, après goto. Mais il y a un cas où celui-ci peut s’avérer être un allié… intéressant. Il s’agit du contexte des macros.

Bien souvent, vous aurez envie que votre macro se comporte le plus possible comme une fonction, c’est-à-dire que si celle-ci renvoie une valeur, vous pouvez l’utiliser dans une expression.

C’est là que l’opérateur virgule (,) entre en jeu : il vous permet de placer plusieurs expressions dans votre macro, et que le tout soit réutilisable… comme une expression ! Les opérateurs ?:, && et || ajoutent un peu de variété à votre éventail de possibilités… mais rappelez-vous qu’il faut employer avec && ou || des opérandes à valeur entière (ou en tout cas qu’il est possible de convertir en entier).

Et un exemple bidon :

#define GETARG(argcptr, argvptr) (--*(argcptr), ++*(argvptr))
#define CMP(p, q) ((p) == (q) || cmpfunc((p), (q)) == 0)

Remarquez le problème des effets de bords potentiellement provoqués par l’évaluation de p ou q, dans la seconde macro ; nous allons y revenir…

Générez des structures de contrôle : utilisez la boucle for !

Mais les macros ne sont pas limitées à remplacer des fonctions, elles peuvent également être utilisées pour créer de nouvelles structures de contrôle.

À la différence d’un appel de fonction, l’utilisation d’une structure de contrôle inclut un (ou plusieurs) blocs de code. Le schéma de base simplifié est le suivant :

BEGIN (/* ... */) {
        /* ... */;
} END (/* ... */);

Il faut bien sûr remplacer BEGIN et END par des macros appropriées.

En utilisant des boucles for pour implémenter votre structure de contrôle, vous pouvez souvent omettre la partie END, et ainsi alléger l’usage de vos macros.

Un exemple de telles constructions peut être trouvé dans sys/queue.h ; dans cet exemple, on parcourt une liste (implémentation typique : une boucle for) :

LIST_FOREACH (var, head, next) {
        /* 'var' pointe successivement sur chaque élément. */
        /* ... */;
}

Un autre exemple, de structure plus complète, munie du marqueur de fin, peut être trouvé dans les bibliothèques standards de Plan 9 ; le code suivant permet de traiter les options de la ligne de commandes :

ARGBEGIN {
case 'a':
        /* Option '-a' spécifiée. */
        /* ... */;
        break;

case 'b':
        /* ... */;
        break;

default:
        usage();
} ARGEND;

Utilisez la concaténation pour simuler la généricité par nom

La concaténation est un mécanisme puissant puisqu’elle permet de générer des identificateurs à partir de fragments, dont certains peuvent être passés en paramètres à vos macros.

Moyennant le respect de quelques conventions dans les noms, vous pouvez écrire du code générique, dont les parties spécifiques sont masquées derrière un espace de nom improvisé passé en argument.

Et encore un exemple bidon pour illustrer le principe de base :

#define RELEASE(name, p) do {                               \
        if (name##_decrref(p) == 0)                         \
                free(p);                                    \
} while (/* CONSTCOND */ 0)

Générez des définitions

Rappelez-vous l’histoire des effets de bord que nous avons rencontrée plus haut. Il n’y a pas de méthode magique, pour éviter les effets de bords, il faut passer par des variables intermédiaires.

Une solution simple et plutôt élégante pour résoudre ce problème est de générer non plus du code directement, mais des fonctions… Mais, mais, me direz-vous, quel intérêt de passer par des macros pour générer des fonctions ? Pourquoi ne pas écrire les fonctions directement ?

Et bien, cette astuce, combinée à la précédente, permet un peu de généricité ! Et un ptit exemple pas très utile, pour la route :

#define ARRAY_FIND_PROTOTYPE(name, type, cmp)                 \
type *name##_ARRAY_FIND(type *, size_t, type *)

#define ARRAY_FIND_GENERATE(name, type, cmp)                  \
type *name##_ARRAY_FIND(type *_a, size_t _n, type *_elm)      \
{                                                             \
        for (; _n > 0; ++_a, --_n) {                          \
                if (cmp(_a, _elm) == 0)                       \
                        return _a;                            \
        }                                                     \
        return NULL;                                          \
}

#define ARRAY_FIND(name, a, n, elm)                           \
        name##_ARRAY_FIND((a), (n), (elm))

Pour des exemples plus complets, je vous invite à jeter un coup d’œil à l’implémentation de sys/tree.h (ou localement, dans /usr/include/sys/tree.h si vous avez un BSD sous la main).

Côté performances, vous n’avez guère de soucis à vous faire. Si vous définissez des fonctions statiques, le compilateur s’occupera tout seul comme un grand de les machiner comme il se doit. Et si vous voulez lui forcer un peu la main, inline est là pour ça.α

α : Mais c’est du C99… je reviendrai probablement sur cette question dans un prochain article.

Simulez les alternatives avec la concaténation

Nous arrivons ainsi à la dernière astuce accessible avec un préprocesseur à la norme C89.

Vous connaissez certainement les directives #if, #ifdef, et compagnie. Hélas, elles ne peuvent être utilisées à l’intérieur d’une définition de macro.

Dans les cas simples — mais très courants — où vous souhaitez simplement discriminer entre plusieurs valeurs d’une constante passée en paramètre, cependant, vous pouvez vous en sortir en utilisant… la concaténation ! Encore elle !

En effet, il suffit pour cela de définir chaque alternative comme une macro séparée, nommée avec un préfixe commun, et un suffixe dépendant du cas. Illustration :

#define DECL_STATIC_0
#define DECL_STATIC_1 static
#define DECL_STATIC(s) DECL_STATIC_##s

#define SOME_FUNCTION_PROTOTYPE(name, type, s)              \
DECL_STATIC(s) type some_function(type);

Simulez les opérations sur les n-uplets avec les macros variadic (C99)

Avec C99 ont été introduites les macros variadic, c’est-à-dire acceptant un nombre variable d’arguments. Cet ajout, a priori mineur, est en vérité très important, car il permet la manipulation des n-uplets, soit des collections de valeurs.

Pour nous, un n-uplet sera une suite d’éléments séparés par des virgules, entre parenthèses. Par exemple :

(a, b, c)

Pour opérer sur des tuples, le truc de base à remarquer est le suivant :

Il faudrait un billet entier pour explorer en profondeur toutes les possibilités offertes par cette astuce. Je ne vais présenter ici qu’un cas d’utilisation simple, mais sachez qu’il est possible, par exemple de créer une macro qui substitue tout n-uplet par 1 et toute autre construction par 0, par exemple ! Je vous laisse imaginer ce que l’on peut en faire, combinée aux alternatives par concaténation expliquées ci-dessus.

Mais revenons à notre modeste exemple, qui illustre la fonction identité de manière ridiculement complexe :

#define IDENTITY(...) __VA_ARGS__

/*
 * 'IDENTITY' peut en fait servir à supprimer des parenthèses
 * superflues gênantes.
 */
#define CALL_WITH_RESOURCE(name, var, aargs, f, args) do {  \
        name##_type var = name##_alloc aargs;               \
        f(var, IDENTITY args);                              \
        name##_free(var);                                   \
} while (/* CONSTCOND */ 0)

#define FILE_type FILE *
#define FILE_alloc fopen
#define FILE_free fclose

/* ... */
CALL_WITH_RESOURCE (FILE, fp, ("log.txt", "a"),
    fprintf, ("%d\n", 42));

COS contient une bibliothèque entière de macros travaillant sur les n-uplets. Je vous invite à regarder le code source si cela vous intéresse ; il s’agit plus particulièrement du dossier CosBase/include/cos/cpp/, dans l’archive.

Énumérez les cas et déroulez les appels pour émuler la récursion (C99)

Au vu de ce que nous venons de voir, vous êtes en droit de vous demander : « Peut-on faire pire ? » Et la réponse est oui ! :) J’ai gardé cette astuce pour la fin car elle constitue, à mes yeux, une limite que je ne souhaite pas, à titre personnel, franchir.

Un peu plus haut, j’ai dit quelque chose d’important : dans une chaîne de substitutions de macros, une macro déjà substituée ne le sera plus, même si elle apparaît dans le texte produit… il est donc impossible de faire des macros récursives !

L’astuce ici consiste à dire : « L’univers est fini, je vais décrire tout l’univers ! » Aidée de la concaténation, les possibilités sont vraiment (in)finies ! Mais je ne m’étendrai pas davantage sur le sujet. Encore une fois, COS contient toute une panoplie d’exemples, qui, je n’en doute pas, apparaîtront brillants pour certains, et affligeants pour d’autres.

Voilà, en espérant vous avoir appris quelques petits trucs rigolos ! Have fun!