À partir d'un code source, la traduction en code exécutable nécessite de nombreux traitements. Les normes des langages C et C++ distinguent respectivement huit et neuf phases C C++. Dans la pratique – et c'est une particularité remarquable de ces deux langages – les six premières phases de la traduction sont confiée à un composant logiciel spécifique, mais néanmoins intégré à la chaîne de compilation, appelé préprocesseur W. On peut alors parler de prétraitements – ou précompilation – du code source, en anglais, preprocessing.

Ce chapitre est consacré à l'étude du préprocesseur du langage C, usuellement noté en abrégé CPP W – pour C preprocessor, et à ne pas confondre avec l'extension .cpp des fichiers sources en langage C++. Il ne présente pas de différences significatives avec le préprocesseur du C++ qui justifieraient, dans le cadre de ce cours, un exposé spécifique pour ce dernier.

En comparaison, les langages plus modernes comme Ada, Java, Python… n'emploient pas de préprocesseur (ils sont conçus pour s'en passer). Néanmoins, il existe des préprocesseurs généralistes, comme par exemple GNU M4 W qui peuvent s'adapter si besoin à toutes sortes de codes sources pour opérer des prétraitements aussi puissants que ceux de CPP. Dans une formation aux bases de la programmation, l'étude de CPP présente donc un intérêt général.

A contrario d'un compilateur, un préprocesseur ne produit aucun élément exécutable, aussi est‑il en principe indépendant de la machine cible pour laquelle le programme est codé. Cependant, dans le cas de CPP, il existe bien évidemment différentes implémentations : celle du projet GNU (cf. chap. C1‑II ), de Clang, de Microsoft, etc.

Dans ce chapitre, on étudiera uniquement le préprocesseur CPP intégré à la chaîne de compilation GCC – donc du projet GNU – implémenté sur un PC à système d'exploitation Linux.

Sous Windows, avec la chaîne de compilation MinGW, on obtient des résultats très similaires.

Dans une chaîne de compilation C ou C++, le rôle de CPP est double :

  1. Il opère des prétraitements implicites sur le fichier source, préparatoires à la compilation, en convertissant le jeu de caractères employé, en supprimant les fins de lignes fictives, en uniformisant caractères d'espacement et supprimant les commentaires.
  2. Il permet au programmeur de coder prétraitements explicites sur le fichier source par le biais d'une syntaxe spécifique différente de celle des instructions – remarquable notamment par le symbole initial #.
  3. On parle de directives au préprocesseur, ces dernières permettant :
    • d'inclure dans le fichier source le contenu d'autres fichiers ; c'est notamment ainsi que sont incorporés les fichiers d'en‑tête de bibliothèques ;
    • de définir des pseudo‑constantes qui ont la particularité, contrairement aux constantes usuelles, d'être non typées ;
    • de définir des pseudo‑fonctions qui ont la particularité, contrairement aux fonctions, de prendre des arguments non typés ;
    • de mettre en œuvre une compilation conditionnelle pour adapter le code source en fonction de valeurs prises par des variables d'environnement, et ainsi pouvoir porter le programme sur différentes machines ou systèmes.

Constituant un véritable métalangage, les directives au préprocesseur confèrent aux langages C et C++ une puissance d'expression exceptionnelle. Néanmoins, il faut beaucoup d'expérience pour en maîtriser les subtilités.

En adoptant un plan similaire à celui de la liste ci‑dessus, ce très long chapitre a pour objectif d'en exposer toutes les bases. Il doit permettre au codeur débutant de se familiariser suffisamment avec cette composante importante de la chaîne de compilation pour pouvoir comprendre son emploi très fréquent, en particulier dans les fichiers sources des modules de bibliothèque Arduino. De nombreux exemples dans ce cours sont tirés de ces fichiers.

Et pour plus de détails, on pourra toujours consulter le guide en ligne fourni par le projet GNU au lien suivant .

Prétraitements implicites du fichier source

Les prétraitements implicites opérés par le préprocesseur CPP correspondent selon la norme aux phases 1 à 6 de traduction, à l'exception de tous ceux de la prise en compte des directives qui constituent des prétraitements explicites, regroupés dans la phase 4.

Ces phases se déroulent en principe dans l'ordre de leur numérotation, mais avec des phases de rétroaction lorsque la phase 4 est abordée.

Invocation du préprocesseur

Un préprocesseur CPP est intégré à toute chaîne de compilation GCC pour les langages C et C++. Implicitement, l'exécution d'une commande gcc ou g++ commence par l'invocation de ce préprocesseur pour traiter le ou les fichiers sources passés en argument.

On parle de prétraitements – en anglais, preprocessing – pour désigner le travail effectué sur le code source par un préprocesseur.

Visualisation des prétraitements du préprocesseur

Si l'on souhaite observer les prétraitements accomplis par le préprocesseur seul – c'est‑à‑dire, sans aller plus loin dans la chaîne de compilation, et en obtenant comme sortie un fichier de texte – il suffit de saisir une commande système de la forme :

cpp fichier source -o fichier prétraité

sachant que si l'option -o n'est pas invoquée, le fichier prétraité n'est pas créé mais simplement affiché dans le terminal.

Dans la forme ci‑dessus, on peut aussi remplacer la commande cpp par gcc -E (ou g++ -E pour le C++), l'option -E ayant pour effet d'arrêter l'exécution de la commande juste après les prétraitements opérés par le préprocesseur (donc, sans compilation proprement dite).

Le fichier prétraité produit en sortie par la commande cpp est seulement une image textuelle du flot de tokens prétraités, c'est‑à‑dire une succession d'unités lexicales qui est en réalités fournies par le préprocesseur au compilateur.

Le contenu de ce fichier est consultable avec n'importe quel éditeur de code ou commande système de visualisation comme cat ou more sous Linux (cf. chap. S1‑III ). Il s'apparente à du code C ou C++ et son extension conventionnelle .i peut permettre facilement aux éditeurs de code de le reconnaître comme tel.

À partir du code source académique ci‑dessous, enregistré par exemple dans un fichier simpleProg.c :

/* Useless but very simple program to test CPP */
char a = 5;

int main(void)
{
  a++;
  // no output
  return 0;
}

la commande :

cpp simpleProg.c -o simpleProg.i

produit en sortie le fichier prétraité simpleProg.i listé ci‑dessous :

# 0 "simpleProg.c"
# 0 "<built-in>"
# 0 "<command-line>"
# 1 "/usr/include/stdc-predef.h" 1 3 4
# 0 "<command-line>" 2
# 1 "simpleProg.c"

char a = 5;

int main(void)
{
 a++;

 return 0;
}

On verra infra  en détails en quoi consistent les prétraitement accomplis par le préprocesseur.

La commande cpp (ou ses équivalents gcc -E et g++ -E) possède de nombreuses options. On peut en trouver la liste exhaustive sur cette page web  du site de référence du projet GNU.

Détail des prétraitements implicites opérés par le préprocesseur

Le travail du préprocesseur commence par des prétraitements préliminaires sur le code source pour faciliter la compilation qui va suivre. Ces prétraitements peuvent être considérés comme implicites dans la mesure où ils sont effectués par défaut, même en l'absence de toute directive.

Certaines options de la commande cpp sont parfois indispensables pour obtenir un code exécutable satisfaisant, notamment si le fichier source n'est pas encodé en UTF‑8.

Quant aux autres options, elles permettent de moduler les prétraitements opérés de façon très pertinente en fonction des spécificités du projet sur lequel on travaille. Dans tous les cas, on se situe déjà dans un usage avancé de la chaîne de compilation.

Seuls les prétraitements les plus remarquables sont exposés par la suite. Pour une présentation exhaustive, on pourra consulter cette page web  du site de référence du projet GNU.

Transcodage du jeu de caractères du fichier source

A priori, un fichier source en langage C (idem en C++) peut être encodé dans n'importe quel jeu de caractères, selon le paramétrage du poste de travail, de son système d'exploitation et de l'éditeur de code employé. On peut déterminer cet encodage :

  • soit par une commande système ;
  • par exemple, sous Linux, il suffit de saisir dans un terminal une commande de la forme (cf. chap. S1-III ) :
      
    file fichier source
  • soit en consultant les propriétés du fichier dans un éditeur de code ;
  • par exemple, avec Sublime Text, le format d'encodage du fichier s'affiche à droite dans la barre de notifications (à condition d'avoir saisi le paramétrage "show_encoding": "true" dans le fichier Settings accessible via le menu Preferences).

Mais un compilateur n'est pas capable de s'adapter seul à toute la diversité des jeux de caractères. Conformément à la norme ISO 10046 du langage C, les algorithmes des compilateurs GCC sont conçus pour traiter des fichiers encodés en UTF‑8 (cf. chap. C3‑VIII ).

Aussi, avant même tout prétraitement, le préprocesseur CPP du projet GNU opère le transcodage dans le format UTF‑8 des fichiers sources passés en arguments de la commande gcc ou g++. Toutefois, il est nécessaire pour cela d'indiquer précisément le format d'encodage de ces fichiers source, à l'aide de l'option :
-finput-charset=source_format 

Selon la norme, ce travail de transcodage constitue la phase 1 de traduction du code source en code exécutable.

Prenons le cas académique d'une instruction de sortie standard comme celle ci‑dessous, dont la chaîne de format contient des caractères accentués.

#include <stdio.h>

int main(void)
{
  printf("Connecté au réseau.\n");
  return 0;
}

Typiquement, si le fichier source – nommé par exemple, testCharset.c :

  • est codé sur un PC Windows dans le format CP1252 (cf. chap. C3‑VIII )
  • puis compilé sur un PC Linux sans option, puis exécuté sur cette même machine,

alors on obtient un remplacement de chaque occurrence de la lettre « é » par un caractère de substitution (cf. chap. C3‑VIII ) :

gcc testCharset.c -o testCharset && ./testCharset
Connect� au r�seau.

En revanche, si ce même fichier source est compilé avec l'option de transcodage appropriée, on obtient une sortie correcte des caractères accentués :

gcc -finput-charset=Windows-1252 testCharset.c -o testCharset && ./testCharset
Connecté au réseau.

Remarque. Rappelons que le transcodage du jeu de caractères du fichier source peut également être opéré avec l'éditeur de code, avant tout appel de la chaîne de compilation.

Par exemple, avec Sublime Text, il suffit de cliquer sur l'indication de format dans la barre de notifications et de sélectionner le format UTF‑8 via le menu déroulant (cf. chap. C3‑VIII . Le fichier source étant ainsi préalablement transcodé, le préprocesseur se trouve déchargé de cette tâche.

Transcodage (inverse) du jeu de caractères pour l'environnement d'exécution

Après tous les prétraitements qu'il opère (y compris ceux des directives), le préprocesseur CPP peut à nouveau transcoder les constantes de type caractère et chaîne de caractères, dans un autre format que UTF‑8, afin qu'il corresponde à celui de l'environnement d'exécution du programme.

Pour cela, il suffit d'employer l'option :
-fexec-charset=output_format 

Selon la norme, ce travail constitue la phase 5 de traduction du code source en code exécutable.

Si un programme est compilé sous Linux mais destiné à être exécuté sous Windows – on parle alors de compilation croisée (cf. chap. C4‑IV ) – on peut saisir l'option :
-fexec-charset=windows-1252
en veillant à ce que cette page de code soit bien activée dans la fenêtre de terminal d'exécution (cf. chap. C3‑VIII ).

Suppression des sauts de ligne « fictifs »

En programmation, un fichier source est constitué de lignes séparées les unes des autres par un caractère LF ou CR (voire une séquence de caractères CR LF) de fin de ligne (cf. chap. C2‑II ).

Mais on a vu également qu'en langages C et C++, il est possible de coder, à destination du préprocesseur, la suppression d'un saut de ligne en faisant précéder ce dernier par le symbole \ (cf. chap. C2‑II ).

On dit d'un saut de ligne ainsi codé qu'il est « fictif ».

Dans un tel cas, le préprocesseur CPP ignore tout simplement le symbole \ et le caractère (ou à la séquence de caractères) de fin de ligne immédiatement consécutif et continue son travail sur la suite du fichier source.

Selon la norme, ce travail constitue la phase 2 de traduction du code source en code exécutable.

Suppression des commentaires

Rappelons qu'en programmation, les commentaires ne constituent pas du code à proprement parler (cf. chap. C2‑II ) et doivent donc être ignorés par le compilateur.

Pour alléger d'autant la mémoire sur laquelle la chaîne de compilation opère (car un fichier source peut être très volumineux), il est pertinent de supprimer au plus tôt les commentaires. C'est donc le préprocesseur qui s'en charge : dans son travail, il les ignore tout simplement.

Avec le CPP du projet GNU, cette suppression ne fait l'objet d'aucune substitution sauf éventuellement – si nécessaire – par un caractère espace.

Selon la norme, la suppression des commentaire est incluse dans la phase 3 de traduction du code source en code exécutable.

Dans le premier exemple supra , les deux commentaires présents dans le fichier source (aux lignes nº 1 & 7 dans le cadre de gauche ci‑dessous) sont absents dans le fichier prétraité simpleProg.i, là où ils « auraient dû » être, c'est‑à‑dire aux lignes nº 7 & 13 du cadre de droite ci‑dessous).

/* Useless but... */
char a = 5;

int main(void)
{
  a++;
  // no output
  return 0;
}

char a = 5;

int main(void)
{
 a++;

 return 0;
}

Toutefois, les sauts de lignes ne faisant pas partie des commentaires, ils sont maintenus. Avec un éditeur de code qui affiche les caractères d'espacement, on peut constater que les lignes nº 7 & 13 correspondantes sont complètement vides.

Remarque. L'éditeur de code Sublime Text affiche les caractères d'espacement dans une portion de code sélectionnée (en surbrillance) si le paramétrage "draw_white_space": "selection" est codé dans le fichier Settings, lequel est accessible via le menu Preferences.

Il est néanmoins possible de garder les commentaires dans le fichier prétraité généré par la commande cpp : il suffit d'employer l'option -C.

Tokénisation

Le préprocesseur CPP est également chargé de transformer le texte du code source en un flux de tokens de prétraitement (preprocessing tokens), à destination du compilateur. Le terme token est un anglicisme qu'on pourrait traduire littéralement par « jeton » et qui désigne une unité lexicale sur laquelle le compilateur va pouvoir aisément travailler. Sans entrer dans les détails, les tokens sont distingués de cinq catégories possibles :

  • identificateurs, y compris les mots‑clefs ;
  • nombres, sans distinction de types (cette tâche est dévolue au compilateur lui‑même),
  • caractère isolés et chaînes de caractères,
  • ponctuateurs (délimiteurs, opérateurs, etc.),
  • autres, sachant que la présence d'un seul token de cette catégorie déclenche une erreur (autrement dit, tous les tokens doivent être normalement de l'une des quatre catégories précédentes).

Selon la norme, cette tokénisation est incluse dans la phase 3 de traduction du code source en code exécutable. Elle n'est pas visualisable dans le fichier prétraité de la commande cpp.

Uniformisation des caractères d'espacement

Rappelons que les langages C et C++ sont dits à format libre (cf. chap. C2‑II ), au sens où on peut ajouter autant de caractères d'espacement que l'on souhaite entre les différents éléments de langage d'un programme source – ces caractères d'espacement étant dans la pratique des espaces (code UTF‑8 0x20) ou des sauts de tabulation (code UTF‑8 0x09).

Au cours de la tokénisation, le préprocesseur CPP fait en sorte que deux tokens successifs soient séparés par un seul caractère espace. Ce faisant, il supprime donc :

  • tous les caractères de tabulation,
  • et tous les caractères espaces surnuméraires.

Mais ce travail – encore en phase 3 de traduction du code source en code exécutable – n'est pas obligatoire selon la norme (il est laissé à la liberté de l'implémentation, et les concepteurs de CPP ont fait le choix de le mettre en œuvre).

Attention : dans le fichier prétraité produit par la commande cpp, les caractères d'espacement surnuméraires ne sont pas forcément supprimés, le but étant que le code traité soit présenté d'une façon similaire à celle du fichier source pour faciliter la lecture par le codeur.

Dans le premier exemple supra , on peut voir que les tabulations d'indentation – d'une valeur de 2 espaces – présentes dans le fichier source au début des lignes nº 6 & 8 ont été remplacées dans le ficher prétraité par de simples espaces au début des lignes nº 12 & 14.

int main(void)
{
  a++;
  // no output
  return 0;
}
int main(void)
{
 a++;

 return 0;
}

Insertion des marques de ligne

Pour localiser de potentielles erreurs détectées par la chaîne de compilation, le préprocesseur CPP ajoute des marques de ligne (en anglais, line‑markers) des fichiers sources parcourus au fur et à mesure des prétraitements. Ces marques de ligne ne sont pas des tokens, elles sont ignorées par les algorithmes du compilateur lors du traitement du code source. Mais elles apparaissent dans le fichier prétraité de la commande cpp.

Lorsqu'une erreur est détectée, le fichier source dans laquelle elle se trouve est identifié par la marque de ligne immédiatement en amont.

