Source: http://rachid.koucha.free.fr/tech_corner/c_preprocessor_fr.html
Timestamp: 2018-01-22 19:49:38+00:00

Document:
Utilisation du preprocesseur C
Last update: 19-Mar-2011
En langage C, le « preprocessing » est l'étape préalable à la compilation. C'est un mécanisme puissant qui permet entre autres la compilation conditionnelle, l'inclusion de fichiers et la définition de macro-instructions. Bien que très simples au premier abord, ces facilités doivent être utilisées avec précautions pour éviter les erreurs de compilation ou pire, les dysfonctionnements des programmes. De plus, le préprocesseur de GCC recèle des fonctionnalités supplémentaires qui peuvent s'avérer très utiles.
1. Une défaillance peu connue du compilateur
2. Terminaison de macro
3. Regrouper les instructions
4. Parenthéser les paramètres
5. Parenthéser les expressions
6. Eviter les expressions en paramètres
7. Nombre variable d'arguments
8. Les macros spéciales
8.1. __GNUC__
8.2. __LINE__, __FILE__ et __FUNCTION__
8.3. L'opérateur « # »
8.4. L'opérateur « ## »
9. Comment éliminer les attributs de GCC
10. Inclusions conditionnelles
Cet article a été publié dans numéro 105.
Grâce au macro processeur du langage C, il est possible de définir et utiliser des constantes de la manière suivante :
Si on compile ce programme, on obtient une erreur inattendue :
Le compilateur détecte une erreur à la ligne 10, colonne 18 c'est-à-dire au niveau de la macro CONSTANTE_2. Cela est une défaillance du compilateur qui croit avoir à faire à une constante entière dénotée par « 0x3 » avec un exposant positif dénoté par « e+20 » : 0x3 multiplié par 10 élevé à la puissance 20. Ce n'est pas une notation légale en langage C car les exposants ne sont pas autorisés pour les constantes hexadécimales. Pour se sortir de cette chausse-trappe, il faut réécrire CONSTANTE_2 en séparant « e » et « + » par un caractère espace :
Ainsi la compilation et l'exécution peuvent se réaliser :
Il est donc conseillé de systématiquement mettre un espace de part et d'autre des opérateurs « + » et « – » quand on définit une macro avec le chiffre hexadécimal « e ».
Il n'est pas recommandé de terminer une macro par un « ; » comme dans l'exemple suivant :
L'instruction else se retrouve orpheline car le if auquel elle est sensée se rapporter contient plus d'une instruction qui ne sont pas regroupées par des accolades : « r = 4 + 5; » et l'instruction vide « ; » .
Quand une macro contient plusieurs instructions comme par exemple :
On peut se retrouver confronté à l'erreur de compilation évoquée au § 2 si elle est utilisée dans une instruction if car l'utilisateur de la macro peut ne pas mettre d'accolades de part et d'autre de l'appel. Ou alors la macro pouvait ne contenir à l'origine qu'une seule instruction et était logiquement utilisée dans les instructions if sans être entourée d'accolades mais une évolution du logiciel a rendu nécessaire l'ajout d'instructions supplémentaires dans celle-ci. Dans ce dernier cas, on risque d'introduire des dysfonctionnements sournois du logiciel si, par exemple, la macro est utilisée dans une instruction while sans accolades :
On s'attend à ce que toutes les instructions regroupées dans ADD() soient exécutées à chaque itérations de la boucle. Malheureusement, le préprocesseur va générer une suite d'instructions sans accolades donc à chaque itérations, seule la première instruction « printf("%d + %d ", a, b) » sera exécutée. Pour éviter ce genre d'erreurs, il est conseillé de regrouper les instructions dans un « do-while » non suivie de « ; » (règle du § 2) afin de faire apparaître la macro multi-instruction comme une seule instruction :
Le compilateur ne génèrera pas de boucle vu que la condition d'itération est toujours fausse : while(0). Il ne faut surtout pas se contenter d'accolades car on ferait apparaître le problème du § 2 si la macro était utilisée dans un « if-else » du fait du « ; » qui risque de suivre l'appel dans le corps du if. Avec la boucle non suivie de « ; », l'utilisateur est obligé de mettre un point virgule après l'appel à la macro sous peine d'erreur de compilation mais il n'aura qu'une instruction : la boucle « do-while ».
Considérons le petit programme suivant qui définit et utilise une macro effectuant une division entière :
A l'exécution on s'attend à l'affichage du résultat 2 pour la première division (12 / 6) et du résultat 3 pour la deuxième division ((12 + 6) / 6). Mais on obtient le résultat erroné suivant :
On constate que l'on se retrouve confronté à un problème de priorité des opérateurs au niveau du deuxième appel à DIV(). En effet, dans l'expression « r = v1 + 6 / v2 », l'opérateur « / » est plus prioritaire que l'opérateur « + ». Le compilateur génère donc l'équivalent de l'opération mathématique « r = v1 + ( 6 / v2 ) ». D'où le résultat 13 quand v1 et v2 valent respectivement 12 et 6. La parade consiste à systématiquement parenthéser toutes les occurrences des paramètres dans une macro :
Considérons la constante définie sous la forme de la macro CONSTANTE et utilisée comme suit :
L'affichage espéré est le résultat de l'opération « 2002 / 2 », soit 1001. Et pourtant on obtient 2001. La sortie du préprocesseur montre que le programme génère un problème de priorité des opérateurs comme on l'a vu au § 4 :
On a souvent tendance à utiliser une macro comme si c'était une fonction ou alors des évolutions de code peuvent mener au fait qu'une fonction deviennent une macro pour des raisons de lisibilité ou d'optimisation. Voici un exemple de programme qui définit et utilise la macro IS_BLANK(c) qui retourne vrai si le paramètre est un caractère blanc (espace ou tabulation) :
Ce programme est sensé afficher les indices des caractères blancs dans son nom. Pourtant l'indice affiché est erroné ou certains blancs ne sont pas détectés :
Le seul blanc détecté est indiqué à l'indice 5 alors que dans la chaîne de caractères « ./nom avec espaces », il y a des blancs aux indices 5 et 10. Ceci est du au fait que le paramètre c est évalué deux fois dans la macro IS_BLANK() : la première fois pour être comparé à '\t' et la seconde pour être comparé à ' '. Or, le programme passe *(p++) à la macro. Ce qui donne en sortie du préprocesseur :
Il n'est pas toujours évident de respecter cette règle surtout si IS_BLANK() était une fonction et après évolution du code source, la fonction est devenue une macro. En effet, une telle modification implique qu'une revue complète du code soit faite. Ce n'est pas facile voire impossible si la macro est définie dans un fichier d'entête qui est déjà utilisé dans nombre de programmes à travers le monde.
Une extension spécifique à GCC, donc pas forcément conforme à l'ANSI C, offre l'opportunité de transformer des blocs d'instructions (instructions encadrées par des accolades) en expression (cf. [1]). Comme il est par ailleurs possible de déclarer des variables locales à un bloc, la macro IS_BLANK() peut être réécrite comme suit :
La variable locale _c a été définie à l'intérieur du bloc de la macro pour stocker la valeur du paramètre. Ainsi, ce dernier n'est évalué plus qu'une seule fois : au moment de l'affectation de la variable locale.
La norme ISO C99 permet de définir des macros avec un nombre variable d'arguments. Il existe deux notations :
DEBUG() et DEBUG2() sont des surcharges de la fonction fprintf() pour afficher un message d'erreur formaté. Le paramètre fmt concaténé à des chaînes de caractères est le format passé en premier argument de la fonction d'affichage. Les notations __VA_ARGS__ ou « args » représentent tous les arguments en nombre variable avec les virgules qui les séparent. La seconde notation est souvent préférée à la première dans la mesure où elle permet de nommer les arguments (par exemple ici avec args) au lieu d'utiliser l'identifiant générique __VA_ARGS__. La seconde notation était d'ailleurs spécifique à GCC avant que les macros à nombre variable d'arguments ne soient normalisées. Sur d'anciennes versions de GCC, c'est donc la seule notation supportée.
Bien que très pratique, ces écritures présentent un inconvénient de taille : elles n'autorisent pas l'appel sans arguments. Voici le résultat de la compilation suivie de la sortie du préprocesseur pour le programme précédent :
Il existe de nombreuses macros et notations ayant un rôle particulier pour le préprocesseur. Dans ce paragraphe, seuls sont cités les plus utiles ou tout au moins les plus usitées.
La macro __GNUC__ est toujours définie lorsqu'on utilise la chaîne de compilation GCC. C'est donc conseillé pour faire de la compilation conditionnelle quand un programme utilise des extensions spécifiques à GCC mais qui est susceptible d'être compilé par d'autres chaînes de compilation (cf. § 9 pour un exemple d'utilisation).
Les macros __LINE__, __FILE__ et __FUNCTION__ sont respectivement remplacées par le numéro de ligne, le nom de fichier et le nom de la fonction dans lesquelles elles apparaîssent. Ces facilités sont en général utilisées pour la génération de traces et d'erreurs pour aider à la mise au point. Pour illustrer, voici un petit programme affichant ses arguments à l'aide de la macro DEBUG() dont la valeur ajoutée est d'imprimer le nom de fichier, le nom de la fonction et le numéro de ligne d'où elle est appelée :
On notera que __FUNCTION__ est spécifique à GCC. Le standard C est arrivé après avec la définition __func__. Bien que donnant un résultat identique, ces deux notations avait une différence de taille : la première s'expansait en une chaîne de caractères constante tandis que l'autre se comportait comme si en début de chaque fonction, un tableau local de nom « __func__ » était défini et initialisé avec la chaîne de caractères constante qui contient le nom de la fonction. Donc d'un coté, on avait une constante qui pouvait être concaténée au moment de la compilation à d'autres chaînes de caractères alors que de l'autre on avait une variable. Mais depuis la version 3.4 du compilateur GCC, ces deux notations sont identiques et se comportent toutes deux comme des variables.
La notation « # » permet de convertir un paramètre de macro en chaîne de caractères. Par exemple, voici une fonction qui affiche un numéro de signal Linux gràce à la macro CASE_SIG() :
On a déjà eu une forme d'utilisation de « ## » dans le § 7. Mais il existe une autre forme d'utilisation où cette directive concatène les éléments lexicaux qui l'entourent pour former une nouvelle unité lexicale. Dans l'exemple suivant les mots CONSTA et NTE sont concaténés pour former le nom CONSTANTE qui s'avère être une macro définissant la constante 233 :
Les attributs sont des extensions spécifiques à GCC pour augmenter les contrôles et contribuer à l'optimisation du code généré. Considérons l'exemple suivant où est décrit l'attribut « format » (pour de plus amples informations sur les attributs de GCC, cf. [4]). funct() fait appel à deux fonctions effectuant de l'affichage formaté à la manière de printf() : le premier paramètre contient une description du format d'une chaîne de caractères et la liste variable des paramètres qui suivent est utilisée pour construire la chaîne à afficher. A l'appel de ces fonctions d'affichage, il y a une erreur classique de programmation : le format nécessite l'adresse d'une chaîne de caractères et un entier signé mais la liste des aguments ne contient qu'un entier signé.
La compilation de ce programme avec l'option -Wall va signaler l'erreur de programmation (sous la forme d'un « warning ») pour la ligne 11 mais pas pour la ligne 9. En effet, en ligne 11, on utilise la fonction my_printf2() qui est définie avec l'attribut format pour indiquer que c'est une fonction « printf-like » qui utilise un format en argument 1 et que la liste variable des arguments commence au deuxième paramètre :
Si la notion d'attribut est pratique et puissante, elle peut poser problème lorsque le compilateur utilisé n'est pas GCC. Comme la directive __attribute__ est définie avec un seul paramètre (d'où le double parenthésage lorsqu'on lui passe plusieurs paramètres de sorte à ce qu'ils n'apparaîssent que sous la forme d'un seul), il est possible d'utiliser le principe de la compilation conditionnelle pour redéfinir __attribute__ à rien lorsque le compilateur utilisé n'est pas GCC :
Dans cet exemple, la condition de compilation utilise le drapeau __GNUC__ qui n'est défini par défaut que lorsque GCC est utilisé (cf. § 8.1).
Un fichier d'entête est inclus dans un fichier source à l'aide de la directive « #include ». Ces fichiers contiennent la plupart du temps des déclarations externes de variables ou fonctions, des définitions de types et de macros. Un fichier d'entête peut lui-même inclure d'autres fichiers d'entête car une règle de base en programmation C est de rendre un fichier d'entête indépendant. En d'autres termes, si un fichier d'entête utilise un type, une macro, une fonction ou une variable, il est recommandé que soit inclus dans ce fichier, le fichier d'entête où se trouve la définition correspondante. Dans l'exemple de la figure 1, le fichier main.c inclut les fichiers d'entête str.h et fct.h qui définissent respectivement le type str_t et la fonction fonc(). Ces deux derniers incluent le fichier entier.h pour la définition du type ENTIER.
Le compilateur remonte le fait que le type ENTIER est défini deux fois. La première définition provient du fichier str.h et la seconde du fichier fct.h qui incluent tout deux le fichier entier.h. Cela donne comme résultat que le fichier main.c inclut le fichier entier.h deux fois. Pour résoudre ce problème on peut utiliser la compilation conditionnelle de sorte à inclure un fichier d'entête seulement si une définition qui lui est propre n'est pas déjà définie. Cette définition est généralement faite à partir du nom du fichier. Afin d'illustrer le propos, voici comment le fichier entier.h est modifié pour être inclus qu'à la condition que ENTIER_H ne soit pas déjà défini :
Cela permet de compiler main.c car ENTIER_H est défini par l'inclusion de entier.h dans str.h et cela va donc empêcher la deuxième inclusion de entier.h via fct.h. D'une manière générale, il est conseillé d'appliquer le principe de l'inclusion conditionnelle à tout fichier d'entête. La figure 2 est une version revue et corrigée de la figure 1 à laquelle a été appliqué ce principe.
Figure 2 : Inclusions conditionnelles
Cet article a présenté de nombreuses règles et astuces pour tirer parti au mieux des facilités du préprocesseur C de sorte à contribuer à la robustesse, la portabilité et la facilité de mise au point des programmes. Ce ne sont là qu'un sous-ensemble des possibilités offertes. Le lecteur pourra consulter les liens et références de cet article pour aller plus loin.
[1] Extensions de GCC : http://sunsite.ualberta.ca/Documentation/Gnu/gcc-2.95.2/html_chapter/gcc_4.html
[2] Le préprocesseur C : http://www.game-lab.com/index.php?section=tutorials&section_id=1&p=tutorial&action=showtut&id=221
[3] Le préprocesseur C : http://www.redhat.com/docs/manuals/enterprise/RHEL-4-Manual/cpp/index.html
[4] Attributs de GCC : http://www.unixwiz.net/techtips/gnu-c-attributes.html
[5] Manuels de GCC : http://gcc.gnu.org/onlinedocs/
[6] Boulay (Nicolas), « Le C n'est pas portable », GLMF 102, Février 2008
[7] Boulay (Nicolas), « Le processus de compilation C », GLMF 103, Mars 2008
[8] Kernighan, Brian.W. & Ritchie, Dennis.M., « Le langage C », 2ème édition, Masson, 1990
L'auteur est un ingénieur en informatique travaillant en France. Il peut être contacté ici ou vous pouvez consulter son site WEB.

References: § 2
 § 2
 § 2
 § 4
 § 9
 § 7
 § 8