précédent  index  suivant

13. Le pré-processeurs


13.1 Quel est le rôle du préprocesseur ?

Le préprocesseur interprète les directives qui commencent par #. Principalement, ces directives permettent d'inclure d'autres fichiers (via #include) et de définir des macros (via #define) qui sont remplacées lors de la compilation.

Chaque directive de compilation commence par un # situé en début de ligne (mais éventuellement précédé par des espaces, des tabulations ou des commentaires) et se termine en fin de ligne.

Le préprocesseur est également responsable de la reconnaissance des trigraphes, des backslashs terminaux, et de l'ablation des commentaires.

haut de page

13.2 Qu'est-ce qu'un trigraphe ?

Dans les temps anciens, les ordinateurs n'utilisaient pas ASCII ; chaque machine avait son propre jeu de caractères. L'ISO a défini un jeu de caractères supposés présents sur toutes les machines, c'est l'Invariant Code Set ISO 646-1983. Ce jeu de caractères ne comporte pas certains caractères intéressants, tels que les accolades et le backslash. Aussi, le standard C89 a introduit les trigraphes, séquences de trois caractères commençant par ??. Il existe neuf séquences remplacées par le préprocesseur. Ce remplacement a lieu avant toute autre opération, et agit également dans les commentaires, les chaînes constantes, etc.

Les trigraphes sont, de fait, rarement utilisés. On les voit apparaître occasionnellement et par erreur, quand on écrit ça :

    printf("Kikoo ??!\n");
    

Le trigraphe est remplacé par un pipe, donc ce code affiche ceci :

    Kikoo |
    

De toutes façons, le redoublement du point d'interrogation est de mauvais goût.

Les compilateurs modernes soient ne reconnaissent plus les trigraphes, soit émettent des avertissements quand ils les rencontrent.

haut de page

13.3 À quoi sert un backslash en fin de ligne ?

Après le remplacement des trigraphes, le préprocesseur recherche tous les caractères backslash situés en fin de ligne ; chaque occurrence de ce caractère est supprimée, de même que le retour à la ligne qui le suit. Ceci permet d'unifier plusieurs lignes en une seule.

Ce comportement est pratique pour écrire des macros ou des chaînes de caractères sur plusieurs lignes :

    printf("Hello\
     World !\n");
    

Pour les chaînes de caractères, on peut aussi écrire plusieurs chaînes côte à côte, et le compilateur les unifiera (mais pas le préprocesseur : pour lui, ce seront deux chaînes à la suite l'une de l'autre).

haut de page

13.4 Quelles sont les formes possibles de commentaires ?

Un commentaire en C commence par /* et se termine par */, éventuellement plusieurs lignes plus loin. Les commentaires ne s'imbriquent pas.

On peut aussi utiliser la compilation conditionnelle, comme ceci :

    #if 0
        /* ceci est ignore */
    #endif /* 0 */
    