Une marque de ligne se présente de la forme :
numéro de ligne fichier drapeaux
sachant que les drapeaux sont facultatifs et qu'ils ne sont que des chiffres parmi les suivants :

  • 1 qui indique le début d'un nouveau fichier ;
  • 2 qui indique le retour dans un fichier (typiquement après une inclusion) ;
  • 3 qui indique que le code à suivre provient d'un fichier d'en‑tête et que certains avertissements de la chaîne de compilation seront inhibés ;
  • 4 qui indique que le code à suivre doit être considéré comme un bloc externe en langage C.

L'ajout de marques de ligne n'est pas mentionné dans la norme. Il est donc difficile de savoir dans quelle phase ce travail s'inscrit. On pourrait même supposer qu'il intervient avant même la phase 1 de traduction du code source en code exécutable.

Dans le premier exemple supra , six marques de ligne apparaissent au début du fichier prétraité obtenu par appel de la commande cpp (on les retrouve en principe dans tous les programmes) :

# 0 "simpleProg.c"
# 0 "<built-in>"
# 0 "<command-line>"
# 1 "/usr/include/stdc-predef.h" 1 3 4
# 0 "<command-line>" 2
# 1 "simpleProg.c"

On peut ainsi retracer certains prétraitements « cachés » de la chaîne de compilation :

  • # 0 "simpleProg.c" signale que le préprocesseur commence son travail à la ligne nº 0 du fichier simpleProg.c, qui est ici le seul fichier source. Cette indication sert surtout à préciser le nom du fichier source, car son prétraitement effectif est immédiatement différé par ce qui suit.
  • # 0 "<built-in>" signale que le préprocesseur commence (à partir de la ligne nº 0) le prétraitement du fichier virtuel built-in où sont codées des directives qui prédéfinissent des pseudo‑constantes des langages C/C++ permettant d'obtenir la valeur de nombreuses variables d'environnement (cf. infra ).
  • # 0 "<commande-line>" signale que le préprocesseur commence (à partir de la ligne nº 0) le prétraitement du fichier virtuel command-line où sont codées des directives qui prédéfinissent des pseudo‑constantes qui sont spécifiques à la ligne de commande (en fonction des options de compilation choisies ou par défaut) ainsi qu'au terminal d'exécution.
  • # 1 "/usr/include/stdc-predef.h" 1 3 4 signale que le préprocesseur commence (à partir de la ligne nº 1) le prétraitement d'un nouveau fichier – d'où le drapeau 1 – dont le chemin d'accès est /usr/include/stdc-predef.h. On verra qu'il s'agit en fait d'une directive d'inclusion. On a également le drapeau 3 puisqu'il s'agit d'un fichier d'en‑tête et le drapeau 4 puisque ce fichier contient du code en langage C.
  • # 0 "<commande-line>" signale que le préprocesseur reprend à la ligne nº 0 le prétraitement du fichier virtuel command-line. Le drapeau 2 indique qu'il s'agit d'un retour (après la prise en compte de la directive d'inclusion du fichier stdc-predef.h qui vient d'avoir lieu).
  • # 1 "simpleProg.c" signale que le préprocesseur commence (cette fois pour de bon) à la ligne nº 1 le prétraitement du fichier source simpleProg.c.
  • Comme il n'y a pas de directives d'inclusion codées dans ce fichier, il n'y a pas d'autres marques de ligne qui suivent dans le fichier prétraité généré par la commande cpp.

Syntaxe générale des directives au préprocesseur

Ainsi qu'il a été énoncé en introduction de ce chapitre, les directives au préprocesseur CPP constituent un métalangage qui est distinct du langage C ou C++ employé pour coder les instructions du programme. Néanmoins, il est commode de considérer parfois que ces directives font « partie » de ces deux langages, tant elles sont indispensables.

Après avoir effectué la plupart des prétraitements implicites décrits supra, le préprocesseur traite les directives une par une au fur et à mesure de leur présence dans le code, et ce jusqu'à ce qu'il n'y en ait plus aucune.

La norme considère ces prétraitements comme la phase 4 de la traduction du code source en code exécutable. Les débutants ont tendance à croire que c'est là que commence véritablement le travail du compilateur (mais on a vu qu'il ne faut pas négliger tous les prétraitements implicites qui les précèdent).

Lignes de contrôles

Isolement et symbole initial

En langages C et C++, toute directive au préprocesseur est codée par une ou plusieurs lignes de contrôles dont chacune :

  • s'inscrit nécessairement seule dans une ligne de fichier source, c'est‑à‑dire sans aucune instruction dans cette même ligne ;
  • débute toujours (avec une éventuelle indentation) par le symbole #, dit croisillon W, dont le rôle est d'indiquer sans ambiguïté que cette ligne est du ressort du préprocesseur.

Après traitement des directives par le préprocesseur, toutes les lignes de contrôle sont en quelque sorte « supprimées » du code source. En fait, elles n'ont tout simplement pas leur place dans le flot de tokens que le préprocesseur transmet au compilateur. Et bien évidemment, elles n'apparaissent pas dans le fichier prétraité généré par la commande cpp .

La directive d'inclusion ci‑dessous est constitué d'une seule ligne de contrôle :

#include <stdio.h>

Dans le fichier prétraité, cette ligne n'existe plus, et pour cause : elle est remplacée par l'intégralité du contenu du fichier d'en‑tête stdio.h de la bibliothèque standard du langage C (cf. infra  pour plus de détails).

  1. Le symbole croisillon W (en anglais hash, d'où le terme hashtag) ne doit pas être confondu avec le symbole musical dièse ♯ (en anglais sharp) – et ce même si le croisillon est abusivement utilisé pour désigner le langage « C‑sharp ».
  2. Les lignes de contrôle ne doivent bien évidemment pas être confondues avec les marques de ligne générées par le préprocesseur (cf. supra ). Même si elles commencent aussi par un croisillon, ces dernières n'ont pas du tout le même rôle.

Mot réservé d'une ligne de contrôle

Dans toute ligne de contrôle d'une directive figure un mot réservé qui précise quel contrôle exerce la ligne sur le code : include, define, undef, if, ifdef, ifndef, etc.

Un mot réservé désignant une ligne de contrôle n'est pas un mot‑clef du langage. S'il y a homonymie, il ne faut pas confondre.

Une ligne de contrôle de compilation conditionnelle #if ne doit pas être confondue avec une structure de contrôle de bifurcation if (condition) (cf. chap. C2‑V ).

Terminaison d'une ligne de contrôle

Par défaut, toute ligne de contrôle d'une directive doit se terminer à la fin de la ligne sans délimiteur ;.

C'est le caractère de contrôle de saut de ligne, le plus souvent invisible dans le fichier, typiquement CR (carriage return) ou LF  (line feed) ou les deux W – cf. chap. C2-II ) qui code implicitement la fin de la ligne de contrôle.

La directive de définition ci‑dessous est constituée d'une seule ligne de contrôle :

#define LED_PIN 2

Comme énoncé dans la règle ci‑dessus, elle s'achève sans le délimiteur d'instruction ; puisqu'il ne s'agit pas d'une instruction.

Remarque. On peut également observer l'absence de l'opérateur d'affectation, même si cette directive « donne » à la pseudo‑constante LED_PIN la valeur 2.

Comme on l'a vu supra , une ligne de contrôle peut néanmoins être prolongée à la ligne suivante si le symbole contre‑oblique \ (en anglais, antislash ou backslash) de suppression de saut de ligne est ajouté juste avant le saut de ligne (aucun caractère, même de commentaire, ne doit être placé entre un symbole \ et le saut de ligne dont ce symbole code la suppression).

Autant de suppression de sauts de lignes consécutifs que nécessaires peuvent être employés pour coder des directives complexes (notamment des macro‑définitions de pseudo‑fonctions).

La directive de définition ci‑dessous (qui sera expliquée en détail plus loin ) est constituée d'une seule ligne de contrôle :

#define swapValues(a, b) { \
  double c = a; \
  a = b; \
  b = c; \
}

mais elle est répartie sur 5 lignes du fichier source grâce à 4 symboles de suppression de saut de ligne employés en fin des lignes nº 1, 2, 3 & 4.

Remarque. Cette directive aurait aussi pu être codée de façon moins lisible sur une seule ligne du fichier source :

#define swapValues(a, b) {double c = a; a = b; b = c;}

C'est d'ailleurs ainsi que la précédente forme codée sur 5 lignes serait d'abord transformée après les prétraitements implicites du préprocesseur.

Séparateurs « blancs » et commentaires

On rappelle (cf. chap. C2‑II ) que les langages C et C++ sont à format libre, c'est‑à‑dire qu'ils autorisent l'insertion de séparateurs blancs et de commentaires partout dans les instructions (mais pas au sein des atomes, bien entendu), sachant que les séparateurs « blancs » sont usuellement les caractères espaces, saut de tabulations horizontale et saut de ligne.

Dans les lignes de contrôle des directives, ces règles de format libre sont presque les mêmes :

  • les caractères espaces et saut de tabulation sont insérables en quantités illimitées, sauf bien sûr dans les atomes (mots réservés, identificateurs) ;
  • et on a vu supra qu'ils sont uniformisés en espaces et laissés uniquement pour séparer les tokens ;
  • en revanche, les sauts de lignes sont à éviter dans la mesure du possible ;
  • on vient de voir que s'il faut insérer un saut de ligne, il doit être précédé par le symbole \ pour être supprimés par le préprocesseur comme indiqué supra  ;

et il en va de même pour les commentaires qui sont, rappelons‑le, insérables partout où un espace est autorisé (cf. chap. C2‑II ), mais qu'il est préférable de cantonner aux fins de ligne.

La directive de définition :

#define LED_PIN 2

aurait pu être truffée d'espaces et de commentaires par un esprit « fantasque » :

	#   define /*this is a directive*/ LED_PIN        2

Mais, évidemment, en termes de lisibilité, l'indentation initiale et le commentaire en plein milieu sont l'un et l'autre à éviter, à moins que la ligne de contrôle soit complexe et nécessite des explications localement détaillées.

La bonne pratique consiste évidemment à privilégier le commentaire de fin de ligne, comme ci‑dessous :

#define LED_PIN 2 // this is a directive

de la même manière usuelle que pour les instructions.

À la fin d'une ligne de contrôle, il est impossible de placer un symbole de suppression de saut de ligne \ après un commentaire de fin de ligne. En effet, ce symbole ne serait pas reconnu comme voulu mais comme un commentaire, et par conséquent ignoré par le préprocesseur.

Pour commenter une ligne de contrôle formatée sur plusieurs lignes du fichier source, il est donc nécessaire d'avoir recours aux délimiteurs de blocs de commentaires /* */.

Position et effet des directives dans le code

En principe, une directive peut être codée partout dans un fichier source, y compris dans une fonction ou un bloc, quel que soit son niveau hiérarchique dans le fichier (cf. chap. C4‑II )

Toutefois, la position d'une ligne de contrôle dans le fichier source n'est pas indifférente. En particulier, une directive #define ou #include :

  • ne prend effet qu'à partir de la ligne où elle est codée ;
  • opère jusqu'à la fin du fichier, sauf directive contraire (par exemple, #undef – cf. infra ) ;
  • n'a aucun effet dans les autres fichiers sources du programme (sauf, évidemment, via une directive d'inclusion).

Pour des questions de lisibilité, on s'efforce de coder ces directives de préférence en début de fichier, hors de toute fonction et plutôt avant les déclarations (mais pas forcément…).

En revanche, ces recommandations de bonnes pratiques ne concernent pas les directives de compilation conditionnelle.

Directive d'inclusion d'un fichier source

Syntaxe générale d'une directive #include

Dans un fichier source qu'on peut qualifier de primaire, une directive d'inclusion d'un fichier source secondaire se code par une seule ligne de contrôle de la forme :
#include chemin délimité
où le chemin délimité est un chemin dans l'arborescence des fichiers du poste de travail, encadré dans une paire de délimiteurs < > " " selon une syntaxe qui sera détaillée infra .

Le fichier secondaire doit bien évidemment être accessible en lecture dans l'arborescence des fichiers du poste de travail où le programme est compilé.

Prétraitement d'une directive #include

Lorsque le préprocesseur « rencontre » une directive d'inclusion, il suspend les prétraitements du fichier primaire pour enchaîner ceux du fichier secondaire, dans le même ordre immuable (prétraitements implicites puis prétraitements explicites, si le fichier secondaire contient lui‑même des directives).

C'est seulement une fois que tous les prétraitements du fichier secondaire sont achevés (y compris leurs effets dans le fichier primaire) qu'intervient la reprise des prétraitements du fichier primaire, à la ligne qui suit immédiatement celle de la directive d'inclusion.

Dans le fichier prétraité généré par la commande cpp, toute directive d'inclusion se voit tout simplement remplacée par le contenu intégral du fichier secondaire.

Du fait que le préprocesseur effectue une tokénisation du code (cf. supra ), le fichier secondaire doit obligatoirement contenir des unités lexicales complètes. Toutefois, il est vivement conseillé de coder des instructions complètes (autrement dit, ne pas coder une instruction à cheval sur plusieurs fichiers).

Voici un programme académique qui compte le nombre de caractères dans une chaîne de caractères mémorisée dans une variable nommée testString, déclarée en ligne nº 1.

L'algorithme utilise deux pointeurs (notion étudiée au chap. C5‑I ) :

  • start pour repérer le début de la chaîne de caractères ;
  • pC, un pointeur de caractères initialisé à la même valeur que start (il pointe donc aussi sur le premier caractère de la chaîne).

Il suffit ensuite d'incrémenter pC tant qu'il ne pointe pas sur le caractère nul qui marque la fin de la chaîne. Le résultat est donné par la différence des deux pointeurs.

Pour ne pas faire usage d'une fonction d'entrée‑sortie standard comme printf qui nécessiterait l'inclusion du fichier d'en‑tête volumineux stdio.h, on utilise de façon non conventionnelle la valeur retournée par la fonction main pour délivrer le résultat.

char testString[] = "12345";

int main(void)
{
  char * start = testString, * pC = testString;
  while (*pC) pC++;
  return pC - start;
}

Cependant, si la chaîne de caractères était très longue (imaginons qu'il s'agisse d'un texte de plusieurs pages), le reste du code serait malcommode à consulter puisqu'il faudrait naviguer loin dans le fichier source pour trouver le début de la fonction main.

La solution la plus intuitive consiste donc à déporter la ligne nº 1 dans un fichier secondaire nommé par exemple testString.h :

char testString[] = "12345";

et placé dans le même répertoire que le fichier source (primaire) du programme. Au début de ce dernier, il suffit alors de coder la directive d'inclusion ci‑dessous :

#include "testString.h"

int main(void)
{
  char * start = testString, * pC = testString;
  while (*pC) pC++;
  return pC - start;
}

Via la commande  :

cpp exampleInclude.c -o exampleInclude.i

on obtient le fichier prétraité suivant :

# 0 "exampleInclude.c"
# 0 "<built-in>"
# 0 "<command-line>"
# 1 "/usr/include/stdc-predef.h" 1 3 4
# 0 "<command-line>" 2
# 1 "exampleInclude.c"
# 1 "testString.h" 1
char testString[] = "12345";
# 2 "exampleInclude.c" 2

int main(void)
{
  char * start = testString, * pC = testString;
  while (*pC) pC++;
  return pC - start;
}

où, à la ligne nº 8, on voit que la directive #include codée dans le fichier source a bien été remplacée par le contenu du fichier testString.h (la déclaration de la chaîne testString).

Sous Linux, on peut aisément vérifier que le programme se compile et s'exécute correctement en enchaînant les commandes suivantes (cf. chap. C2‑I ) :

gcc exampleInclude.c -o exampleInclude.i
./exampleInclude
echo $?
5

On obtient bien la valeur retournée 5 puisque la chaîne déclarée testString comporte cinq caractères.

Syntaxe du chemin d'accès au fichier source secondaire

Le chemin d'accès au fichier secondaire à inclure est formé avec le caractère / (slash) comme séparateur générique de répertoires (opérationnel même si le système d'exploitation – notamment Windows – en utilise un autre).

Il peut être délimité selon deux syntaxes qui ne doivent pas être confondues :

  1. #include <chemin>  (entre chevrons), si le fichier est ciblé à partir d'un des répertoires connus par la chaîne de compilation (typiquement, par la variable d'environnement PATH) ;
  2. #include "chemin" (entre guillemets), si le fichier est ciblé :
    • d'abord à partir du répertoire de compilation,
    • puis, s'il n'y est pas trouvé, à partir d'un des répertoires connus par la chaîne de compilation ;

sachant qu'en dernier recours, le préprocesseur cherche le fichier à partir du répertoire racine du poste de travail. On peut donc spécifier aussi bien un chemin relatif qu'absolu.

En pratique, la syntaxe <chemin> s'emploie de préférence lorsque le fichier secondaire à inclure est un fichier d'en‑tête issu :

  • de la bibliothèque standard du langage C (par exemple stdio.h, stdlib.h, math.h…) ou du langage C++ (par exemple iostream, cmath… et dans ce cas, on rappelle l'absence d'extension .h – cf. C2‑I ) ;
  • des bibliothèques enregistrées d'un framework dans un environnement de programmation comme par exemple Arduino, par exemple SD.h, SPI.h, avr/interrupt.h, etc.

