Préprocesseur C

Le préprocesseur C ou cpp assure une phase préliminaire de la traduction (compilation) des programmes informatiques écrits dans les langages de programmation C et C++. Comme préprocesseur, il permet principalement l'inclusion d'un segment de code source disponible dans un autre fichier (fichiers d'en-tête ou header), la substitution de chaînes de caractères (macro définition), ainsi que la compilation conditionnelle.

Dans de nombreux cas, il s'agit d'un programme distinct du compilateur lui-même et appelé par celui-ci au début de la traduction. Le langage utilisé pour les directives du préprocesseur est indépendant de la syntaxe du langage C, de sorte que le préprocesseur C peut être utilisé isolément pour traiter d'autres types de fichiers sources.

Phases

Le préprocesseur s'occupe des quatre premières (sur huit) phases de traduction pour la norme C :

  1. Remplacement de tri-graphes : le préprocesseur remplace les séquences de tri-graphes par les caractères qu'ils représentent ;
  2. Raccordement de ligne : les lignes physiquement séparées mais qui sont connectées par des séquences de saut de ligne sont reliées pour former des lignes logiques ;
  3. Tokenisation : le préprocesseur découpe le code résultant en jetons (ou token) de pré-traitement et d'espaces insécables. Il retire les commentaires ;
  4. Expansion de macros et manipulation directive : les lignes contenant des directives de pré-traitement, dont l'inclusion de fichiers ou la compilation conditionnelle, sont exécutées. Le préprocesseur remplace chaque appel à une macro par sa définition. Dans la version 1999 de la norme C, il traite aussi les opérateurs _Pragma.

Types de directives

Inclusion

L'usage le plus fréquent du préprocesseur C est la directive:

 #include <…>

dont le rôle est de recopier le contenu d'un fichier dans le fichier courant. On l'emploie généralement pour inclure les en-têtes de bibliothèques, telles que les fonctions mathématiques (#include <math.h>) ou les fonctions d'entrée/sortie standard (#include <stdio.h>).

Dans l'exemple ci-dessous, la directive « #include <stdio.h> » est remplacée par le texte contenu dans le fichier stdio.h.

#include <stdio.h>

int main(void)
{
    printf("Bonjour le monde!\n");
    return 0;
}

Cette directive peut aussi être écrite en utilisant les guillemets doubles (" ") comme #include "stdio.h". Si le nom du fichier est enfermé à l'intérieur des chevrons ( < > ), le fichier est recherché dans les chemins connus d'un compilateur standard (aussi appelé include paths). Si le nom du fichier est encadré par des guillemets doubles, le répertoire courant est ajouté avant l'ensemble des chemins de recherche de fichiers; la recherche débute donc par le répertoire courant avant de se poursuivre dans ceux de chemins de recherche. Les compilateurs C et les environnements de développement permettent généralement au programmeur d'inclure les répertoires où les fichiers peuvent être trouvés.

Par convention, les fichiers à inclure ont une extension ".h" (ou .hpp en C++), et les fichiers ne devant être inclus ont une extension ".c" (ou ".cpp" en C++). Toutefois, il n'est pas nécessaire que cela soit observé. Occasionnellement, des fichiers avec d'autres extensions sont inclus: les fichiers avec une extension de .def peuvent désigner des fichiers destinés à être inclus plusieurs fois, à chaque fois élargissant le contenu à inclure, aussi #include "icon.xbm" fait référence à un fichier image XBM (qui est en même temps un fichier C).

La directive #include souvent est accompagnée de directives dites de #include guards ou de la directive #pragma pour éviter la double inclusion.

La compilation conditionnelle

Les directives #if, #ifdef, #ifndef, #else, #elif and #endif peuvent être utilisées pour la compilation conditionnelle.

#if VERBOSE >= 2
  printf("Hello world!");
#endif

La plupart des compilateurs pour Microsoft Windows définissent implicitement _WIN32. Cela permet au code, y compris les commandes de préprocesseur, de compiler uniquement lorsque l'on cible les systèmes d'exploitation Windows. Quelques compilateurs définissent WIN32 à la place. Pour les compilateurs qui ne définissent pas implicitement la macro _WIN32, il est possible de le spécifier sur la ligne de commande du compilateur, en utilisant -D_WIN32.

#ifdef __unix__ // __unix__ est souvent défini par les compilateurs pour des systèmes Unix 
# include <unistd.h>
#elif defined _WIN32 // _Win32 est souvent défini par les compilateurs pour des systèmes Windows 32 ou 64 bit 
# include <windows.h>
#endif

Cet exemple de code teste si la macro __unix__ est définie. Si c'est le cas, le fichier <unistd.h> est inclus. Sinon, il teste si la macro _WIN32 est définie, auquel cas le fichier <windows.h> est inclus.

Il est possible d'utiliser des opérateurs avec la directive #if, comme :

#if !(defined __LP64__ || defined __LLP64__) || defined _WIN32 && !defined _WIN64
	// compilation pour un système 32-bit
#else
	// compilation pour un système 64-bit
#endif

La majorité des langages de programmation modernes n'utilisent pas ces fonctionnalités et dépendent plutôt d'une utilisation des habituels opérateurs if…then…else…, laissant au compilateur la tâche de supprimer le code inutile.

Définition de macro et expansion

Le mécanisme des macros est fréquemment utilisé en C pour définir de petits extraits de code qui seront réutilisés à divers endroits du programme. Durant l'exécution du préprocesseur, chaque appel de la macro est remplacé, dans le corps du fichier, par la définition de cette macro. La macro aboutirait donc à une exécution plus rapide qu'un appel à une routine, mais il faut souvent y regarder à deux fois, comme on va le voir.

Les syntaxes générales pour déclarer une macro de remplacement de type objet et fonction, respectivement, sont :

#define <identifiant> <contenu de remplacement> // type objet
#define <identifiant>(<liste des paramètres>) <contenu de remplacement> // type fonction avec paramètres

Les macros de type fonction ne doivent pas contenir d'espace entre l'identifiant et la parenthèse ouvrante.

Un exemple de macro de type objet est :

#define PI 3.14159

qui définit une macro PI (identifiant) qui prendra la valeur 3.14159 (contenu de remplacement). Cette macro peut être appelée comme n'importe quelle fonction C. Ainsi, après passage du préprocesseur, double z =PI; sera remplacé par double z = 3.14159;

Un exemple de macro de type fonction :

 #define MAX(a,b) a>b?a:b

définit la macro MAX (identifiant), prenant 2 paramètres (a et b) et calculant a>b?a:b (contenu de remplacement). Cette macro peut être appelée comme n'importe quelle fonction C. Ainsi, après passage du préprocesseur, z = MAX(x, y); sera remplacé par z = x>y?x:y;

Cet usage des macros est fondamental en C, notamment pour définir des structures de données sûres ou en tant qu'outil de débogage ; cependant il peut ralentir la compilation et parfois l'exécution, et présente de nombreux pièges. Ainsi, si f et g sont deux fonctions, z = MAX(f(), g()); sera remplacée par z = (f() > g())?f():g(); : cette commande nécessite deux fois l'évaluation d'une des deux fonctions, ce qui ralentit le code.

À noter aussi, qu'il est possible d'annuler la définition d'une macro avec la directive "#undef" :

#undef <identifiant>

Concatenation de tokens

L’opérateur ## élimine tous les espaces (ou whitespace) autour de lui et concatène (assemble) deux tokens (ou jetons) en un. Cela sert à créer de nouveaux tokens et ne peut être utilisé que dans des macros, comme :

#define ma_macro(x) x ## _suffixe

remplacera

ma_macro(identifiant);

par

identifiant_suffixe;

Erreurs de compilation définies par l'utilisateur

La directive #error ajoute un message au flux des messages d'erreur.

#error "message d'erreur"

Il est ainsi possible d'utiliser ces directives, par exemple pour créer une erreur de traduction :

#if RUBY_VERSION == 190
#error 1.9.0 not supported
#endif

Implémentations

Caractéristiques des préprocesseurs specifiques à un compilateur

La directive #pragma

La directive #pragma est une directive spécifique à un compilateur. Cette directive est souvent utilisée pour permettre la suppression de certains messages d'erreur ou pour gérer la pile du processeur ou le tas du ramasse-miettes.

La version 1999 de la norme C (C99) introduit une série de directives #pragma, telles que #pragma STDC pour contrôler la précision numérique ou fma() pour l'opération combinée Multiplieur-accumulateur.

Messages d'alerte

De nombreuses implémentations (dont les compilateurs C pour GNU, intel, Microsoft et IBM) fournissent une directive non standard pour afficher des messages d'alerte dans la sortie sans toutefois arrêter la compilation. Un usage fréquent de ce type de directives est de prévenir qu'une partie du code est « dépréciée » (erreur de traduction très répandue pour deprecated qui signifie 'obsolète') :

  • Pour GNU, Intel et IBM :
#warning "Ne pas utiliser SOPA ou PIPA, qui sont dépréciées. Utilisez CC à la place."
  • Microsoft :
#pragma message("Ne pas utiliser SOPA ou PIPA, qui sont dépréciées. Utilisez CC a la place.")

Autres directives spécifiques

  • Le préprocesseur de GCC autorise la directive #include_next. Cette directive indique au préprocesseur de continuer à chercher le nom de fichier spécifié et de l'inclure après le répertoire courant. La syntaxe est similaire à #include.
  • Les préprocesseurs de Objective-C comprennent la directive #import, qui est similaire à #include mais n'inclut le fichier qu'une seule fois.

Bibliographie

Glossaire

Le terme directive renvoie aux lignes du code source devant être traitées par le préprocesseur (p. ex. #define ou #include). Par extension, il sert à désigner l'instruction ou macro-définition destinée au préprocesseur.

Outils

L'outil GNU Cppi vise à la bonne indentation des directives du préprocesseur pour refléter leur imbrication, et veille à ce qu'il y ait exactement un espace entre chaque directive #if, #elif, #define et le jeton suivant, pour in fine écrire le résultat en sortie. Le nombre d'espaces entre le caractère `#' et la directive suivante doit correspondre au niveau d'imbrication de cette directive (voir aussi le livre cppi sur Wikibooks). C'est un paquet GNU maintenu par Jim Meyering et distribué selon les termes de la licence publique générale GNU[1].

Notes et références

  1. (en) Jim Meyering, cppi-1.18 released, info-gnu, (lire en ligne).