Dans ce cas, il faut que ce qui est ignoré soit une suite de tokens valide (le préprocesseur va quand même les regarder, afin de trouver le #endif). Ceci veut dire qu'il ne faut pas de chaîne non terminées. Ce genre de commentaire n'est pas adapté à du texte, à cause des apostrophes.

La nouvelle norme du C (C99) permet d'utiliser les commentaires du C++ : ils commencent par // et se terminent en fin de ligne. Ce type de commentaire n'est pas encore supporté partout, donc mieux vaut ne pas s'en servir si on veut faire du code portable.

haut de page

13.5 Comment utiliser #include ?

#include comporte trois formes principales :

    #include <fichier>
    #include "fichier"
    #include tokens
    

La première forme recherche le fichier indiqué dans les répertoires système ; on peut les ajuster soit via des menus (dans le cas des compilateurs avec une interface graphique), soit en ligne de commande. Sur un système Unix, le répertoire système classique est /usr/include/. Une fois le fichier trouvé, tout se passe comme si son contenu était tel quel dans le code source, là où se trouve le #include.

La deuxième forme recherche le fichier dans le répertoire courant. Si le fichier ne s'y trouve pas, il est ensuite cherché dans les répertoires systèmes, comme dans la première forme.

La troisième forme, où ce qui suit le #include ne correspond pas à une des deux formes précédentes, commence par effectuer tous les remplacements de macros dans la suite de tokens, et le résultat doit être d'une des deux formes précédentes.

Si le fichier n'est pas trouvé, c'est une erreur, et la compilation s'arrête. On notera que si on se sert d'habitude de #include pour inclure des fichiers d'en-tête (tels que stdio.h), ce n'est pas une obligation.

haut de page

13.6 Comment éviter l'inclusion multiple d'un fichier ?

Il arrive assez souvent qu'un fichier inclus en incluse un autre, qui lui-même en inclut un autre, etc. On peut arriver à des boucles, qui peuvent conduire à des redoublements de déclarations (donc des erreurs, pour typedef par exemple), voire des boucles infinies (le compilateur finissant par planter).

Pour cela, le moyen le plus simple est de « protéger » chaque fichier par une construction de ce genre :

    #ifndef FOO_H_
    #define FOO_H_
    /* ici, contenu du fichier */
    #endif /* FOO_H_ */
    

Ainsi, même si le fichier est inclus plusieurs fois, son contenu ne sera actif qu'une fois. Certains préprocesseurs iront même jusqu'à reconnaître ces structures, et ne pas lire le fichier si la macro de protection (ici FOO_H_) est encore définie.

Il y a eut divers autres moyens proposés, tels que #import ou #pragma once, mais ils ne sont pas standards, et encore moins répandus.

haut de page

13.7 Comment définir une macro ?

On utilise #define. La forme la plus simple est la suivante :

    #define FOO  3 + 5
    

Après cette déclaration, toute occurrence de l'identificateur FOO est remplacée par son contenu (ici 3 + 5). Ce remplacement est syntaxique et n'a pas lieu dans les chaînes de caractères, ou dans les commentaires (qui n'existent déjà plus, de toutes façons, à cette étape).

On peut définir un contenu vide. La macro sera remplacée, en cas d'utilisation, par rien. Le contenu de la macro est une suite de tokens du C, qui n'a pas à vouloir dire quelque chose. On peut définir ceci, c'est valide :

    #define FOO  (({/coucou+}[\}+ "zap" 123
    

La tradition est d'utiliser les identificateurs en majuscules pour les macros ; rien n'oblige cependant à appliquer cet usage. Les règles pour les identificateurs de macros sont les mêmes que pour celles du langage.

haut de page

13.8 Comment définir une macro avec des arguments ?

On fait ainsi :

    #define FOO(x, y)   ((x) + (x) * (y))
    

Ceci définit une macro qui attend deux arguments ; notez qu'il n'y a pas d'espace entre le nom de la macro (FOO) et la parenthèse ouvrante. Toute invocation de la macro par la suite est remplacée par son contenu, les arguments l'étant aussi. Ainsi, ceci :

    FOO(bla, "coucou")
    

devient ceci :

    ((bla) + (bla) * ("coucou"))
    

(ce qui ne veut pas dire grand'chose en C, mais le préprocesseur n'en a cure). Le premier argument est remplacé deux fois, donc, s'il a des effets de bord (appel d'une fonction, par exemple), ces effets seront présents deux fois.

Si la macro est invoquée sans arguments, elle n'est pas remplacée. Cela permet de définir une macro sensée remplacer une fonction, mais en conservant la possibilité d'obtenir un pointeur sur la fonction. Ainsi :

    int min(int x, int y) { return x < y ? x : y; }
    #define min(x, y)   ((x) < (y) ? (x) : (y))
    min(3, 4);    /* invocation de la macro */
    (min)(3, 4);  /* invocation de la fonction, via le pointeur */
    

C'est une erreur d'invoquer une macro à argument avec un nombre incorrect d'arguments. En C99, on peut utiliser des arguments vides ; en C89, c'est flou et mieux vaut éviter.

haut de page

13.9 Comment faire une macro avec un nombre variable d'arguments ?

Ce n'est possible qu'en C99. On utilise la construction suivante :

    #define error(l, ...)    { \
            fprintf(stderr, "line %d: ", l); \
            fprintf(stderr, __VA_ARGS__); \
        }
    