Quant à la syntaxe "chemin", elle peut aussi fonctionner dans les cas précédents, mais la bonne pratique consiste à l'employer seulement pour un fichier secondaire stocké dans le répertoire de projet (ou éventuellement ses sous‑répertoires) du programme que l'on compile. En effet, dans un tel cas, la syntaxe entre chevrons ne fonctionne pas.

Dans tous les cas, le chemin est reconnu par le préprocesseur comme une chaîne de caractères, avec pour conséquences :

  • qu'elle ne peut faire l'objet d'aucune substitution via une directive #define (cf. infra ) ;
  • qu'elle ne peut comporter aucun commentaire ;
  • qu'elle est insensible aux séquences d'échappement (cf. chap. C3‑VIII ).

Enfin, le nom du fichier secondaire obéit aux mêmes contraintes que pour n'importe que fichier source codé en langage C ou C++ (extension sans importance, pas d'espace ni symboles, ni caractères spéciaux, etc. – cf. chap. C2‑I ).

  1. Dans un programme qui fait appel à un ou plusieurs fonctions mathématiques définies dans la bibliothèque standard, on code :
  2. #include <math.h>
    
  3. Dans un programme qui ferait appel à un fichier source secondaire display.h rangé dans un répertoire LIB lui‑même créé dans le répertoire de projet, on coderait :
  4. #include "LIB/display.h"
    

Lorsqu'il existe plusieurs fichiers homonymes dans la liste des répertoires que le préprocesseur scanne pour une directive d'inclusion, c'est le premier trouvé qui est inclus.

Sur terminale de commande Linux, on peut afficher la liste des répertoires scannés par la commande :

cpp -v /dev/null -o /dev/null

Plus généralement, le préprocesseur CPP accepte diverses options pour gérer précisément la liste des répertoires où chercher les fichiers à inclure. Pour plus de détails, on peut se reporter à ce lien .

Emplois des directives d'inclusion

Les directives d'inclusion constituent un outil essentiel de la conception modulaire des langages C et C++, puisque c'est le seul moyen pour exploiter leurs bibliothèques standards respectives et toutes les autres bibliothèques déjà développées.

En termes de lisibilité, la bonne pratique consiste à coder toutes les directives d'inclusion des fichiers d'en‑tête au tout début des fichiers sources.

Dans le cadre du développement d'un programme, il est bien évidemment recommandé d'adopter la même stratégie en répartissant le code source sur plusieurs fichiers. Les aspects techniques d'une telle répartition sont étudiés en détail au chap. C4‑V .

Un autre emploi usuel des directives d'inclusion est de pouvoir déporter dans des fichiers séparés des textes volumineux, comme des longs messages d'entrée‑sortie ou même du code HTML/CSS/JS.

Pour les exercices nº 5 à 8 du TP nº R3‑3 (partie R3 du module de formation aux réseaux ), on embarque dans une carte à microcontrôleur un serveur web, dont la page d'accueil est codée dans un fichier séparé nommé index_html.h.

On donne ici sa version simplifiée (il s'agit d'une chaîne de caractères brute – cf. chap. C5‑VI ) :

const char index_html[] PROGMEM = R"=====(
<!DOCTYPE html>
<html lang="fr">
  <head>
    <meta charset="utf-8">
    <title>LED</title>
  </head>
  <body>
    <h2> LED state: %LED% </h2>

    <h2 style="display: inline;"> LED control: 
      <a href="/switchLedOn"><button style="font-size: 100%;">ON</button></a>
      <a href="/switchLedOff"><button style="font-size: 100%;">OFF</button></a>
    </h2>
  </body>
</html>
)=====";

Ce fichier fait l'objet d'une directive d'inclusion dans le fichier principal du programme (d'extension .ino) :

#include "index_html.h"

Attention. Même si le fichier index_html.h contient principalement du code HTML, il ne doit pas être nommé avec une extension .html car il reste fondamentalement du code C ou C++. Cependant, dans un éditeur de code, rien n'interdit de le visualiser avec une coloration syntaxique adaptée au langage HTML (avec Sublime Text, il suffit de cliquer sur le nom du langage dans le coin droit de la barre d'état et de cocher le langage voulu).

Si les fichiers à inclure sont volumineux, l'ajout de nombreuses directives d'inclusion peut augmenter significativement le temps de compilation du programme. C'est une des raisons qui justifie le fait que les fichiers d'en‑tête contiennent seulement des déclarations et des directives, afin de rester aussi légers que possible. Les blocs de définition des fonctions sont quant à eux codés dans des fichiers distincts, compilables séparément.

Généralités sur les macro‑définitions

D'une façon générale, en programmation, on appelle macro‑définition W – et on emploie usuellement le terme abrégé « macro » pour le désigner – un fragment de code auquel on attribue un nom.

Ce nom ainsi défini peut ensuite être utilisé à la place du fragment de code dans tout le code consécutif à cette macro‑définition.

Syntaxe générale d'une macro‑définition

En langages C et C++, les macro‑définitions sont codées à l'aide d'une directive constituée d'une une seule ligne de contrôle de la forme :
#define expression d'identification   fragment de code

Cette forme syntaxique présente des subtilités auxquelles il faut dès maintenant prêter la plus grande attention :

  • L'expression d'identification comporte le nom de la macro et éventuellement des arguments (un peu comme une fonction). Elle est soumise à de fortes contraintes syntaxiques (il ne peut pas s'agir de n'importe quelle chaîne de caractères).
  • Le fragment de code, qui est considéré comme étant la « définition de la macro », est a contrario soumis à peu de contrainte syntaxiques.
  • On parle aussi de chaîne de substitution au sens où ce fragment de code peut être considéré comme une « chaîne de caractères » dans le fichier source (mais avec un sens beaucoup plus général que celui d'une donnée de type « chaîne de caractères » en langage C) qui vient se substituer au nom. C'est donc ce terme de chaîne de substitution que nous privilégierons par la suite.

Pour se faire une première idée, on retiendra donc que par les contraintes syntaxiques portant sur le nom, une directive #define est moins souple qu'une fonctionnalité « rechercher/remplacer » dans un éditeur de code, mais également plus puissante puisqu'elle peut admettre des arguments.

Emplois usuels des macro‑définitions

La directive #define est principalement employée pour coder dans un fichier source des pseudo‑constantes et des pseudo‑fonctions.

Une pseudo‑constante (en anglais, objet‑like macro) est défini par une macro dont :

  • l'expression d'identification est simplement le nom de la macro, c'est‑à‑dire simplement un identificateur ;
  • la chaîne de substitution est une expression dont l'évaluation prend, après expansion complète (cf. infra ), la valeur d'une constante littérale du langage.

Dans le fichier d'en‑tête Arduino.h pour cartes à cœur AVR, on trouve la macro‑définition de la pseudo‑constante :

#define DEG_TO_RAD 0.017453292519943295769236907684886

On conçoit aisément son utilité : il est bien plus lisible d'employer son identificateur DEG_TO_RAD que la valeur numérique codée par sa chaîne de substitution 0.017 qu'il est difficile pour un codeur d'identifier immédiatement.

Une pseudo‑fonction (en anglais, function‑like macro) est défini par une macro dont :

  • l'expression d'identification de la macro est composé d'un identificateur associé une liste d'arguments formels codée entre parenthèses () comme pour une « vraie » fonction mais sans typage
  • la chaîne de substitution est une expression qui peut comporter autant d'occurrences que voulu des arguments formels.

Toujours dans le fichier d'en‑tête Arduino.h pour cartes à cœur AVR, on trouve la macro‑définition de la pseudo‑fonction :

#define radians(deg) ((deg)*DEG_TO_RAD)

Elle permet de convertir en radians – c'est justement ce terme qui est choisi pour son identificateur – un angle exprimé en degrés – son argument formel deg.

Sa chaîne de substitution code tout simplement la multiplication de son argument formel deg par la pseudo‑constante DEG_TO_RAD préalablement définie.

Dans la suite du code, une expression comme radians(180) serait remplacée par le préprocesseur par l'expression :
((180) * 0.017453292519943295769236907684886)
laquelle serait ensuite évaluée environ 3.141592653589793 par le compilateur.

Pour les tester, on peut employer ces deux macro‑définition en les codant dans un programme académique en langage C comme ci‑dessous :

#include <stdio.h>

#define radians(deg) ((deg)*DEG_TO_RAD)
#define DEG_TO_RAD 0.017453292519943295769236907684886

int main(void)
{
  printf("Pi approximately equals %.15f \n", radians(180));
  return 0;
}

Prétraitement d'une directive #define

De façon générale, lorsqu'il rencontre une directive #define dans un fichier source (y compris si cette directive provient d'un fichier inclus), le préprocesseur remplace séquentiellement, dans toute la suite du code source, chaque occurrence de l'identificateur par sa chaîne de substitution.

On dit qu'il procède ainsi à l'expansion de la macro‑définition codée par cette directive.

En revanche, contrairement au compilateur, le préprocesseur ne procède à aucun calcul dans une expression formée par l'expansion d'une macro‑définition.

Le programme académique ci‑dessous affiche le prix d'une baguette de pain hors‑taxe (HT) puis toute taxe comprise (TTC), compte tenu du taux de TVA de 0,055 % sur les produits alimentaires W. Ce taux est codé à la ligne nº 3 par la macro‑définition d'une pseudo‑constante dont l'identificateur est TVA et la chaîne de substitution 0.055 sa valeur numérique  :

#include  <stdio.h>

#define TVA 0.055 // %

int main(void)
{
  const float PRIX_BAGUETTE_HT  = 1.14; // euros
  const float PRIX_BAGUETTE_TTC = PRIX_BAGUETTE_HT * (1 + TVA);
  printf("Baguette HT  : %.2f euros\n", PRIX_BAGUETTE_HT);
  printf("Baguette TTC : %.2f euros\n", PRIX_BAGUETTE_TTC);
  printf("(taux de TVA appliqué %.1f %%)\n", TVA * 100);
  return 0;
}

À partir de la ligne nº 4, dans toute la suite du code source, le préprocesseur remplace les deux occurrences (lignes nº 8 & 11) de TVA par 0.055. Et en effet, dans le fichier prétraité généré par la commande cpp, la fonction main devient :

int main(void)
{
  const float PRIX_BAGUETTE_HT  = 1.14; // euros
  const float PRIX_BAGUETTE_TTC = PRIX_BAGUETTE_HT * (1 + 0.055);
  printf("Baguette HT  : %.2f euros\n", PRIX_BAGUETTE_HT);
  printf("Baguette TTC : %.2f euros\n", PRIX_BAGUETTE_TTC);
  printf("(taux de TVA appliqué %.1f %%)\n", 0.055 * 100);
  return 0;
}

Occurrences d'expansion d'une macro‑définition

Insistons sur le fait que le premier élément d'une directive de macro‑définition est un identificateur et non pas une simple chaîne de caractères. Dans le fichier source, son expansion n'opère donc pas pour certaines occurrences de la suite de caractères que forme son identificateur, en particulier :

  • ni dans les objets de type chaîne de caractères ;
  • ni à l'intérieur (sur des parties) d'autres identificateurs qui sont, rappelons‑le, des éléments lexicaux atomiques (cf. chap. C2‑IV ) ;
  • ni bien évidemment dans les commentaires (où il ne peut y avoir d'identificateur puisqu'il ne s'agit pas de code).

À la ligne nº 11 du fichier source de l'exemple précédent  :

  printf("(taux de TVA appliqué %.1f %%)\n", TVA * 100);

le préprocesseur n'a pas effectué d'expansion dans la chaîne de format de la fonction printf puisqu'il s'agit d'une chaîne de caractères et que les trois lettres successives « TVA » qui y figurent ne constituent pas un identificateur.

Il en serait de même si l'on avait codé la macro‑définition d'une deuxième pseudo‑constante à la suite de la première, comme par exemple  :

#define FACTEUR_TVA (1 + TVA)

Lors de l'expansion de la première macro‑définition, le préprocesseur aurait opéré à la ligne nº 4 la substitution de l'identificateur TVA par 0.055 dans l'expression (1 + TVA) (ou l'identificateur TVA est utilisé) mais pas dans l'identificateur FACTEUR_TVA car il ne peut pas être décomposé (c'est un atome).

Inhibition d'une macro‑définition : la directive #undef

En aval d'une macro‑définition (et même si elle figure dans un autre fichier préalablement inclus), il est possible de coder son inhibition grâce à une ligne de contrôle de la forme :
#undef identificateur

Au delà de cette ligne, l'identificateur devient non défini au sens où il n'est plus reconnu comme tel par le préprocesseur.

Après son inhibition, l'identificateur d'une directive de macro‑définition peut alors être réutilisé pour une nouvelle macro‑définition ou une déclaration sans surcharge (cf. infra ). Ainsi, on peut notamment :

  • changer la valeur d'une pseudo‑constante en la redéfinissant autant de fois que nécessaire dans le fichier source ;
  • localiser une pseudo‑constante dans une partie de code en inhibant définitivement son identificateur à une ligne voulue du fichier source, et ce indépendamment de toute structuration du code – c'est‑à‑dire, dans un même bloc, ou même à cheval sur plusieurs niveaux de blocs…

Lors d'une inhibition, si l'identificateur n'est pas préalablement défini, alors la directive est inopérante mais silencieuse : elle ne déclenche pas d'erreur ni d'avertissement du préprocesseur.

Dans le programme académique ci‑dessous, à la ligne nº 9, on a codé l'inhibition de la macro‑définition de la pseudo‑constante TVA pour la redéfinir avec une nouvelle valeur à la ligne suivante :

#include <stdio.h>

#define     TVA   0.055  // %

int main(void)
{
  printf("(taux de TVA appliqué %.1f %%)\n", TVA * 100);

#undef      TVA
#define     TVA   0.2    // %

  printf("(taux de TVA appliqué %.1f %%)\n", TVA * 100);
  return 0;
}    

On évite ainsi de définir deux pseudo‑constantes distinctes TVA_1 et TVA_2, voire une variable dont on changerait la valeur.

Néanmoins, l'avantage de telle ou telle solution en termes de lisibilité et de robustesse aux erreurs ne peut être jugé qu'au cas par cas, au regard d'autres aspects du programme.

Emplois spéciaux de la directive #define