Ceci définit une macro, qui attend au moins un argument ; tous les arguments supplémentaires sont concaténés, avec leurs virgules de séparation, et on peut les obtenir en utilisant __VA_ARGS__. Ainsi, ceci :

    error(5, "boo: '%s'\n", bla)
    

sera remplacé par ceci :

    { fprintf(stderr, "line %d: ", 5); \
      fprintf(stderr, "boo: '%s'\n", bla); }
    

Ce mécanisme est supporté par les dernières versions de la plupart des compilateurs C activement développés ; mieux vaut l'éviter si le code doit aussi fonctionner avec des compilateurs un peu plus anciens.

Il existe aussi des extentions sur certains compilateurs. Par exemple, sous GCC, le code suivant est équivalent à l'exemple précédent :

    #define error(l, format...)    { \
            fprintf(stderr, "line %d: ", l); \
            fprintf(stderr, format); \
        }
    

haut de page

13.10 Que font les opérateurs # et ## ?

L'opérateur # permet de transformer un argument d'une macro en une chaîne de caractères. On fait ainsi :

    #define BLA(x)    printf("l'expression '%s' retourne %d\n", #x, x);
    BLA(5 * x + y);
    

ce qui donne le résultat suivant :

    printf("l'expression '%s' retourne %d\n", "5 * x + y", 5 * x + y);
    

Les éventuelles chaînes de caractères et backslashs dans l'argument sont protégés par des backslashes, afin de constituer une chaîne valide.

L'opérateur ## effectue la concaténation de deux tokens. Si le résultat n'est pas un token valide, alors c'est une erreur ; mais certains préprocesseurs sont peu stricts et se contentent de re-séparer les tokens. On l'utilise ainsi :

    #define FOO(x, y)   x ## y
    FOO(bar, qux)();
    

qui donne ceci :

    barqux();
    

haut de page

13.11 Une macro peut-elle invoquer d'autres macros ?

Oui. Mais il est prévu un mécanisme qui empêche les boucles infinies.

Tout d'abord, les invocations de macros ne sont constatées que lors de l'utilisation de la macro, pas lors de sa définition. Si on fait ceci :

    #define FOO    BAR
    #define BAR    100
    

alors on obtient bien 100, pas BAR.