On peut concevoir des emplois de la directive #define potentiellement plus complexes que la macro‑définition de pseudo‑constantes et de pseudo‑fonctions. Pour se faire une idée des possibilités, la directive #define permet :

  • de substituer des synonymes aux mots‑clefs et aux opérateurs du langage (mais cet usage doit rester exceptionnel car en général peu lisible – cf. l'exemple ci‑après) ;
  • de composer séparément des fragments de codes incomplets pour les réunir ensuite en fonction du contexte de la compilation ;
  • de composer des chaînes de caractères variables en fonction du contexte de la compilation, etc.

Seuls les mots réservés des directives (include, define, etc.) et les opérateurs du C++ ne peuvent faire l'objet d'une macro‑définition (en fait, ces derniers possèdent déjà des mots‑clefs alternatifs prédéfinis ; par exemple, and pour &&, or pour ||, etc.).

  1. On a vu au chap. C3‑III  que les langages C/C++ ne disposaient pas de symboles spécifiques pour coder les opérateurs booléens xor (ou exclusif ) et xnor.
  2. Néanmoins, il est très facile de pallier cette lacune en codant les deux macro‑définitions suivantes :
    #define xor  !=
    #define xnor ==
    
    Elles permettent ensuite d'employer les identificateurs xor et xnor comme des opérateurs, respectivement à la place des opérateurs de comparaison != et == qui, l'un comme l'autre, ne sont pas lisibles comme opérateurs booléens.
    Remarques.
    • De telles directives ne définissent ni des pseudo‑constantes (car leur chaîne de substitution n'est pas une constante littérale), ni des pseudo‑fonctions (car elles n'ont pas d'argument).
    • Il n'est pas possible de procéder de même pour définir les opérateurs booléens nand et nor. En effet, ces deux opérateurs sont seulement codables l'un comme l'autre par composition de deux opérateurs . On peut donc les macro‑définir comme des pseudo‑fonctions, mais pas comme des opérateurs.
  3. À éviter ! Un francophone « allergique » à l'anglais pourrait être tenté de coder le programme ci‑dessous, qui commence par quatre directives pour définir des synonymes français aux mots‑clefs et autres éléments de langages prédéfinis :
  4. #include <stdio.h>
    
    #define entier              int 
    #define retourne            return
    #define fonctionPrincipale  main
    #define vide                void
    #define affiche             printf
    
    entier fonctionPrincipale(vide)
    {
      affiche("Bonjour, Monde !");
      retourne 0;
    }
    
    Grâce au préprocesseur, ce programme serait parfaitement compilable et exécutable, en C comme en C++. Et en effet, si l'on observait le contenu du fichier prétraité engendré par la commande cpp, on y retrouverait tous les éléments de langage originaux :
    int main(void)
    {
      printf("Bonjour, Monde !");
      return 0;
    }
    
    Toutefois, même pour un codeur débutant, une telle démarche est vivement déconseillée. En effet, au delà d'une hypothétique commodité individuelle, ces macro‑définitions nuisent gravement à la lisibilité du code. En définissant de tels synonymes, on s'exclut de la communauté des codeurs expérimentés et dans la perspective d'un futur projet en équipe, il faut en effet penser à tous les lecteurs potentiels du code qui n'auront pas en tête ces directives de « traduction » personnelles et ne voudront pas prendre du temps pour se familiariser avec.

Par ailleurs, il faut savoir que les normes des langages C et C++ interdisent en principe de redéfinir un mot‑clef, c'est‑à‑dire de coder une macro‑définition dont ce mot‑clef est l'identificateur.

Mais dans la pratique, les préprocesseurs sont le plus souvent permissifs sur ce point. Et un tel procédé est parfois utilisé pour adapter un programme à un compilateur particulier, notamment sur le typage des variables, comme l'illustre l'exemple ci‑après.

Il va sans dire que ce genre de pratique est vivement déconseillée à tout codeur débutant.

Dans le programme académique ci‑dessous, la directive codée en ligne nº 3 redéfinit le mot‑clef const par une chaîne de substitution vide :

#include <stdio.h>

#define const   // empty substitution string => keyword deleted below

int main(void)
{
  const int a = 5;
  a = 0;
  printf("%d\n", a);
  return 0;
}

Autrement dit, à partir de la ligne nº 4, le préprocesseur supprime purement et simplement toutes les occurrences du mot‑clef const. De ce fait, la donnée a déclarée en ligne nº 6 devient une variable et peut faire l'objet d'une nouvelle affectation comme celle codée en ligne nº 7.

Bien évidemment, cet exemple n'est surtout pas à suivre systématiquement (il n'y a pas d'intérêt à déclarer une constante pour ensuite vouloir changer sa valeur par une astuce). Mais pour adapter un code source volumineux à une chaîne de compilation ancienne qui ne comprend pas le mot‑clef const (car il n'a pas toujours existé en langage C), il est bien plus commode de recourir à une directive astucieuse plutôt que de procéder par des copier‑coller fastidieux dans tous les fichiers source du programme.

Règles générales de codage des macro‑définitions

Le codage des macro‑définitions obéit à des règles différentes de celles qui s'imposent déclarations de constantes et de fonctions. Il faut en avoir pleinement conscience pour éviter certains écueils « classiques ».

Les règles générales qui suivent sont considérées comme telles car elles sont valables pour les pseudo‑constantes et les pseudo‑fonctions. C'est seulement pour des raisons de simplicité pédagogique qu'elles sont illustrées avec des exemples mettant en œuvre des pseudo‑constantes uniquement.

Les règles spécifiques au codage des pseudo‑fonctions seront abordées dans une partie suivante .

Choix du nom d'une macro‑définition

Comme expliqué supra , le nom d'une macro‑définition doit être un identificateur. Cette contrainte répond au fait que le préprocesseur exige le respect des mêmes règles lexicographiques de formation des noms (cf. chap. C2‑II ) que le compilateur.

De plus, pour une pseudo‑constante, il est d'usage de coder son identificateur tout en majuscules, comme pour une constante (cf. chap. C2‑X ).

  • Identificateurs valides :
    D5   NEW_VOLTAGE   FORFAIT_EN_EUROS   _RAM_SIZE  …
  • Identificateurs non valides :
    • 5D  (chiffre initial interdit),
    • FORFAIT_EN_€  (symbole «  » interdit),
    • NEW VOLTAGE  (espace interdit),
    • NEW-VOLTAGE  (trait d'union interdit).

« Portée » d'une macro‑définition

Par défaut, l'expansion d'une macro‑définition n'est pas localisée dans un éventuel bloc où sa ligne de contrôle serait codée. En effet, le préprocesseur ignore totalement la structuration des instructions. La « portée » d'une macro‑définition est donc a priori globale.

En l'absence de toute ligne de contrôle d'inhibition, une macro‑définition opère donc jusqu'à la fin du code source que le préprocesseur est amené à traiter, c'est‑à‑dire jusqu'à la fin du fichier où elle est codée – ainsi que dans tous les fichiers où ce dernier serait éventuellement inclus.

Non‑récursivité de l'expansion

Une macro‑définition peut être codée de façon récursive, c'est‑à‑dire en employant son propre identificateur dans sa chaîne de substitution.

Néanmoins, le préprocesseur n'effectue pas d'expansion de ces occurrences récursives d'identificateur, sinon il s'enfermerait dans un travail sans fin.

Pour que le code généré par le préprocesseur soit par la suite compilable, il faut qu'un tel identificateur :

  • ne soit jamais utilisé dans les instructions du programme ;
  • ou alors fasse par ailleurs l'objet d'une déclaration.

Dans la pratique, un tel codage n'a de sens que dans le cadre plus large de directives de compilation conditionnelle .

Dans le code académique (incomplet) ci‑dessous :

enum {AM, PM} midDay;

#define  AM AM
  • on déclare une variable de type énuméré anonyme dont chaque élément (AM, PM) est, rappelons‑le, implicitement une constante déclarée de type int (cf. chap. C3‑IV ) ;
  • et on code (avant ou après, peu importe) une macro‑définition homonyme – donc récursive – de AM ; il s'agit donc d'une pseudo‑constante (sa valeur vaut 0).

L'intérêt d'une telle directive est qu'elle permet ensuite coder une ligne de contrôle de compilation conditionnelle comme par exemple #ifdef AM.

Surcharge d'un identificateur déjà défini ou déclaré

Les diverses possibilités de surcharge d'un identificateur par une macro‑définition de ce dernier, qu'il s'agissent d'un identificateur de donnée (variable ou constante) ou d'une pseudo‑constante, sont décrites ci‑après. Elles sont intéressantes notamment pour effectuer des modifications ponctuelles d'un code existant, en agissant uniquement par des directives, donc sans toucher aux instructions.

Plus « souple » que le compilateur, le préprocesseur accepte la surcharge de l'identificateur d'une macro‑définition par une autre macro‑définition dans la même portée (cf. chap. C4‑II ).

Toutefois, une telle surcharge n'est pas conforme aux règles de bonnes pratiques, aussi le préprocesseur la signale par un avertissement. Pour bien faire, il faut préalablement inhiber une macro‑définition avant de la redéfinir (cf. supra ), et alors on peut considérer qu'il n'y a plus de surcharge à proprement parler.

Dans le programme académique ci‑dessous de calculs financiers, la macro‑définition de la pseudo‑constante TVA à la ligne nº 3 est surchargée par celle de la ligne nº 9 avec une valeur différente.

#include  <stdio.h>

#define     TVA   0.055 // %

int main(void)
{
  printf("(taux de TVA appliqué %.1f %%)\n", TVA * 100);

#define     TVA   0.2  // %
  
  printf("(taux de TVA appliqué %.1f %%)\n", TVA * 100);
  return 0;
}

À l'invocation de la commande cpp ou gcc, même sans option particulière, on obtient donc un avertissement, comme ci‑dessous :

gcc testDefineTVA2.c -o testDefineTVA2
testDefineTVA2.c:8: warning: "TVA" redefined     9 | #define TVA 0.2 // %       | testDefineTVA2.c:3: note: this is the location of the previous definition     3 | #define TVA 0.055 // %       |

Néanmoins, la compilation n'échoue pas et on obtient une exécution satisfaisante avec la sortie ci‑dessous :

./testDefineTVA2
(taux de TVA appliqué 5.0 %) (taux de TVA appliqué 20.0 %)

Si la surcharge de l'identificateur d'une macro‑définition procède par une redéfinition à l'identique, le préprocesseur n'émet pas d'avertissement.

Cela peut arriver notamment lorsqu'un fichier d'en‑tête redéfinit à l'identique une macro‑définition d'un autre fichier d'en‑tête préalablement inclus.

Attention, pour qu'une redéfinition soit reconnue par le préprocesseur comme étant identique à la précédente, il faut qu'elle engendre les mêmes tokens, ce qui peut être assez subtil en termes d'espacements.

En revanche, comme tout identificateur de macro‑définition est systématiquement remplacé par sa chaîne de substitution avant transmission au compilateur, il peut être employé en surcharge d'une déclaration antérieure y compris dans la portée de cette déclaration. Il n'y a alors aucun avertissement du préprocesseur.

Dans le programme académique ci‑dessous, après la déclaration à la ligne nº 3 de la constante globale TVA, on surcharge à la ligne nº 9 son identificateur avec la macro‑définition d'une pseudo‑constante qui change sa valeur.

#include  <stdio.h>

const float TVA = 0.055; // %

int main(void)
{
  printf("(taux de TVA appliqué %.1f %%)\n", TVA * 100);

#define     TVA   0.2    // %

  printf("(taux de TVA appliqué %.1f %%)\n", TVA * 100);
  return 0;
}

La chaîne de compilation opère sans avertissement et l'exécution du programme se déroule comme pour l'exemple précédent, ainsi qu'on peut s'y attendre. Dans le fichier prétraité généré par la commande cpp, on obtient :

const float TVA = 0.055;

int main(void)
{
  printf("(taux de TVA appliqué %.1f %%)\n", TVA * 100);



  printf("(taux de TVA appliqué %.1f %%)\n", 0.2 * 100);
  return 0;
}

On voit donc que la déclaration de la constante globale TVA est intacte, puisqu'elle est antérieure à la macro‑définition. Mais après l'emplacement où figurait la macro (ligne nº 733), cette déclaration devient inopérante : l'identificateur surchargé a été remplacé par la constante littérale 0.2.

Remarque. En revanche, si on avait codé la macro‑définition de TVA avant la déclaration de la constante globale du même nom, cette déclaration serait devenue non compilable après prétraitement par le préprocesseur :
const float 0.2 = 0.055;

De plus, contrairement à ce que pourrait laisser penser la remarque supra, on peut aussi surcharger une déclaration ultérieure de l'identificateur d'une donnée par une macro‑définition, mais à condition que la chaîne de substitution de cette dernière soit elle‑même un identificateur (il ne s'agit pas alors d'une véritable pseudo‑constante).

Un tel procédé revient donc seulement à changer l'identificateur de cette déclaration.

Dans le programme académique ci‑dessous, avant la déclaration à la ligne nº 5 de la constante globale TVA, on surcharge à la ligne nº 3 son identificateur avec une macro‑définition qui le transforme en VAT (sigle anglais signifiant value added tax).

#include  <stdio.h>

#define     TVA   VAT

const float TVA = 0.055; // %

int main(void)
{
  printf("(taux de TVA appliqué %.1f %%)\n", TVA * 100);
  return 0;
}

Là encore, la chaîne de compilation opère sans avertissement et on obtient une exécution normale du programme. Dans le fichier prétraité généré par la commande cpp, on obtient :

const float VAT = 0.055; 

int main(void)
{
  printf("(taux de TVA appliqué %.1f %%)\n", VAT * 100);
  return 0;
}

Remarque. Dans cet exemple, la surcharge n'a pas vraiment d'utilité dans la mesure où, a priori, on a aucune raison de générer le fichier prétraité, sauf pour vérifier le travail accompli par le préprocesseur.

Contraintes d'antériorité

Rappelons qu'en règle générale, en langages C et C++, un identificateur peut être invoqué dans un code source seulement après avoir été déclaré (cf. chap. C2‑III ) – aussi bien s'il s'agit d'un identificateur de donnée ou de fonction.

Mais lorsque l'on compose la chaîne de substitution d'une macro‑définition, cette contrainte d'antériorité ne s'applique pas toujours – et même assez rarement. Pour le comprendre, il faut analyser la façon dont le préprocesseur opère l'expansion des macros, et ce en amont du compilateur. On s'y attachera dans les exemples ci‑après.

Néanmoins, il est quand même préférable de coder les macro‑définitions comme s'il s'agissait de déclarations, en respectant la règle usuelle d'antériorité.

Dans le programme académique ci‑dessous, la chaîne de substitution de la macro‑définition codée en ligne nº 3 invoque l'identificateur TVA qui n'est pas encore défini (il l'est à la ligne suivante).

#include  <stdio.h>

#define     TVA_100  (TVA * 100)
#define     TVA       0.055 

int main(void)
{
  printf("(taux de TVA appliqué %.1f %%)\n", TVA_100);
  return 0;
}

Pourtant la compilation s'effectue sans avertissement et on obtient une exécution normale. Dans le fichier prétraité généré par la commande cpp, on obtient :

int main(void)
{
  printf("(taux de TVA appliqué %.1f %%)\n", (0.055 * 100));
  return 0;
}

Comme le préprocesseur a‑t‑il opéré ?

  • Il a prétraité la première directive #define en scannant le fichier source. Il a trouvé la ligne nº 7 une occurrence de l'identificateur TVA_100 qu'il a remplacée par (TVA * 100).
  • Puis il a prétraité la deuxième directive #define en scannant le code qu'il a déjà prétraité. Il a donc trouvé une occurrence de l'identificateur TVA qu'il a remplacée par 0.055.

Et il en aurait fait de même pour toute autre occurrence de la pseudoTVA_100 codée après sa macro‑définition.

Dans l'exemple précédent :

  1. Si on avait codé les deux macro‑définitions dans l'ordre inverse :
  2. #define     TVA       0.055 
    #define     TVA_100  (TVA * 100)
    
    on aurait obtenu le même fichier prétraité. En effet :
    • le préprocesseur aurait remplacé par 0.055 l'occurrence de l'identificateur TVA dans la chaîne de substitution de la macro‑définition de TVA_100, laquelle serait devenue (0.055 * 100) ;
    • puis il aurait remplacé l'occurrence de TVA_100 en ligne nº 7 par cette nouvelle de substitution modifiée.
  3. Si on avait déclaré TVA comme une « vraie » constante avant ou après la macro‑définition de TVA_100, par exemple ainsi :
  4. #define     TVA_100  (TVA * 100)
    const float TVA = 0.055;
    
    on aurait obtenu un fichier prétraité différent :
    const float TVA = 0.055;
    
    int main(void)
    {
      printf("(taux de TVA appliqué %.1f %%)\n", (TVA * 100));
      return 0;
    }
    
    mais compilable avec une exécution satisfaisante.
  5. En revanche, si l'on avait déclaré TVA_100 comme une « vraie » constante en premier :
  6. const float TVA_100 = TVA * 100;
    #define     TVA       0.055 
    
    on aurait obtenu un fichier prétraité non compilable :
    const float TVA_100 = TVA * 100;
    
    
    int main(void)
    {
      printf("(taux de TVA appliqué %.1f %%)\n", TVA_100);
      return 0;
    }
    
    puisque dans sa déclaration, l'affectation sur TVA_100 invoque l'identificateur TVA qui n'est pas déclaré ! En effet, l'expansion de sa macro‑définition dans le fichier source n'a pas eu lieu dans cette déclaration antérieure. Il aurait donc fallu coder la coder après la macro‑définition de la pseudo‑constante TVA.
    Dans notre exemple, c'est donc le seul cas où une contrainte d'antériorité s'impose.

Règles de parenthésage

On rappelle (cf. supra ) que lors de l'expansion d'une macro‑définition, le préprocesseur compose des expressions par substitution de son identificateur et de ses éventuels arguments, mais sans faire de calculs d'évaluation des expressions produites par substitution. C'est seulement lors de la compilation ou de l'exécution que les expressions ainsi composées seront évaluées.

En règle générale, pour garantir une évaluation correcte – et ce quels que soient les rangs de priorité des opérateurs mis en jeu dans les expressions invoquant la macro‑définition – il est vivement recommandé d'encapsuler dans des parenthèses :

  • sa chaîne de substitution toute entière ;
  • et dans cette dernière, s'il s'agit d'une pseudo‑fonction, chaque occurrence de chaque argument.

On verra plus loin  que cette règle ne s'applique pas forcément dans certains cas particuliers, notamment à une pseudo‑fonction dont la chaîne de substitution code plusieurs instructions.

Dans le programme académique ci‑dessous de calcul du prix d'une baguette de pain avec et sans TVA :

#include <stdio.h>

#define TVA          0.055 // %
#define TVA_100     (100 * TVA)
#define FACTEUR_TVA (1 + TVA)

const float PRIX_BAGUETTE_HT  = 1.14; // euros

int main(void)
{
  printf("Baguette HT  : %.2f euros\n", PRIX_BAGUETTE_HT);
  printf("Baguette TTC : %.2f euros\n", PRIX_BAGUETTE_HT * FACTEUR_TVA);
  printf("(taux de TVA appliqué %.1f %%)\n", TVA_100);
  return 0;
}

Avec les parenthèses encapsulant les chaînes de substitution des macro‑définition de TVA_100 et FACTEUR_TVA, ce programme est codé correctement. En effet, dans le fichier prétraité engendré par la commande cpp, la fonction main devient :

int main(void)
{
  printf("Baguette HT  : %.2f euros\n", PRIX_BAGUETTE_HT);
  printf("Baguette TTC : %.2f euros\n", PRIX_BAGUETTE_HT * (1 + 0.055));
  printf("(taux de TVA appliqué %.1f %%)\n", (100 * 0.055));
  return 0;
}

où l'expression PRIX_BAGUETTE_HT * (1 + 0.055) sera ensuite évaluée comme attendu 1,44 × 1,055 par le compilateur, grâce aux parenthèses encapsulant l'addition.

En revanche, si à la ligne nº 5 du fichier source on avait codé la macro‑définition comme ci‑dessous, avec sa chaîne de substitution sans les parenthèses :

#define FACTEUR_TVA  1 + TVA

alors on aurait obtenu dans le fichier prétraité :

  printf("Baguette TTC : %.2f euros\n", PRIX_BAGUETTE_HT * 1 + 0.055);
}

où l'expression PRIX_BAGUETTE_HT * 1 + 0.055 serait ensuite évaluée 1,44 + 0,055 par le compilateur – valeur non conforme à l'objectif du programme.

  1. On peut noter que le non respect de la règle de parenthésage ne provoque pas systématiquement des dysfonctionnements : tout dépend des opérateurs mis en jeu. Dans l'exemple supra, si à la ligne nº 4 du fichier source, la macro‑définition de TVA_100 était codée sans parenthèses, cela ne poserait pas de problème dans le programme puisque cette macro‑définition est invoquée seule dans une expression.
  2. Le respect de la règle de parenthésage n'en reste pas moins vivement recommandée par principe de précaution, dans l'hypothèse où le programme serait par la suite développé en faisant d'autres usages de cette macro, potentiellement problématiques sans parenthésage.
  3. Contrairement au compilateur, le préprocesseur ne procède aucune vérification de l'équilibre des paires de délimiteurs (parenthèses, crochets, accolades) dans la chaîne de substitution d'une macro‑définition. Cela permet de définir des objets « composites » qui se complètent après expansion pour former un code compilable.

Macro‑définition d'une pseudo‑constante

Notion détaillée de pseudo‑constante

Une macro‑définition introduit une pseudo‑constante lorsque :

  • son expression d'identification est sans argument – c'est un simple identificateur ;
  • sa chaîne de substitution est telle que son expansion aboutit systématiquement à une expression constante (cf. chap. C2‑II ).

Même si aucune règle syntaxique ne s'y oppose, la notion de pseudo‑constante exclut l'emploi de tout identificateur de variable dans sa chaîne de substitution. En effet, cela créerait sinon une expression a priori non constante, ce qui n'est évidemment pas l'objectif.

De plus, il est déconseillé d'employer dans cette chaîne de substitution un identificateur de constante car sinon :

  • l'expansion de la pseudo‑constante ne serait pas conforme à une expression constante entière (cf. chap. C2‑II ), ce qui est parfois précisément l'objectif de l'emploi d'une pseudo‑constante – cf. infra  ;
  • cela compose un code source hétérogène en termes de niveaux de langage ; il est préférable de rester homogène en n'employant que des constantes littérales, et éventuellement d'autres pseudo‑constantes.

Néanmoins, il peut arriver de devoir déroger à cette règle, en particulier lorsqu'on veut coder une pseudo‑constante dont la valeur dépend de celle d'une constante déclarée dans un fichier d'en‑tête de bibliothèque.

Même en respectant la règle ci‑dessus, une macro‑définition de pseudo‑constante permet de coder des objets plus complexes qu'une simple constante.

On verra au chapitre C5‑III  qu'en langage C ou C++, l'initialisation d'une variable de type tableau peut être effectuée par une liste d'expressions.

Si, dans un programme, une liste particulière était d'usage multiple, il serait souhaitable de la déclarer comme une constante, dans un objectif légitime de factorisation du code. Malheureusement, cela n'est syntaxiquement pas possible.

En revanche, rien n'interdit d'utiliser, comme dans l'exemple académique ci‑dessous, une macro‑définition qui n'est pas une pseudo‑constante puisque sa chaîne d'expansion est une liste (un type d'objet qui n'existe pas en langage C) :

#define  INITIAL_SPEED_VALUES  1.05, 5.001, -3.2

double speedVector[3] = { INITIAL_SPEED_VALUES };

Comparaison avec l'emploi d'une « vraie » constante

Rappelons que l'emploi des constantes déclarées à la place de constantes littérales (ou d'expressions composées avec des constantes littérales) est vivement recommandé pour la lisibilité et la robustesse aux erreurs de codage (cf. chap. C2‑III ).

Le recours à une pseudo‑constante doit partir des mêmes motivations et s'imposer lorsqu'il n'est pas possible de procéder avec une simple constante déclarée.

Lorsqu'on code une macro‑définition avec les restrictions syntaxiques évoquées supra , on parle de « pseudo‑constante » parce que, même si elle permet de représenter par un identificateur une valeur constante comme une « vraie » constante déclarée, elle jouit de propriétés fondamentalement différentes, puisqu'elle procède par expansion dans le code source et non pas par mémorisation d'une donnée lors de l'exécution.

Codage d'expressions constantes entières en langage C

En langage C (mais pas en C++), le principal intérêt de la notion de pseudo‑constante est de surmonter l'obstacle que constitue la contrainte de devoir coder obligatoirement une expression constante entière (cf. chap. C2‑II ) dans certaines formes syntaxiques, en particulier pour :

  • les étiquettes de cas d'une bifurcation multiple switch (cf. chap. C2‑V ) ;
  • les valeurs explicites des constantes entières listées dans la déclaration d'un type énuméré, qu'il soit anonyme ou nommé (cf. chap. C3‑IV ) ;
  • le nombre d'éléments d'un tableau, aussi bien dans une déclaration de type que de donnée (cf. chap. C5‑III ) ;
  • la largeur d'un champ de bits d'une structure hétérogène, aussi bien dans une déclaration de type ou de donnée (cf. chap. C5‑V ).

Dans ces cas répertoriés, plutôt que de devoir coder des expressions à base de constantes littérales uniquement, on peut recourir à des pseudo‑constantes dont l'expansion produira les expressions souhaitées. On parvient ainsi à une factorisation du code comme avec l'usage d'une constante déclarée.

Supposons que l'on veuille mémoriser dans un tableau les 100 premiers nombres premiers. En langage C, il est pertinent de vouloir coder la valeur 100 dans une pseudo‑constante, nommée par exemple NUMBER_OF_PRIMES, car on en aura besoin à la fois pour déclarer le tableau, et pour parcourir ses éléments avec une boucle de répétition for. Le programme pourra donc débuter par le code ci‑dessous :

#include <stdio.h>
#include <stdbool.h>

#define NUMBER_OF_PRIMES 100

bool isNotPrime(int n);

int main(void)
{
  int primes[NUMBER_OF_PRIMES] = {2, 0};
  int testedNumber = 3;
  printf("The %d first prime numbers are: \n", NUMBER_OF_PRIMES);
  printf("%4d", 2);
  for (int i = 1; i < NUMBER_OF_PRIMES; i++) { 
    // ...

Par ailleurs deux autres aspects essentiels peuvent justifier de recourir à une pseudo‑constante plutôt qu'une « vraie » constante déclarée :

  • le coût mémoire, qui est a priori nul pour une pseudo‑constante (à moins que la valeur de cette dernière occupe plus de place qu'une adresse).
  • le typage des valeurs, qui peut être nul ou du moins très faible pour une pseudo‑constante, avec l'avantage que cela apporte en termes de puissance d'expression, mais aussi l'inconvénient en termes de risques d'erreurs de codage.

Mais ces deux aspects sont plus complexes qu'il n'y paraît et nécessitent quelques approfondissements.

Coût mémoire

La macro‑définition d'une pseudo‑constante ne requiert aucun espace réservé en mémoire lors de l'exécution, ni dans les segments de données statiques (.data, .rodata ou .bss), ni dans la pile (cf. chap. C4‑II ). Sa valeur est « disséminée » dans le code exécutable du programme (segment .text) à chaque fois qu'elle est utilisée dans une expression, à la place du pointeur d'adresse qui renverrait à son espace mémoire si elle avait été déclarée comme une « vraie » constante.

En termes d'occupation mémoire, cette stratégie est donc a priori intéressante uniquement la valeur de la pseudo‑constante est encodée sur moins voire autant d'octets qu'un pointeur d'adresse. Mais dans le cas contraire, elle risque fort d'être contre‑productive.

Et en tout état de cause, elle ne se justifie que dans le cadre d'un programme développé pour une machine cible très pauvre en mémoire, typiquement :

  • le microcontrôleur Atmel ATmega328P qui équipe les cartes Arduino Uno et Nano (32 ko pour le programme, 2 ko pour les données – cf. C1‑III ) ;
  • ou plus encore, l'Atmel ATtiny85 (8 ko pour le programme, 512 o pour les données – cf. C1‑III ).

Par ailleurs, il importe de ne pas surestimer l'hypothétique avantage du gain d'espace en mémoire. En effet, les compilateurs des langages C et C++ intègrent depuis longtemps des algorithmes d'optimisation qui peuvent mettre en œuvre le même genre de travail qu'un préprocesseur pour les constantes, c'est‑à‑dire remplacer dans le segment .text les occurrences de l'identificateur de cette constante non pas par son adresse mais par sa valeur. Par conséquent, lorsque l'on déclare une constante globale, cela n'engendre le plus souvent aucune consommation d'espace mémoire dans le segment .rodata.

On peut deviner l'existence d'un tel mécanisme d'optimisation en compilant dans l'IDE Arduino le programme blink ci‑dessous (cf. chap. C2‑IX ) :

#define BLINK_HALF_PERIOD 500              // comment to test the difference
// const uint16_t BLINK_HALF_PERIOD = 500; // uncomment to test the difference

void setup()
{
  pinMode(LED_BUILTIN, OUTPUT);
}

void loop()
{
  digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN));
  delay(BLINK_HALF_PERIOD);
}

En effet, que l'on utilise la ligne nº 1 (macro‑définition d'une pseudo‑constante) ou la ligne nº 2 (déclaration d'une constante) pour mémoriser la valeur de la demi‑période de clignotement, aucune différence n'apparaît dans le résultat de la compilation affiché par Arduino IDE :

Le croquis utilise 960 octets (2%) de l'espace de stockage de programmes. Le maximum est de 32256 octets.
Les variables globales utilisent 9 octets (0%) de mémoire dynamique, ce qui laisse 2039 octets pour les variables locales. Le maximum est de 2048 octets.

sachant que les 9 octets utilisés correspondent à des données cachées, présentes même avec un programme vide.

L'optimisation du code exécutable par le compilateur qui consiste à traiter la déclaration d'une constante par recopie de sa valeur dans le segment .text n'est pas possible si le codeur déclare un pointeur sur cette constante (cf. chap. C5‑I ). Dans ce cas, l'allocation d'un espace mémoire adressable à cette donnée est indispensable.

Mais en termes de comparaison avec l'emploi d'une pseudo‑constante, l'avantage qu'on pourrait avoir à utiliser cette dernière ne tient pas, puisqu'on ne peut pas déclarer un pointeur sur une pseudo‑constante…

Typage des données

Contrairement à ce que pourrait penser un codeur débutant, la macro‑définition d'une pseudo‑constante laisse quand même des possibilités de typage de la valeur attribuée, notamment via :

  • les règles de syntaxe de codage des constantes littérales, et notamment l'emploi de suffixes (cf. chap. C3‑II  et chap. C3‑V ) ;
  • l'opérateur de conversion explicite (cf. chap. C3‑VI ).

Mais en l'absence de typage explicite, une constante littérale est a priori compilée dans le type standard par défaut ou compatible avec sa valeur, et il en va de même pour une expression (cf. chap. C3‑VI ).

Le fait de laisser au codeur le choix de typer ou non la valeur d'une pseudo‑constante apporte une puissance d'expression qui peut être vue comme un avantage mais aussi un inconvénient. En effet, la versatilité du type d'une donnée est potentiellement porteuse d'une ambiguïté.

Dans le code maladroit ci‑dessous :

#define NOMINAL_VOLTAGE  13
#define STARTING_VOLTAGE (NOMINAL_VOLTAGE / 2)

le codeur a oublié de coder les points décimaux « de précaution », notamment à la valeur de la pseudo‑constante NOMINAL_VOLTAGE (il aurait dû coder 13.0). Il en résulte que la pseudo‑constante STARTING_VOLTAGE prend, après expansion des macro‑définitions, la valeur entière 6 et non pas la valeur décimale 6.5 qu'il est logique d'attendre, étant donné ce que cette pseudo‑constante est censée représenter – une tension, donc fondamentalement un nombre décimal.

Une telle erreur serait évitée en recourant à une « vraie » constante déclarée comme par exemple ci‑dessous :

const double NOMINAL_VOLTAGE = 13;
const double STARTING_VOLTAGE = NOMINAL_VOLTAGE / 2;

En effet, même en l'absence des points décimaux dans les constantes littérales 13 et 2, la STARTING_VOLTAGE prend la valeur 6.5 car dans l'expression NOMINAL_VOLTAGE / 2, la constante NOMINAL_VOLTAGE impose son type décimal et conduit le compilateur à une évaluation correcte.

Retour d'expérience sur l'emploi des pseudo‑constantes

Historiquement, l'emploi des macro‑définitions de pseudo‑constantes a toujours été très fréquent dans les programmes codés en langage C. Malgré le peu d'avantages qu'apporte aujourd'hui cette technique compte tenu des mécanismes d'optimisation mis en œuvre par les compilateurs, elle reste appréciée des codeurs en raison de sa commodité, et sans doute aussi parce que les risques qu'elle présente sont sous‑estimés… et que les habitudes ont la vie dure.

En revanche, les spécialistes du C++ sont nettement moins enthousiastes. Ils savent que le développement de ce langage a aussi été l'occasion de corriger les défauts du langage C pour pouvoir se passer des macro‑définitions. Sauf cas particuliers, ils préfèrent de loin la déclaration de « vraies » constantes typées.

Pseudo‑constantes prédéfinies

En langages C et C++, plus d'une centaine de pseudo‑constantes sont prédéfinies pour permettre au codeur de récupérer des valeurs de variables d'environnement. Leur identificateur est presque toujours de la forme __NOM-EN-MAJUSCULES__.

Ces pseudo‑constantes ne font pas explicitement partie du noyau du langage puisque leur valeur dépend du contexte, mais elles sont néanmoins utilisables sans requérir une directive d'inclusion de fichiers de bibliothèque. Elles sont incluses par le préprocesseur dans le code source via des fichiers virtuels (cf. supra ).

Parmi ces pseudo‑constantes prédéfinies, on distingue 3 catégories dites standards, communes et spécifiques.

  1. Parmi les pseudo‑constantes prédéfinies standards aux langages C et C++, on trouve notamment :
    • __STDC_VERSION__ et __cplusplus qui donnent la version du langage employé (on les a déjà présentées au chap. C1‑II ) ;
    • __DATE__ et __TIME__ qui donnent, sous la forme d'une chaîne de caractères formatée, respectivement la date et l'heure de compilation du programme (informations données par le système d'exploitation du poste de travail) ;
    • __FILE__ et __LINE__ qui donnent respectivement le chemin absolu du fichier en cours de compilation sous la forme d'une chaîne de caractères et le numéro de ligne courant dans ce fichier sous la forme d'un entier.
  2. Les pseudo‑constantes prédéfinies communes dépendent de la chaîne de compilation utilisée et de la machine cible. Très nombreuses, elles permettent notamment de caractériser les types de données utilisables.
  3. Par exemple, on trouve la pseudo‑constante __FLT_MIN__ qui donne, pour la machine cible, la plus petite valeur des données de type float. C'est la valeur de cette variable d'environnement qui permet ensuite de définir la pseudo‑constante FLT_MIN dans le fichier d'en‑tête float.h C de la bibliothèque standard du langage C (cf. chap. C3‑V ) :
    #define FLT_MIN                __FLT_MIN__
    
  4. Les pseudo‑constantes prédéfinies spécifiques à la machine cible permettent d'identifier cette dernière, notamment son système d'exploitation pour coder des programmes portables via des directives de compilation conditionnelle (cf. infra ).
  5. Par exemple, la pseudo‑constante __linux__ est prédéfinie et prend la valeur 1 en compilation native sur un poste de travail disposant d'un système d'exploitation Linux.

Sur un poste de travail, on peut obtenir la liste exhaustive de toutes les pseudo‑constantes prédéfinies par le préprocesseur CPP pour un fichier source donné via la commande :

cpp -dM fichier source

le mnémonique de l'option -dM étant « defined macros ».

Macro‑définition d'une pseudo‑fonction

Notion de pseudo‑fonction

Supra , on a vu que la syntaxe de la directive #define accepte la notion d'argument, comme dans une fonction (cf. chap. C4‑I ). On parle alors de pseudo‑fonction dans la mesure où il y quelques différences subtiles avec la notion de « vraie » fonction mais que par ailleurs, on y retrouve de nombreuses similitudes dans les concepts sous‑jacents (arguments formels, effectifs, etc.).

En effet, dans la macro‑définition d'une pseudo‑fonction, par analogie, on peut considérer :

  • que la ligne de contrôle d'une telle directive constitue la définition de la pseudo‑fonction, où :
    • l'expression d'identification joue le rôle d'en‑tête, dans laquelle on retrouve un identificateur suivi d'une liste d'arguments formels, mais sans typage ;
    • la chaîne de substitution joue le rôle de corps de définition de la pseudo‑fonction – avec beaucoup moins de contrainte syntaxiques, bien sûr –, et où l'on peut insérer autant d'occurrences que voulu des arguments formels listés dans l'expression d'identification ;
  • que dans la suite du code, toute occurrence de l'identificateur de la pseudo‑fonction peut alors être associée à des expressions formant ses arguments effectifs – il doit y en avoir autant que d'arguments formels – dans ce qui s'apparente à une expression d'appel – ou d'invocation – de la pseudo‑fonction ;
  • sachant que la transmission des arguments effectifs aux arguments formels ne procède ni par valeur, ni par adresses ou références, mais par substitution.

On adoptera donc ce vocabulaire d'« argument formel » et d'« argument effectif » relatif aux vraies fonctions tout en ayant bien conscience des différences qui existe (absence de typage, transmission par substitution).

Syntaxe de codage d'une macro‑définition de pseudo‑fonction

On code la macro‑définition d'une pseudo‑fonction par une directive #define respectant la forme syntaxique suivante :
#define identificateur(argument formel 1, argument formel 2,  ) chaîne de substitution
où les arguments formels sont simplement des identificateurs sans typage, et dont la « portée » se réduit à la chaîne de substitution de la macro‑définition.

Bien que simple en apparence, cette syntaxe recèle plusieurs subtilités.

Contrairement au codage d'une fonction, une expression d'identification n'est pas à format libre : le codeur ne peut placer aucun espace entre l'identificateur et la parenthèse ouvrante ( de la liste des arguments formels. Sinon, cet espace serait reconnu par le préprocesseur comme le séparateur entre l'expression d'identification et la chaîne de substitution et alors, la liste des arguments formels serait intégrée à cette dernière, générant quasi‑certainement un code non compilable !

En revanche, comme pour une fonction, la liste des arguments formels peut éventuellement être vide, c'est‑à‑dire codée de la forme () si la pseudo‑fonction n'emploie pas d'arguments.

Mêmes vides, les parenthèses joue un rôle syntaxique déterminant : elles permettent au préprocesseur d'identifier une macro‑définition de pseudo‑fonction et non pas de pseudo‑constante. On verra infra  que les règles d'expansion ne sont pas les mêmes pour ces deux catégories de macro‑définitions.

Quant à la chaîne de substitution, elle ne répond à aucune contrainte syntaxique en matière de délimiteurs (accolades non obligatoires, parenthésage pas forcément équilibrés, etc.). Mais en règle générale, on encapsule cette chaîne de substitution :

  • entre parenthèses ( ) si la pseudo‑fonction est codée dans la perspective d'être invoquée au sein d'expressions (comme une « vraie » fonction qui retourne une valeur), en vertu des mêmes règles que pour une pseudo‑constante (cf. supra ) ;
  • de plus, il est vivement recommandé d'encapsuler entre parenthèses chaque occurrence de chaque argument formel, dans la mesure où son argument effectif correspondant peut éventuellement être une expression composée d'opérateurs qui, après expansion, devra être évaluée avant que les opérateurs codés dans la chaîne de substitution n'interviennent ;
  • dans une structure de contrôle do { } while (0) (cf. chap. C2‑V , mais attention, sans séparateur ; à la fin) si la pseudo‑fonction est codée dans la perspective d'être invoquée seule dans une instruction, comme une fonction de type void.
  • (Cette structure de contrôle garantit l'exécution de son bloc une et une seule fois. Elle permet d'éviter la présence indésirable d'une instruction vide à chaque fin d'expansion de la macro – cf. la section Swallowing the semicolon de la notice de CPP .)

En dehors de ces deux emplois usuels, et notamment si la pseudo‑fonction est codée dans la perspective d'être invoquée dans une déclaration, aucune règle générale ne se dégage : c'est au cas par cas qu'on doit s'assurer de la validité du code généré après l'expansion de sa macro‑définition.

  1. Dans le fichier d'en‑tête Arduino.h  pour cartes à cœur AVR, on trouve la macro‑définition :
  2. #define sq(x) ((x)*(x))
    
    Cette directive définit une pseudo‑fonction nommée sq (abréviation de l'anglais square) :
    • qui admet un argument formel nommé x ;
    • dont la chaîne de substitution ((x)*(x)) utilise deux occurrences de son argument (elle consiste en la multiplication de x par x) ;
    Elle est codée dans la perspective d'être invoquée au sein d'expressions, d'où l'encapsulation globale entre parenthèses ainsi que l'encapsulation individuelle de chacune des deux occurrences de l'argument formel.

    Remarque. Cette façon simple d'élever au carré son argument présente l'avantage de ne pas recourir à une fonction puissance de la bibliothèque standard, mais présente aussi un inconvénient en termes d'effet de bord (cf. infra )

  3. Pour implémenter une procédure de permutation des valeurs de deux variables, la macro‑définition de la pseudo‑fonction swapValues codée ci‑dessous peut constituer une bonne alternative à la solution proposée au chapitre C4‑I  :
  4. #define swapValues(a, b) do { \
      double c = a; \
      a = b, b = c; \
    } while (0) 
    
    Elle est codée dans la perspective d'être invoquée seule dans une instruction, d'où l'encapsulation dans la structure de contrôle do { } while (0).
    Remarquons qu'ici, les occurrences des arguments formels a et b ne sont pas encapsulés entre parenthèses. En effet, les opérateurs utilisés ici (= et  ,) ont les rangs de priorité les plus bas (respectivement 14 et 15 – cf. chap. C2‑IV ) et dans une expression d'invocation de la pseudo‑fonction, les arguments effectifs ne peuvent être qu'atomiques ou composés d'opérateurs à très hauts rangs de priorité (typiquement, l'opérateur de déréférencement * – cf. chap. C5‑I ).

Invocation d'une macro‑définition de pseudo‑fonction

Après sa macro‑définition, une pseudo‑fonction peut être invoquée dans le code source, à chaque fois que nécessaire, par une expression de la forme :
identificateur (argument effectif 1, argument effectif 2,  )
où les arguments effectifs sont des expressions correspondant aux arguments formels codés dans l'expression d'identification de la macro‑définition, dans l'ordre de leurs positions respectives.

Contrairement une expression d'identification, une expression d'invocation est à format libre : elle peut notamment être codée avec un voire des séparateurs blancs entre l'identificateur et la liste d'arguments effectifs.

À chaque invocation, le préprocesseur effectue l'expansion de la macro‑définition en remplaçant l'expression par sa chaîne de substitution, dans laquelle chaque occurrence d'un argument formel est préalablement remplacée par son argument effectif correspondant.

Il doit y avoir impérativement le même nombre d'arguments effectifs dans l'expression d'invocation que d'arguments formels dans l'expression d'identification de la macro‑définition, faute de quoi le préprocesseur signale une erreur bloquante pour la chaîne de compilation.

Néanmoins, dans une expression d'invocation, il reste possible de coder un voire plusieurs arguments effectifs vides, du moment que occurrences attendues des séparateurs , sont saisies. Mais cette permissivité du préprocesseur ne garantit pas pour autant que le code qu'il génère soit ensuite compilable.

Par ailleurs, si une pseudo‑fonction code dans sa chaîne de substitution une affectation (simple ou composée – cf. chap. C2‑IV ) sur un argument formel, alors son argument effectif correspondant dans une invocation doit impérativement être une l‑value, un peu comme pour une fonction codée en transmission par référence (cf. chap. C4‑I ) mais avec une contrainte de typage moins forte (il suffit que l'affectation soit acceptable par le compilateur).

  1. Dans un fichier source d'un programme pour carte Arduino (où est par défaut inclus le fichier d'en‑tête Arduino.h qui comporte la macro‑définition de la pseudo‑fonction sq présentée supra ) :
    • une expression d'invocation comme par exemple sq(5.0) serait remplacée par le préprocesseur par l'expression :
      ((5.0)*(5.0))
      puis serait par défaut compilée comme étant de type double (cf. chap. C3‑VI ) ;
    • une expression d'invocation comme par exemple sq(2 + 3) serait remplacée par le préprocesseur par l'expression :
      ((2 + 3)*(2 + 3))
      puis serait par défaut compilée comme étant de type int.
    Dans ces deux cas, ces expressions seront évaluées – soit par le compilateur, soit lors de l'exécution – en prenant l'une comme l'autre la valeur 25.
    Remarque : si, dans la chaîne de substitution de sq, les occurrences de l'argument formel x n'étaient pas encapsulées dans des parenthèses, alors l'expansion de sq(2 + 3) donnerait le code :
    (2 + 3*2 + 3)
    et serait ensuite évaluée 11 !
  2. Le programme ci‑dessous invoque la pseudo‑fonction swapValues présentée supra ) deux fois, d'abord avec des arguments effectifs de type float, puis de type int.
  3. #include <stdio.h>
    
    #define swapValues(a, b) do { \
      double c = a; \
      a = b, b = c; \
    } while (0) 
    
    int main(void)
    { 
      float x = 1.0, y = 2.0;
      swapValues(x, y);
      printf("After swap: x = %g   y = %g\n", x, y);
      printf("\n");
    
      int u = 1, v = 2;
      swapValues(u, v);
      printf("After swap: u = %d   v = %d\n", u, v);
      return 0;
    }
    
    Il est intéressant de consulter ce que devient la fonction main le fichier prétraité engendré par la commande cpp :
    int main(void)
    {
      float x = 1.0, y = 2.0;
      do { double c = x; x = y, y = c; } while (0);
      printf("After swap: x = %g   y = %g\n", x, y);
      printf("\n");
    
      int u = 1, v = 2;
      do { double c = u; u = v, v = c; } while (0);
      printf("After swap: u = %d   v = %d\n", u, v);
      return 0;
    }
    
    À la fin des lignes nº 749 & 753, on observe que, conformément à la syntaxe de la boucle do while, un séparateur ; est bien présent pour compléter l'expansion de la macro‑définition de swapValues où ce séparateur final n'est pas codé. Il provient respectivement des fins de lignes nº 11 & 16 du fichier source (il ne fait pas partie des expressions d'invocation de swapValue, donc il est laissé tel quel dans le fichier prétraité).

Dès lors qu'une pseudo‑fonction emploie une donnée locale, comme la variable tampon c déclarée à la ligne nº 4 dans l'exemple 2) supra, sa chaîne de substitution doit être impérativement encapsulée dans un bloc. Sinon, la deuxième invocation de cette pseudo‑fonction dans un même bloc va inévitablement produire un code non compilable après expansion, puisque le même identificateur de donnée sera déclaré deux fois.

C'est exactement ce qui se passerait si on codait la pseudo‑fonction swapValues comme ci‑dessous, pour tenter maladroitement d'éviter la structure do { } while (0) :

#define swapValues(a, b) \
  double c = a; \
  a = b, b = c   // macro badly coded without braces! 

Dans le bloc de la fonction main du programme précédent, on aurait deux déclarations de la même variable double c = ce que le compilateur ne manquerait pas de signaler comme une erreur.

Particularités d'expansion d'une macro‑définition de pseudo‑fonction

On a évoqué supra  le fait que les règles d'expansion ne sont pas les mêmes pour une pseudo‑fonction que pour une pseudo‑constante.

En effet, une pseudo‑fonction est un objet potentiellement beaucoup plus complexe qu'une pseudo‑constante.

Tout d'abord, il y a le fait qu'une pseudo‑fonction admet des arguments formels qui eux‑mêmes font l'objet d'expansion par leurs arguments effectifs correspondants.

De plus, sauf en cas d'emploi d'opérateurs spéciaux, si les arguments effectifs sont eux‑même composés avec d'autres macro‑définitions, l'expansion de ces dernières est traitée par le préprocesseur avant celle des arguments formels.

Ensuite, les pseudo‑fonctions bénéficient de deux opérateurs spéciaux, codés respectivement par # et ##, qui répondent à des règles d'expansion différentes.

Invocation d'une pseudo‑fonction sans expansion

Une expression d'invocation de pseudo‑fonction fait l'objet d'une expansion seulement si son identificateur est suivi d'une liste d'arguments effectifs encapsulée entre parenthèses ( ), sachant que la liste peut éventuellement être vide et séparée de l'identificateur par des espaces.

En revanche, l'absence de liste entre parenthèses codée après l'identificateur conduit le préprocesseur à transmettre ce dernier tel quel au compilateur, c'est‑à‑dire sans expansion.

Bien évidemment, pour qu'une telle invocation sans expansion soit ensuite compilable, il faut que cet identificateur fasse par ailleurs l'objet d'une déclaration. Typiquement, on déclare (et on définit) une fonction homonyme, en surchargeant l'identificateur de la pseudo‑fonction (cf. supra ). Dès lors, on peut :

  • soit invoquer cette pseudo‑fonction par son identificateur suivi de parenthèses ; le préprocesseur procède alors à l'expansion de la macro‑définition et de ses éventuels arguments comme dans tous les exemples présentés jusqu'ici ;
  • soit appeler sa fonction homonyme par son identificateur seul qui, laissé tel quel par le préprocesseur, est compilé en pointeur de fonction (cf. chap. C5‑II ).

Opérateur de conversion en chaîne de caractères d'un argument

Rappelons qu'il n'y a pas d'expansion de l'occurrence d'un identificateur de macro‑définition codé au sein d'une chaîne de caractères . Dans la chaîne de substitution d'une pseudo‑fonction, il en va de même pour un identificateur d'argument formel : si ce dernier est codé dans une chaîne de caractères, il ne sera pas reconnu en qualité d'identificateur par le préprocesseur, mais seulement comme une portion de cette chaîne.

En conséquence, il n'est pas possible de coder comme ci‑dessous la macro‑définition d'une pseudo‑fonction qu'on nommerait par exemple str et dont le rôle serait de créer une chaîne de caractères à partir d'un argument :
#define str(s) "s" // do not work!
parce que dans sa chaîne de substitution "s", l'occurrence du caractère s n'est pas reconnue comme l'argument formel s de la pseudo‑fonction. Autrement dit, l'expansion de cette macro‑définition résulterait invariablement en la chaîne de caractères "s" quel quel soit la valeur de l'argument effectif correspondant à s dans une expression d'invocation de cette pseudo‑fonction.

Pour qu'une pseudo‑fonction puisse opérer sur une chaîne de caractère, le préprocesseur met justement à disposition du codeur l'opérateur unaire # – en anglais stringizing operator, difficile à traduire littéralement en français.

Codé devant l'occurrence d'un identificateur d'argument formel dans la chaîne de substitution d'une pseudo‑fonction, il conduit le préprocesseur à lui substituer son argument effectif correspondant dans une expression d'invocation, encapsulé dans une paire de délimiteurs " ", autrement dit comme une chaîne de caractère.

  1. Avec l'opérateur « stringizing », la pseudo‑fonction str évoquée ci‑dessus se code tout simplement :
    #define str(s) #s // OK!
  2. Reprenons le programme d'essai de la pseudo‑fonction swapValue proposé supra . Grâce à l'opérateur « stringizing », plutôt que de répéter l'instruction d'affichage du résultat dans le corps de la fonction main, on peut la coder dans la chaîne de substitution de la pseudo‑fonction, comme ci‑dessous à la ligne nº 6 :
  3. #include <stdio.h>
    
    #define swapValues(a, b) do { \
      double c = a; \
      a = b, b = c; \
      printf("After swap: %s = %g   %s = %g\n", #a, (double) a, #b, (double) b); \
    } while (0) 
    
    int main(void)
    { 
      float x = 1.0, y = 2.0;
      swapValues(x, y);
      printf("\n");
    
      int u = 1, v = 2;
      swapValues(u, v);
      return 0;
    }
    
    Dans la chaîne de format "After swap" de l'appel de la fonction printf, les identificateurs des deux variables à permuter sont introduits par des spécifications de conversion %s de chaînes de caractères avec pour arguments respectifs #a et #b codés après la chaîne de format.
    Remarque : dans cette même ligne, on recourt à la conversion explicite (cf. chap. C3‑VI ) dans le type double des arguments a et b pour que leurs valeurs soient systématiquement rendues compatibles avec les spécifications de conversion %g.

Opérateur de concaténation de tokens

Rappelons qu'à partir du code source, le préprocesseur transmet au compilateur un flux de tokens de quatre catégories possibles : identificateurs, nombres, chaînes de caractères et ponctuateurs .

Dans la chaîne de substitution d'une pseudo‑fonction, le codeur dispose de l'opérateur binaire ## dit de concaténation W. Son rôle consiste, dans la mesure du possible, à « assembler » les deux tokens identifiés respectivement à sa gauche et à sa droite pour n'en former qu'un seul, sachant que :

  • les éventuels séparateurs blancs intercalaires codés entre l'opérateur et les deux tokens initiaux sont tous supprimés par le processus de concaténation ;
  • si la concaténation ne forme pas un token d'une des quatre catégories admises, le préprocesseur signale une erreur qui bloque la chaîne de compilation.

L'emploi de l'opérateur de concaténation ## n'est pertinent que si au moins l'un des deux tokens est constitué par l'argument formel de la macro‑définition. Sinon la concaténation est statique et peut être codée directement sans cet opérateur.

Avec un argument, on peut notamment composer dynamiquement un identificateur (de donnée, de fonction…), aussi bien dans une déclaration que dans une instruction. Une telle possibilité n'est réalisable avec aucune autre technique de codage en langage C.

Et il y a beaucoup d'autres applications. Celle illustrée dans l'exemple ci‑dessous ne donne qu'un tout petit aperçu de la variété d'emplois de l'opérateur de concaténation dans la pratique.

Supposons que l'on veuille facilement convertir toutes les constantes littérales décimales d'un programme dans l'un des trois types standards d'encodage float (suffixe f ou F), double (type par défaut, pas de suffixe) ou long double l ou L) – cf. chap. C3‑V .

Il serait difficile d'effectuer une telle manipulation avec un éditeur de code par une procédure du type rechercher/remplacer. En effet, d'une part les constantes littérales sont potentiellement toutes différentes et d'autre part, une fois codés, les suffixes ne sont pas des symboles isolés. Ils peuvent être confondus avec d'autres usage de leur lettre.

Une solution consiste par exemple à coder, comme ci‑dessous à la ligne nº 3 la pseudo‑fonction fps (pour floating point suffix) qui concatène le suffixe voulu à droite de son argument :

#include <stdio.h>

#define fps(x) x ## F

int main(void)
{
  printf("Pi = %.8f\n", fps(3.141592653));
  return 0;
}

Il faut alors invoquer cette pseudo‑fonction à chaque emploi d'une constante littérale décimale. Ensuite, pour changer le type d'encodage de toutes les constantes dans le programme, il n'y a plus qu'à modifier la chaîne de substitution dans la définition de fps (pour le type double qui n'a pas de suffixe, on code simplement x, sans opérateur de concaténation).

Pseudo‑fonction d'expansion préliminaire

Considérons une pseudo‑fonction qui applique dans sa chaîne de substitution l'opérateur # à une occurrence d'un arguments formel qu'elle possède. Si dans une invocation d'une telle pseudo‑fonction, l'argument effectif correspondant est composé avec un identificateur de macro‑définition, cette dernière ne sera pas remplacée par sa chaîne de substitution, mais directement traitée comme une suite de caractères à encapsuler dans des délimiteurs " " et ensuite, aucune expansion n'y sera effectuée.

Il en va de même pour les identificateurs situés immédiatement avant et après l'opérateur ## dans la chaîne de substitution d'une macro‑définition. Ils ne font jamais l'objet d'une expansion préalable au traitement de l'opération de concaténation.

  1. Le programme académique ci‑dessous est codé pour afficher une approximation du nombre π sous la forme d'une chaîne de caractères :
  2. #include <stdio.h>
    
    #define PI 3.1415926535
    #define str(s) #s
    
    int main(void)
    { 
      printf("Pi = %s\n", str(PI)); // do not work! PI is not expanded :(
      return 0;
    }
    
    Toutefois, il n'est pas opérationnel : en effet, dans le fichier prétraité généré par la commande cpp, la ligne nº 7 devient :
      printf("Pi = %s\n", "PI"); 
    
    Par conséquent, même si ce code est compilable, son exécution n'est pas satisfaisante puisqu'on obtient en sortie standard Pi = PI.
  3. Le programme académique ci‑dessous codé pour afficher une approximation du nombre π encodé dans le type float :
  4. #include <stdio.h>
    
    #define PI 3.141592653
    #define fps(x) x ## F
    
    int main(void)
    {
      printf("%.8f\n", fps(PI));
      return 0;
    }
    
    n'est pas compilable. En effet, dans le fichier prétraité généré par la commande cpp, la ligne nº 8 devient :
      printf("Pi = %.8f\n", PIF);
    
    alors qu'on aurait attendu que l'expansion de l'expression fps(PI) aboutisse à 3.141592653F.

Pour surmonter ces difficultés, la solution usuelle consiste à coder une pseudo‑fonction d'expansion préliminaire spécifique, dont le rôle est simplement d'invoquer la pseudo‑fonction que l'on souhaite employer pour provoquer l'expansion préalable de ses arguments.

Souvent, on préfixe le nom de cette pseudo‑fonction par la lettre x comme abréviation de « expand ».

  1. Reprenons le programme académique donné à l'exemple 1 supra. Pour résoudre le problème d'expansion, il suffit de coder par exemple :
  2. #include <stdio.h>
    
    #define PI 3.141592653
    #define str(s) #s
    #define xstr(s) str(s)
    
    int main(void)
    { 
      printf("Pi = %s\n", xstr(PI));
      return 0;
    }
    
    et l'exécution devient satisfaisante. En effet, la pseudo‑fonction xstr n'emploie pas l'opérateur #, donc le préprocesseur procède à son expansion en commençant par celle de son argument effectif, comme indiqué ci‑dessous :
    xstr(PI)   →   xstr(3.14)   →   str(3.14)   →   #3.14   →   "3.14"
  3. Reprenons le programme académique donné à l'exemple 2 supra. Pour résoudre le problème d'expansion, il suffit de coder par exemple :
  4. #include <stdio.h>
    
    #define PI 3.1415926535
    #define fps(x) x ## F
    #define xfps(x) fps(x)
    
    int main(void)
    {
      printf("Pi = %.8f\n", xfps(PI));
      return 0;
    }
    
    et l'exécution devient satisfaisante. L'expansion se déroule de façon similaire à celle de l'exemple précédent :
    xfps(PI)   →   xfps(3.14)   →   fps(3.14)   →   3.14 ## F   →   3.14F

Typage des arguments d'une pseudo‑fonction

Comme pour une pseudo‑constante (cf. supra ), le codeur dispose d'une grande liberté de typage des expressions codées par la chaîne de substitution. Aucune contrainte syntaxique ne s'imposant a priori, on peut employer des conversions explicites et des suffixes associés aux constantes littérales pour modifier les types déterminés implicitement par le compilateur.

Il en va de même pour un argument formel qui accepte a priori n'importe quels types d'arguments effectifs tant que leur substitution produit des expressions compilables. On peut donc plus facilement coder une pseudo‑fonction polyvalente que s'il s'agissait d'une vraie fonction.

Mais comme pour une pseudo‑constante, cette liberté de typage présente des risques en termes d'erreurs de conception qui peuvent ensuite engendrer des dysfonctionnements inopinés du programme. En effet, il est difficile de tester toutes les possibilités d'exécution quand les valeurs potentiellement prises par des arguments sont aussi variées.

La pseudo‑fonction sq du framework Arduino opère donc aussi bien sur un argument effectif de type entier ou décimal et produit un résultat dans le type par défaut correspondant. Ainsi, après expansion :

  • l'expression substituée à sq(5) est de type int par défaut ;
  • l'expression substituée à sq(5.0) est de type double par défaut ;
  • l'expression substituée à sq(5.0F) est de type float grâce au suffixe F codé dans l'argument effectif.

Mais on pourrait très bien lui imposer systématiquement le type float par défaut en codant cette pseudo‑fonction comme ci‑dessous :

#define sq(x) ((float)(x)*(x))

Argument descripteur de type

Avec une pseudo‑fonction, grâce au procédé de substitution, il est également possible de coder un argument qui ne correspond pas à une données. En particulier, il peut très bien s'agir d'un descripteur de type.

La pseudo‑fonction sq pourrait ainsi être codée avec un deuxième argument formel nommé Type pour imposer une conversion explicite de l'expression à substituer, comme dans la macro‑définition ci‑dessous :

#define sq(x, Type) ((Type)((x)*(x)))

Dans une expression d'invocation de cette pseudo‑fonction, le deuxième argument effectif coderait alors un type particulier, par exemple :
sq(5.0F, float)
pour que l'expansion génère une conversion explicite dans le type float.

Recours à l'opérateur conditionnel ? :

En règle générale, le codage d'une pseudo‑fonction ne souffre a priori d'aucune limitation de longueur de code. De plus, grâce aux sauts de lignes « fictifs », il est possible d'étendre la chaîne de substitution sur autant de lignes que nécessaires pour former n'importe quelle séquence d'instructions – y compris avec des structures de contrôle – avec une présentation aussi lisible que celle des instructions usuelles.

Toutefois, les règles de bonnes pratiques veulent que l'on réserve le recours aux macro‑définition pour coder des petites routines dont la chaîne de substitution se limite si possible à une expression, suffisamment simple pour ne poser a priori aucun problème, ni de fonctionnement, ni de compréhension.

Dans cette perspective, il est quand même utile de pouvoir coder des conditions. Notamment pour cela, les langages C et C++ disposent de l'opérateur conditionnel (déjà mentionné au chap. C2‑V ). Il permet justement de composer une expression conditionnelle avec une syntaxe de la forme :
condition ? affirmative : négative
condition, affirmative et négative sont également trois expressions.

Lors de son évaluation, cette expression conditionnelle prend :

  • la valeur de l'expression affirmative si l'expression condition est évaluée vraie ;
  • la valeur de l'expression négative si l'expression condition est évaluée fausse.

En d'autres termes, l'expression conditionnelle prend la même valeur que celle que retournerait une « fonction équivalente » dont le corps de définition serait codé de la forme :

if (condition) return affirmative;
else return négative;

La plupart des fonctions mathématiques (cf. chap. C2‑IV ) définies dans le fichier d'en‑tête Arduino.h G pour cartes à cœur AVR sont en fait des pseudo‑fonctions codées à l'aide de l'opérateur conditionnel :

#define min(a,b) ((a)<(b)?(a):(b))
#define max(a,b) ((a)>(b)?(a):(b))
#define abs(x) ((x)>0?(x):-(x))
#define constrain(amt,low,high) ((amt)<(low)?(low):((amt)>(high)?(high):(amt)))
#define round(x)     ((x)>=0?(long)((x)+0.5):(long)((x)-0.5))

À titre d'exemple emblématique, détaillons le code de la pseudo‑fonction min.

  • Sa chaîne de substitution est une expression qui code le minimum (le plus petit) de ses deux arguments formels.
  • Une invocation de cette pseudo‑fonction comme par exemple min(5, 2) sera remplacée par le préprocesseur par ((5)<(2) ? (5) : (2)).
  • Et comme la condition (5)<(2) est fausse, cette expression prendra la valeur de la négative (2), c'est‑à‑dire la valeur entière 2 qui est bien le plus petit des deux arguments effectifs codés dans l'invocation.

Remarque. L'opérateur conditionnel ? : peut, comme tous les opérateurs, être composé avec d'autres opérateurs, en particulier avec lui‑même (cf. la pseudo‑fonction constrain codée ci‑dessus, à la ligne nº 95).

Attention aux pseudo‑fonctions non‑sûres !

On l'a vu supra, le procédé de substitution par lequel une pseudo‑fonction « transmet la valeur » d'un argument effectif à son argument formel correspondant est très commode (il n'y a pas de contrainte de typage sur l'argument) et très puissant (l'argument ne correspond pas forcément à une donnée).

Mais le procédé de substitution présente néanmoins un inconvénient majeur dont les codeurs débutants n'ont pas forcément conscience, lorsque la chaîne de substitution d'une pseudo‑fonction comporte plusieurs occurrences d'un même argument formel.

En effet, dans une invocation, l'expression de l'argument effectif correspondant sera évaluée autant de fois, ce qui peut poser un problème si cette expression emploie un opérateur à affectation composée (cf. chap. C2‑IV ) ou plus généralement une fonction à effet de bord (cf. chap. C2‑IV ) ! On parle de macro‑définition non sûre (en anglais, unsafe macro ).

Les risques de dysfonctionnement sont élevés, même pour un codeur expérimenté, s'il emploie sans le savoir des pseudo‑fonctions non‑sûres (parce qu'il ne les a pas lui‑même développées et qu'il n'a pas pris le temps de lire leur code). Il est donc impératif qu'elles soient signalées comme telles dans leur documentation.

Ainsi, toutes les pseudo‑fonctions mathématiques Arduino présentées supra , qui sont autant d'exemples de macro‑définitions non‑sûres, font l'objet d’un avertissement sur leur page de référence – cf. à titre d'exemple emblématique, le cas de la pseudo‑fonction min A.

Pour illustrer les risques de dysfonctionnement, considérons le programme académique ci‑dessous qui tente maladroitement d'afficher le carré d'un nombre incrémenté puis la valeur de ce dernier. À la ligne nº 8, la pseudo‑fonction sq non‑sûre est composée avec l'opérateur à effet de bord ++ :

#include <stdio.h>

#define sq(x) ((x)*(x))    // warning: unsafe macro!

int main(void)
{
  int a = 2;
  printf("%d\n", sq(a++)); // 6!!
  printf("%d\n", a);       // 4!
  return 0;
}  

À l'exécution, on obtient en sortie standard successivement l'affichage de :

  • 6 qui est censée être la valeur de a élevée au carré ;
  • 4 qui est censée être la valeur de a après son incrémentation unitaire.

Et en effet, lors de expansion de la pseudo‑fonction sq par le préprocesseur, l'expression d'invocation sq(a++) devient ((a++)*(a++)). Il en résulte que, conformément aux règles de priorité et de sens d'associativité des opérateurs (cf. chap. C2‑IV ) :

  • l'évaluation du premier terme (a++) conduit au remplacement de la variable a par sa valeur initiale 2 puis à son incrémentation ;
  • l'évaluation du deuxième terme (a++) conduit au remplacement de la variable a par sa nouvelle valeur 3 puis encore à son incrémentation ;
  • le produit (a++)*(a++) devient donc 2*3 et la variable a vaut alors 4 !

La bonne pratique consiste évidemment à ne pas coder une telle composition mais procéder en deux étapes, comme par exemple ci‑dessous :

int main(void)
{
  int a = 2;
  a++;
  printf("%d\n", sq(a));  // 9
  printf("%d\n", a);      // 3
  return 0;
}  

Remarque. Dans cet exemple :

  • le résultat peut sembler « moins grave » si l'on code l'expression d'invocation sq(++a). La variable a étant incrémentée avant toute évaluation, on obtient successivement les valeurs 16 et 4 ; néanmoins, le problème de la double incrémentation perdure
  • le résultat devient satisfaisant si, en plus de l'amélioration ci‑dessus, à la ligne nº 3, on remplace le code de sq par celui d'une vraie fonction comme ci‑dessous :
  • int sq(int x) {return x*x;}
    
    car on obtient alors les valeurs attendues 9 et 3.

Comparaison avec l'emploi d'une vraie fonction

On a comparé supra  l'emploi d'une pseudo‑constante avec une vraies constante. Il est pertinent de procéder de même entre une pseudo‑fonction et une « vraie » fonction. Encore une fois, plusieurs aspects sont à considérer.

  • Le coût mémoire est cette fois potentiellement bien plus important avec une pseudo‑fonction, puisque sa définition, au lieu d'être stockée une seule fois dans le segment .text du code exécutable, y est copiée autant de fois que ce dernier comporte d'invocations de cette pseudo‑fonction.
  • La vitesse d'exécution est, en revanche, a priori meilleure puisque qu'il n'est pas nécessaire à chaque appel de la pseudo‑fonction de procéder à l'allocation d'un espace mémoire dans la pile avant d'exécuter ses instructions.
  • Quant à la souplesse de typage des arguments, elle peut être vue comme un avantage en termes de polyvalence du code à tous types d'arguments, mais un inconvénient en termes de vulnérabilité aux erreurs qui pourraient être détectées dès la compilation par un typage plus strict.
  • Enfin, la polyvalence du code peut trouver, dans le cas d'une pseudo‑fonction, une limite d'emploi pour des arguments à effets de bord — limite qui n'existe pas avec une vraie fonction. Il faut alors le plus souvent utiliser une variable intermédiaire pour dissocier l'opération à effet de bord de l'appel de la pseudo‑fonction.

Retour d'expérience sur l'emploi des pseudo‑fonctions

En langage C — et même parfois en C++, comme par exemple dans le framework Arduino — surtout au regard des avantages en termes de souplesse de typage, les pseudo‑fonctions restent très utilisées. Tant que le code de ces dernières reste court et simple, et tant que les utilisateurs sont conscients de la limite d'emploi avec des arguments à effet de bord, cette pratique ne pose a priori pas de problème.

Cependant, comme pour les pseudo‑constantes, les spécialistes du C++ sont en général beaucoup plus réticents à utiliser les pseudo‑fonctions. D'ailleurs, on a vu que ce langage met à disposition du codeur d'autres concepts pour produire des fonctions polyvalentes : arguments optionnels, surcharge d'identificateur – cf. chap. C4‑I .

De plus, il existe aussi un moyen pour diminuer le temps d'exécution d'une fonction : l'extension inline (qui fait aussi partie du langage C).

Alternative aux pseudo‑fonctions : l'extension inline

Une fonction dont l'en‑tête de définition est précédée du mot‑clef inline W n'est a priori pas compilée qu'une seule fois dans le segment .text comme l'est usuellement une fonction.

Si le compilateur tient compte de cette extension, il développe le code exécutable de cette fonction à la place de chacun de ses appels, en y transposant à chaque fois les arguments effectifs aux arguments formels correspondants.

Mise en en œuvre par le compilateur et non pas par le préprocesseur, l'extension inline partage avec la notion de macro‑définition :

  • le même avantage en termes de diminution du temps d'exécution,
  • le même inconvénient en termes d'augmentation du volume de stockage du code exécutable.

Toutefois, comme la spécification de classe d'allocation register (cf. chap. C4-II ), l'extension inline n'est pas garantie. La décision est laissé « à l'appréciation du compilateur », c'est‑à‑dire au gré des algorithmes d'optimisation qu'il met en œuvre. Il faut donc aller regarder le code en langage d'assemblage généré par le compilateur pour vérifier au cas par car si l'extension inline est bien mise en œuvre (cf. chap. C4-IV ).

Directives de compilation conditionnelle

Principe

Durant de la conception d'un programme, on est parfois tenté de mettre en commentaire une partie du code source parce que l'on ne souhaite pas, temporairement ou en certaines circonstances, l'inclure dans la production du code exécutable. Mais cette pratique est malcommode, ne serait‑ce qu'avec l'impossibilité d'encapsuler les blocs de commentaires les uns dans les autres (cf. chap. C2‑II ). On est donc contraint d'ajouter un séparateur // au début de chaque ligne et, même avec un bon éditeur de code, de telles manipulations deviennent fastidieuses lorsqu'il faut les répéter fréquemment.

Une directive de compilation conditionnelle consiste justement à indiquer au préprocesseur qu'une partie du code source doit être compilée seulement si une condition est vérifiée. Dans la négative, lors du traitement de cette directive, le préprocesseur ne transmet pas cette partie du code au compilateur.

Syntaxe générale

Une directive de compilation conditionnelle adopte une structure similaire à celle d'une bifurcation if (cf. chap. C2‑V ), mais avec une syntaxe spécifique à l'aide de plusieurs lignes de contrôle .

Typiquement, on code une telle directive de la forme :

#if condition 1
code contrôlé 1
(compilé seulement si condition 1 est vraie)

#elif condition 2
code contrôlé 2
(compilé seulement si condition 2 est vraie)

#elif

#else
code contrôlé n
(compilé dans tous les autres cas)

#endif // condition 1

où :

  • toutes les conditions sont des expressions à destination du préprocesseur ; leur codage est régi par une syntaxe restreinte détaillée infra  ;
  • la première ligne de contrôle #if peut être aussi être codée #ifdef ou #ifndef si la première condition exprime le fait qu'un identificateur de macro‑définition est antérieurement défini ou non (cf. infra ) ;
  • les autres lignes de contrôle #elif (mot réservé signifiant par abréviation « else if ») et #else sont facultatives ; elles permettent de coder autant que nécessaire d'alternatives à la première condition ;
  • la dernière ligne de contrôle #endif est obligatoire pour clôturer la directive dans le fichier source où elle débute ; le commentaire (évidemment facultatif) ajouté à sa suite est vivement recommandé pour la bonne lisibilité du code.

Règles d'indentation

Comme pour les autres directives, les lignes de contrôle de compilation conditionnelle ne sont soumis à presque aucune contrainte de format (cf. supra ). Elles peuvent donc être indentées à souhait, sachant que l'indentation peut être placée avant ou après le symbole #.

Pourtant, en règle générale, cette possibilité n'est pas systématiquement exploitée comme pour les instructions.

Une bonne pratique consiste à :

  • ne pas indenter ces lignes de contrôle même si elles s'insèrent dans une partie de code déjà indentée ;
  • ne pas modifier l'indentation normale du code contrôlé sauf s'il ne s'agit que de lignes de contrôle d'autres directives ;

Ainsi, le fichier prétraité éventuellement généré par le préprocesseur se présente comme un code source usuel.

De plus, lorsqu'une directive s'étend sur une grande partie de code (typiquement, lorsqu'elle dépasse la taille d'affichage d'un moniteur) ou lorsqu'il y a plusieurs directives imbriquées les unes dans les autres sans indentation, des commentaires placés en fin des lignes de contrôle sont bienvenus faciliter la lecture du code.

Les fichiers d'en‑tête du framework Arduino fournissent de nombreux exemples de directives de compilation conditionnelle. En revanche, ils ne sont pas indentés selon des règles systématiques. Ainsi, dans le fichier HardwareSerial.h G pour cartes à cœur AVR, on trouve :

  • deux directives imbriquées l'une dans l'autre qui permettent de définir la pseudo‑constante SERIAL_TX_BUFFER_SIZE (laquelle détermine la taille du buffer d'émission de l'objet Serial – cf. chap. C3‑X ) :
  • #if !defined(SERIAL_TX_BUFFER_SIZE)
    #if ((RAMEND - RAMSTART) < 1023)
    #define SERIAL_TX_BUFFER_SIZE 16
    #else
    #define SERIAL_TX_BUFFER_SIZE 64
    #endif
    #endif
    
    qu'il aurait d'ailleurs été plus clair de coder et d'indenter comme ci‑dessous :
    #ifndef SERIAL_TX_BUFFER_SIZE
    #  if (RAMEND - RAMSTART) < 1023
    #    define SERIAL_TX_BUFFER_SIZE 16
    #  else
    #    define SERIAL_TX_BUFFER_SIZE 64
    #  endif
    #endif
    
  • une directive pour définir le type tx_buffer_index_t des variables d'indexation de ce buffer en fonction de sa taille :
  • #if (SERIAL_TX_BUFFER_SIZE>256)
    typedef uint16_t tx_buffer_index_t;
    #else
    typedef uint8_t tx_buffer_index_t;
    #endif
    
    sachant qu'ici, l'absence d'indentation est satisfaisante car il n'y a que des instructions dans le code contrôlé (dans le fichier prétraité généré par la commande cpp, seule l'une des deux instructions typedef serait présente, et sans indentation inutile).

Expressions de compilation conditionnelles

L'expression de compilation conditionnelle codée dans une ligne de contrôle #if est évaluée par le préprocesseur. Elle est similaire à une expression composée en langage C, mais avec des restrictions syntaxiques importantes.

Une expression de compilation conditionnelle et les sous‑expressions qui la composent sont évaluées dans le plus grand type entier standard signé reconnu par le compilateur – donc, en règle générale, sur 64 bits.

Selon le même principe que pour toutes les expressions en langage C, la condition exprimée est analysée par le préprocesseur CPP comme :

  • fausse si l'évaluation est égale à 0 ;
  • vraie sinon.

Une expression de compilation conditionnelle ne nécessite pas d'encapsulation dans des parenthèses (contrairement à celle de condition d'une structure de contrôle if). Une telle encapsulation est simplement sans effet.

Restrictions syntaxiques

Une expression de compilation conditionnelle se compose exclusivement avec :

  • des atomes (éléments non décomposables) qui ne peuvent être que :
    • des constantes littérales entières (ex. : 1, 2, etc.) – et donc, pas décimales ;
    • des constantes littérales de type caractère (ex. : 'A', 'B', etc.) , mais pas de type chaîne de caractères (car non évaluable comme valeur entière) ;
    • des identificateurs, qu'il soient définis ou non par des directives antérieures, sachant que les identificateurs non définis prennent la valeur 0 par défaut ;
  • des opérateurs qui ne peuvent être que :
    • les opérateurs élémentaires du langage C (cf. chap. C2‑IV ) – donc à l'exclusion de l'opérateur trois voies <=> qui est spécifique au C++ ;
    • l'opérateur d'élévation de rang de priorité( ) et l'opérateur conditionnel ? : (cf. supra ) ;
    • l'opérateur unaire defined qui est spécifique au préprocesseur ; appliqué à un identificateur, il donne la valeur 1 si ce dernier est défini par une directive antérieure, sinon la valeur 0 ;
    • autrement dit, #if defined est équivalent à #ifdef (de même que #if !defined est équivalent à #ifndef) ;
  • des invocations de pseudo‑fonctions, qui doivent évidemment en respecter la syntaxe (parenthèses, nombre d'arguments, etc. – cf. supra ).

Toujours dans le fichier HardwareSerial.h G du framework Arduino pour cartes à cœur AVR, on trouve notamment deux exemples d'expressions de compilation conditionnelle au sein d'une ligne de contrôle d'une directive :

  • À la ligne nº 43 :
  • #if ((RAMEND - RAMSTART) < 1023)
    
    on a l'expression ((RAMEND - RAMSTART) < 1023), sachant que RAMEND et RAMSTART sont deux pseudo‑constantes entières définies dans un autre fichier d'en‑tête préalablement inclus (notons au passage que les parenthèses externes sont inutiles).
    Pour être plus précis, ces deux pseudo‑constantes donnent respectivement l'adresse de fin et de début de la mémoire RAM sur la carte cible (cf. les lignes nº 883 & 884 du fichier avr/iom328p.h pour les cartes à microcontrôleur ATmega328p ).
  • à la ligne nº 142 :
  • #if defined(UBRRH) || defined(UBRR0H)
    
    on a l'expression defined(UBRRH) || defined(UBRR0H). Ici, le recours à l'opérateur defined est indispensable (on ne peut pas composer deux lignes de contrôle #ifdef avec l'opérateur ||).
    Pour être plus précis, les deux pseudo‑constantes UBRRH et UBRR0H correspondent potentiellement à des adresses de registres du microcontrôleur sur la carte cible (ainsi, pour le ATmega328p, on a UBRR0H qui est définie à ligne nº 781 du fichier avr/iom328p.h).

  1. Dans une directive de la forme :
    #if pseudo‑constante
    si la pseudo‑constante est bien préalablement définie mais vaut 0, la condition est évaluée fausse.
  2. Il faut donc ne pas confondre avec une directive de la forme :
    #ifdef pseudo‑constante
    qui serait, elle, évaluée vraie quelle que soit la valeur de la pseudo‑constante, dès lors que cette dernière est définie.
  3. Le préprocesseur évaluant ses expressions dans le type entier standard le plus grand possible (cf. supra ), il ne procède à aucune analyse ni conversion de type. Tout emploi d'un suffixe de typage ou d'un opérateur de cast (cf. chap. C3‑VI ) y est donc aussi inutile qu'erroné.

Directives de diagnostic de prétraitement

Le préprocesseur CPP admet deux directives qui permettent d'interférer sur le processus de prétraitement d'un programme :

  • #warning qui émet un message – typiquement, un avertissement – dans le compte‑rendu de compilation ;
  • #error qui, en plus émettre un message, déclenche une erreur et fait donc échouer la compilation.

Tout élément codé après #warning ou #error dans la ligne de contrôle de la directive est reconnu par le préprocesseur comme la chaîne de caractères constituant le message à afficher, sans qu'il soit nécessaire de l'encapsuler entre guillemets doubles " " (même si cette pratique existe). Il ne fait l'objet d'aucune expansion.

Dans la pratique, ces lignes de contrôle sont employées dans le cadre de directives de compilation conditionnelle, sinon leur intervention serait systématique. Elles permettent notamment d'avertir l'utilisateur d'un code source lorsqu'il tente une compilation pour un environnement qui présente une incompatibilité avec le programme.

Elles peuvent également être utilisée pour la mise au point des directives d'un programme, notamment si l'on veut vérifier qu'un identificateur est défini, qu'une pseudo‑constante prend telle ou telle valeur, etc.

Les fichiers du framework Arduino utilisent occasionnellement des directives #warning et #error. Les exemples ci‑dessous en donnent un aperçu.

  1. Dans le fichier d'implémentation WInterrupts.c G pour cartes à cœur AVR, on trouve plusieurs directives #warning, notamment :
  2. #if EXTERNAL_NUM_INTERRUPTS > 8
      #warning There are more than 8 external interrupts. Some callbacks may not be initialized.
    
    Codée dans la déclaration d'un tableau de pointeurs de fonctions à appeler en cas d'interruption, elle avertit l'utilisateur du code que ce tableau ne sera pas complètement initialisé si la pseudo‑constante EXTERNAL_NUM_INTERRUPTS est supérieure à 8.
  3. Dans le fichier d'implémentation HardwareSerial0.cpp G pour cartes à cœur AVR, on trouve plusieurs directives #error, notamment à la ligne nº 46 :
  4. #if defined(USART_RX_vect)
      ISR(USART_RX_vect)
    #elif defined(USART0_RX_vect)
      ISR(USART0_RX_vect)
    #elif defined(USART_RXC_vect)
      ISR(USART_RXC_vect) // ATmega8
    #else
      #error "Don't know what the Data Received vector is called for Serial"
    #endif
    
    Cette directive déclenche une erreur de compilation si aucune des trois pseudo‑constantes USART_RX_vect, USART0_RX_vect ou USART_RXC_vect n'est définie.

Principales applications des directives de compilation conditionnelle

Les directives de compilation conditionnelle ont pour principale application de produire :

  • des programmes portables sur divers environnements (machines cibles, systèmes d'exploitation…), en adaptant le code source à des variables d'environnement définies comme des pseudo‑constantes ;
  • de programmes évolutifs où le code source contient plusieurs versions sectionnables via une ou plusieurs pseudo‑constantes dont la définition peut même être saisie dans la commande système de compilation.

Mais il existe aussi d'autres applications très courantes, notamment :

  • la protection d'un fichier source contre les inclusions multiples ;
  • la mise en hors compilation d'une partie de code, comme on serait tenté de le faire par une mise en commentaire, mais sans les problèmes que pose cette « astuce ».

Ces deux applications sont détaillées ci‑après.

Protection d'un fichier source contre les inclusions multiples

Dans le cadre de développement d'un programme comportant plusieurs fichiers sources (problématique qui sera abordée dans les chapitres suivants de la partie C4 du module), il est usuel que certains d'entre eux requièrent l'inclusion d'un même fichier. Or cela pose inévitablement des conflits de déclaration lors de la compilation, puisque tous les éléments déclarés dans ce fichier le seront plusieurs fois.

Pour éviter un tel problème, lorsqu'on développe un fichier modulaire (fichier d'en‑tête ou autre…) destiné à être inclus dans divers programmes, il est d'usage d'encapsuler l'intégralité de son code source dans une directive de compilation conditionnelle de protection contre les inclusions multiples. Cette directive prend la forme suivante :

#ifndef pseudo‑constante spécifique
#define pseudo‑constante spécifique

code intégral à inclure

#endif

Elle consiste donc :

  • à vérifier que la pseudo‑constante spécifique au fichier source n'est pas définie ;
  • et dans ce cas seulement, à définir cette pseudo‑constante (sans chaîne de substitution car la seule utilité de cette pseudo‑constante est de pouvoir vérifier le fait qu'elle est définie ou non) et encapsuler tout le code du fichier.

Donc, lorsque le préprocesseur traite cette directive, si cette pseudo‑constante est déjà définie, cela signifie que le fichier est déjà inclus. Son code source est alors ignoré, pour ne pas être à nouveau inclus.

En règle générale, l'identificateur de la pseudo‑constante spécifique reprend le nom du fichier source (extension comprise). Le suffixe _H (pour évoquer l'extension .h) ou tout autre ajout comme _INCLUDED est bienvenu pour éviter un risque de conflit avec un éventuel identificateur qu'un programme utilisateur du fichier pourrait employer.

Tous les fichiers d'en‑tête de bibliothèques, standards ou non, en langage C ou C++, sont protégés par une directive de la forme ci‑dessus (ou éventuellement un peu plus complexe). Ceux du framework Arduino ne dérogent pas à la règle. Le fichier Arduino.h G pour les cartes à cœur AVR en donne un exemple typique. On y trouve dès la première ligne de code (après le bloc de commentaires en préambule) :

#ifndef Arduino_h
#define Arduino_h

et tout à la fin, la ligne de contrôle de clôture de cette directive :

#endif 

Mise hors compilation d'une partie de code source

Rappelons que lorsque l'on souhaite qu'une partie du code d'un programme ne soit pas compilée tout en restant enregistrée dans le fichier, il est envisageable – mais néanmoins malcommode – de mettre en commentaires de cette partie de code (cf. chap. C2‑II ).

La bonne pratique consiste plutôt à encapsuler la partie de code dans une directive de compilation conditionnelle de la forme :

#if 0

code à exclure de la compilation

#endif // #if 0

La condition 0 étant toujours fausse, toute la partie de code jusqu'à la ligne de contrôle #endif n'est jamais transmise au compilateur. Automatiquement, tout bon éditeur de code octroie à cette partie de code la même coloration syntaxique qu'un commentaire pour repérer au premier coup d'œil qu'elle est inopérante.

Cette méthode présente plusieurs avantages sur une véritable mise en commentaire :

  • être opérationnelle quel que soit le contenu du code encapsulé, même s'il s'y trouve des blocs de commentaires (on rappelle en effet qu'on peut pas utiliser les délimiteurs /* */ pour encapsuler une partie de code qui contient déjà ces délimiteurs) ;
  • ne pas gêner la lecture de cette partie de code avec un séparateur // au début de chaque ligne ;
  • ne pas requérir la sélection de toute la partie de code à « commenter » ou « décommenter » (ce qui n'est pas très commode lorsque cette partie est grande) ; il suffit juste d'ajouter ou supprimer les deux lignes de contrôle #if 0 et #endif.