Si la macro possède des arguments, chaque fois que cet argument est utilisé (sans être précédé d'un # ou précédé ou suivi d'un ##), il est d'abord examiné par le préprocesseur, qui, s'il y reconnaît une macro, la remplace. Une fois les arguments traités, le préprocesseur les implante à leur place dans la suite de tokens générés par la macro, et gère les opérateurs # et ##.

À la suite de cette opération, le résultat est de nouveau examiné pour rechercher d'autres remplacements de macros ; mais si une macro est trouvée, alors qu'on est dans le remplacement de ladite, cette macro n'est pas remplacée. Ceci évite les boucles infinies.

Je sais, c'est compliqué. Quelques exemples :

    #define FOO    coucou BAR
    #define BAR    zoinx FOO
    FOO
    

FOO est remplacée par coucou BAR, et le BAR résultant est remplacé par zoinx FOO. Ce FOO n'est pas remplacé, parce qu'on est dans le remplacement de FOO. Donc, on obtient coucou zoinx FOO.

Un autre exemple, plus tordu :

    #define FOO(x)   x(5)
    FOO(FOO);
    

La macro FOO est invoquée ; elle attend un argument, qui est FOO. Cet argument est d'abord examiné ; il y a FOO dedans, mais non suivi d'une parenthèse ouvrante (l'argument est examiné tout seul, indépendamment de ce qui le suit lors de son usage), donc le remplacement n'a pas lieu. Ensuite, l'argument est mis en place, et on obtient FOO(5). Ce résultat est réexaminé ; cette fois, FOO est bien invoquée avec un argument, mais on est dans le deuxième remplacement, à l'intérieur de la macro FOO, donc on ne remplace pas. Le résultat est donc : FOO(5);

Si vous voulez utiliser ce mécanisme, allez lire une douzaine de fois la documentation du GNU cpp, et surtout le paragraphe 12 de l'annexe A du Kernighan & Ritchie (2ème édition).

haut de page

13.12 Comment redéfinir une macro ?

On peut redéfinir à l'identique une macro ; ceci est prévu pour les fichiers d'en-tête inclus plusieurs fois. Mais il est en général plus sain de protéger ses fichiers contre l'inclusion multiple (cf. 13.6).

Redéfinir une macro avec un contenu ou des arguments différents, est une erreur. Certains préprocesseurs laxistes se contentent d'un avertissement. La bonne façon est d'abord d'indéfinir la macro via un #undef. Indéfinir une macro qui n'existe déjà pas, n'est pas une erreur.

haut de page

13.13 Que peut-on faire avec #if ?

#if permet la compilation conditionnelle. L'expression qui suit le #if est évaluée à la compilation, et, suivant son résultat, le code qui suit le #if jusqu'au prochain #endif, #elif ou #else est évalué, ou pas. Quand le code n'est pas évalué, les directives de préprocesseur ne le sont pas non plus ; mais les #if et similaires sont néanmoins comptés, afin de trouver la fin de la zone non compilée.

Lorsque le préprocesseur rencontre un #if, il :

L'expression ne doit comporter que des constantes entières (donc, éventuellement, des constantes caractères), qui sont promues au type (unsigned) long (en C89) ou (u)intmax_t (en C99). Les flottants, les pointeurs, l'accès à un tableau, et surtout l'opérateur sizeof ne sont pas utilisables par le préprocesseur.

Il n'est pas possible de faire agir un #if suivant sizeof(long), pour reprendre un desiderata fréquent. Par ailleurs, les constantes de type caractère n'ont pas forcément la même valeur pour le préprocesseur et pour le compilateur.

haut de page

13.14 Qu'est-ce qu'un #pragma ?

C'est une indication pour le compilateur. Le préprocesseur envoie cette directive sans la modifier. Le standard C89 ne prévoit aucune directive standard, mais le préprocesseur comme le compilateur sont sensés ignorer les directives inconnues.

Le C99 définit trois #pragma qui permettent d'ajuster le comportement du compilateur, quant au traitement des nombres flottants et complexes.

haut de page

13.15 Qu'est-ce qu'un #assert ?

C'est une extension gérée par GNU et (au moins) le compilateur Sun (Workshop Compiler, pour Solaris). C'est une sorte d'alternative à #ifdef, avec une syntaxe plus agréable. C'est à éviter, car non standard.

haut de page

13.16 Comment définir proprement une macro qui comporte plusieurs statements ?

On peut ouvrir un bloc, car tout statement est remplaçable, en C, par un bloc, mais cela pose des problèmes avec le ; terminal du statement. La manière recommandée est la suivante :

    #define foo(x)   do { f(x); printf("coucou\n"); } while (0)
    

On peut ainsi l'utiliser comme ceci :

    if (bar) foo(1); else foo(2);
    

Si on avait défini foo sans le do et le while (0), le code ci-dessus aurait provoqué une erreur de compilation, car le else serait séparé du if par deux statements : le bloc et le statement vide, terminé par le point-virgule.

haut de page

13.17 Comment éviter les effets de bord ?

Les macros peuvent être dangereuses si l'on ne fait pas attention aux effets de bord. Par exemple si l'on a le code suivant :

    #define MAX   3 + 5

    int i = MAX;
    

La variable i vaudra bien 8, mais si on utilise la macro MAX ainsi :

    int i = MAX * 2;
    

La variable i ne vaudra pas 16, mais 13. Pour éviter ce genre de comportement, il faut écrire la macro ainsi :

    #define MAX   (3 + 5)
    

Dans certains cas, une macro représente une expression C complète. Il est alors plus cohérent de placer des parenthèses vides pour simuler une fonction. Et dans ce cas il ne faut pas la terminer par un point virgule, et

    #define PRTDEBUG()   (void)printf("Coucou\n")
    

ainsi, on pourra utiliser la macro par :

    if (i == 10) {
       PRTDEBUG();
    }
    else {
       i++;
    }
    

Quand une macro a des arguments il faut faire attention à la façon de les utiliser. Ainsi la macro :

    #define CALCUL(x, y)   (x + y * 2)
    

a des effets de bord suivant la façon de l'utiliser :

    int i = CALCUL(3, 5);
    

donnera bien un résultat de 13, alors que le même résultat serait attendu avec :

    int i = CALCUL(3, 2 + 3);
    

qui donne 11. Pour éviter cela, il suffit de placer des parenthèses sur les arguments de la macro:

    #define CALCUL(x, y)   ((x) + (y) * 2)
    

Un effet de bord qui ne peut être contourné survient quand la macro utilise plusieurs fois une variable :

    #define MAX(x, y)   ((x) > (y) ? (x) : (y))
    

Si on utilise la macro comme cela :

    i = MAX(j, k);
    

On obtiendra un résultat correct, alors qu'avec :

    i = MAX(j++, k++);
    

une des variables j ou k sera incrémentée 2 fois. Pour éviter ce genre de comportement, il faut remplacer la macro par une fonction, de préférence inline (C99) :

    inline int max(int x, int y)
    {
        return x > y ? x : y;
    }
    

En règle générale, quand on utilise une fonction avec un nom en majuscule (comme MAX), on s'attend à ce que ce soit en fait une macro, avec les effets de bord qui en découlent. Alors que si le nom est en minuscule, il s'agit sûrement d'une véritable fonction. Cette règle n'est hélas pas générale et donc il convient de vérifier le véritable type d'une fonction si l'on ne veut pas être surpris lors de son utilisation.

haut de page

13.18 Le préprocesseur est-il vraiment utile ?

La réponse est oui. Il existe un mouvement qui voudrait faire disparaître le préprocesseur, en remplaçant les #define par des variables constantes, et avec un quelconque artifice syntaxique pour importer les déclarations d'un fichier d'entête.

Il s'avère qu'on a vraiment besoin d'un mécanisme polymorphe (comme une fonction qui accepterait plusieurs types différents), et que seules les macros apportent ce mécanisme en C. Les anti-préprocesseurs acharnés parlent d'adopter le mécanisme des templates du C++, mais ça ne risque pas d'arriver de sitôt.

Dans la vie de tous les jours, l'utilisation du préprocesseur, avec quelques #include et des #define sans surprise, ne pose pas de problème particulier, ni de maintenance, ni de portabilité.

haut de page

13.19 Approfondir le sujet.

Outre le Kernighan & Ritchie, qui comporte quelques pages très bien faites sur le sujet, on peut lire la documentation au format info du GNU cpp ; elle est assez partiale dans certains cas (condamnation explicite des trigraphes, par exemple) mais assez riche en enseignements.

Pour le reste, chaque compilateur C vient avec un préprocesseur, et il existe quelques préprocesseurs indépendants (un écrit par Dennis Ritchie en personne, qu'on voit inclus dans lcc, et aussi ucpp, une oeuvre à moi : www.di.ens.fr/~pornin/ucpp/ ).

haut de page

précédent  index  suivant

faq-fclc 5/3/2002 (8h 59:05)