Quelques raccourcis :

Le préprocesseur

Le préprocesseur est un outil qui modifie le code source avant que le compilateur n'entre en oeuvre. Ses directives débutent toutes par un dièse (#) se trouvant dans la première colonne d'une ligne de code, et ne se terminent pas par un point virgule (;). Le langage du préprocesseur, malgré l'habitude que nous développons à son usage, n'est ni celui du langage C, ni celui du langage C++, mais bien un langage à part entière.

Sur cette page, nous poserons notre regard sur les possibilités et les limites de cet outil, en nous concentrant sur ses directives et leur effet sur les transformations apportées au code source.

Ce document n'est pas complet; je n'ai pas eu le temps de parler de #error, de macros variadiques, et des expressions pouvant faire partie de macros. Simple question de temps...

Directives de définition de symboles – #define et #undef

La directive #define introduit une macro, soit un symbole qui sera remplacé par une expression (optionnelle) lors de la précompilation d'un fichier. En contrepartie, la directive #undef élimine un symbole de la liste de ceux qui sont définis.

Directive Signification et effet
#define sym [rempl]

Définit le symbole sym, associé (de façon optionnelle) au texte de remplacement rempl.

Si le symbole est associé à un texte de remplacement, alors chaque occurrence de sym dans le code source sera remplacé par rempl. Le remplacement réalisé sera lexical,

Le texte de remplacement peut être une expression paramétrique.

#undef sym

Élimine la définition du symbole sym. À partir de cet endroit, le symbole en question n'existe plus.

La directive #define crée un symbole pour le préprocesseur. Ce symbole peut, de façon optionnelle, avoir une valeur. Le cas où aucun texte de remplacement n'est offert sera couvert plus bas, là où il prendra son sens.

Symboles avec texte de remplacement

Prenons l'exemple suivant, qui associe au symbole XYZ l'équivalent lexical 3.

#define XYZ 3

Ainsi, à chaque endroit où le préprocesseur rencontrera dans le texte du code source ayant défini cette directive le lexème XYZ, il le remplacera par le texte 3.

Avant le préprocesseur Après le préprocesseur
#define XYZ 3
 
int a = XYZ;
int a = 3;
z = f(-XYZ);
z = f(-3);
#define FRED 33
int somme(int a, int b) {
   return (a + b);
}
int main() {
   int a= -FRED* 4+ somme(FRED, -4);
}
int somme(int a, int b) {
   return (a + b);
}
int main() {
   int a= -33* 4+ somme(33, -4);
}

De manière semblable, une macro paramétrique est possible, comme par exemple :

#define min(a,b) (((a)<(b))?(a):(b))

Une telle macro provoquera un remplacement paramétrique du symbole par l'expression correspondante, en tenant compte des paramètres, mais sur une base lexicale. Ainsi :

Avant le préprocesseur Après le préprocesseur
#define min(a,b) (((a)<(b))?(a):(b))
 
int f();
int g();
int f();
int g();
int main() {
   int m, n;
   if (cin >> m >> n)
      cout << min(m, n) << endl;
   cout << min(f(), g()) << endl;
}
int main() {
   int m, n;
   if (cin >> m >> n)
      cout << (((m)<(n))?(m):(n)) << endl;
   cout << (((f())<(g()))?(f()):(g())) << endl;
}

Une des raisons pour lesquelles C++ préfère les templates aux macros est le remplacement lexical des expressions : ici, à gauche, pourrait penser avoir appelé f(), puis g(), et avoir trouvé la valeur minimale des deux valeurs ainsi obtenues, comme lors d'un appel de fonction, mais en pratique, nous appellerons deux fois l'une des deux fonctions (impossible d'établir laquelle au préalable).

Question piège

Quelle sera la valeur de z dans ce qui suit? Notez bien que ceci compile mais n'est absolument pas recommandable.

#define A 4
#define B ((A > b)? A: b)
int main() {
   int b= 3, z= B;
}

Directives d'inclusion de fichier – #include

Directive Signification et effet
#include <fich.h>

Sera remplacé par le texte du fichier fich.h par le préprocesseur, qui le cherchera dans les répertoires où se logent les bibliothèques standard.

#include <fich>

Comme dans le cas ci-dessus, à un détail près. Les en-tètes standard (sans extension .h peuvent ne pas exister sous forme de fichier sur disque et être plutôt logés en mémoire vive par le compilateur.

#include "fich.h"

Sera remplacé par le texte du fichier fich.h par le préprocesseur, qui cherchera d'abord ce fichier dans le répertoire courant.

La directive #include demande au préprocesseur d'inclure le texte d'un fichier à la place de la directive d'inclusion elle-même.

// cette ligne sera remplacée par le texte du fichier bits.h
#include "bits.h"
// cette ligne sera remplacée par le texte de cmath
#include <cmath>

La différence entre un fichier inclus entre <> et un autre inclus entre "" est la suivante :

Question piège

Si a.cpp contient la ligne

#include "a.h"

...et si b.h contient la ligne

#include "a.h"...

et si c.h contient les lignes

#include "a.h"
#include "b.h"

...et si c.cpp contient la ligne

#include "c.h"

...alors combien de fois le texte du fichier a.h se retrouvera-t-il inclus dans le fichier c.cpp?

En effet, si on prend l'exemple d'une déclaration de type, avoir une seule déclaration qui puisse être inclue par différents fichiers source, inclure ce fichier assure que tous les fichiers sources perçoivent exactement le même type de la même manière.

Évidemment, mettre à la disposition de toutes et de tous des déclarations communes et destinées à être utilisées de manière généralisée est une chose, mais il faut bien entendu que les gens s'en servent.

Par exemple, si le fichier octets.h contient (entre autres) la déclaration suivante...

enum partie_octet {
   partieHaute, partieBasse
};

...alors tout fichier ayant la directive d'inclusion visible à droite...

#include "octets.h"

...s'assurera d'inclure de facto la déclaration du type partie_octet s'y trouvant. Ceci évite que quelqu'un décide de définir localement le même type, et rédige par exemple ce type comme suit et que le programe se trouve aux prises avec deux conceptions différentes (et incompatibles) d'une même idée...

enum partie_octet {
   partieBasse, partieHaute
};

Les valeurs associées aux constantes énumérées que sont partieBasse et partieHaute dans les deux cas ne sont pas les mêmes, ce qui pourrait conduire à des problèmes de logique lors de l'exécution du programme.

Dans le schéma visible à droite, les flèches indiquent le sens de l'inclusion des fichiers.

Remarquez que bits.h est inclus par bits.cpp et par UnProjet.cpp. Ceci signifie que le texte de bits.h fera partie à la fois de bits.cpp et de UnProjet.cpp.

La même règle s'applique ici au contenu de octets.h, qui se retrouve intégralement dans octets.cpp et dans UnProjet.cpp.

Cela signifie que le type partie_octet, mentionné en exemple plus haut et déclaré dans octets.h, fait partie des fichiers octets.cpp et UnProjet.cpp mais ne fait pas partie de bits.cpp.

Directives d'inclusion conditionnelle – #if, #ifdef, #ifndef, #elif, #else et #endif

Les directives d'inclusion conditionnelle permettent de contrôler l'ajout (ou non) de régions de code dans l'unité de traduction qui sera bel et bien compilée.

Directive Signification et effet
#ifdef sym

Le code suivant cette directive fera partie de la compilation seulement si sym a été défini (par #define) au préalable.

La section de code qui suit cette directive doit se terminer par un #elif, un #else ou un #endif.

#ifndef sym

Le code suivant cette directive fera partie de la compilation seulement si sym n'a pas été défini (par #define) au préalable.

La section de code qui suit cette directive doit se terminer par un #elif, un #else ou un #endif.

#if condition

Le code suivant cette directive fera partie de la compilation seulement si condition est vrai.

On peut inclure dans condition des opérateurs logiques, arithmétiques ou relationnels sur les symboles et leur texte de remplacement, de même que defined sym qui est vrai seulement si sym a été préalablement défini (par #define).

La section de code qui suit cette directive doit se terminer par un #elif, un #else ou un #endif.

#elif [condition]

Équivalent d'un else if pour le préprocesseur. Les règles du #if s'appliquent.

#else

Équivalent d'un else pour le préprocesseur.

La section qui suit un #else fera partie de la compilation dans la mesure où aucune des directives #if, #ifdef, #ifndef ou #elif la précédant logiquement n'ont été évaluées comme vraies.

Doit se terminer par un #endif.

#endif

Termine la section d'une directive #if, #ifdef, #ifndef, #elif ou #else.

Question piège

Laquelle des versions de fonctionBizarre() sera celle appelée dans main()?

#define ROGER
#define BIQUETTE 4
#ifndef ROGER
int fonctionBizarre(int z) {
   return (z * 34 - (z*z)/15);
}
#elif defined BIQUETTE && (BIQUETTE >= 3)
int fonctionBizarre(int z) {
   return ((float)z / 15 - (1.0/z));
}
#else
int fonctionBizarre(int z) {
   return (-1);
}
#endif
int main() {
   int a= fonctionBizarre(4);
}

Directives natives – #pragma

Bien que le langage C++ permette d'écrire des programmes qui soient pleinement portables (du point de vue du code source), le mot portable étant un anglicisme au sens de utilisable sur plusieurs plateformes, il est possible d'utiliser des directives natives à chaque plateforme et à chaque compilateur.

Évidemment, il y a un prix à payer pour avoir recours à des tactiques non portables (il faut une version des sources pour chaque plateforme!). Il faut donc minimiser le recours à cette tactique. Si une alternative à une directive indigène existe (par exemple, la directive #pragma once du compilateur C++ de Microsoft remplace une technique très simple et très répandue qui n'utilise que des directives portables), alors il faut éviter les directives indigènes qui ne font que nuire à la portabilité sans entraîner de réels avantages.

En retour, pour les cas très propres à un compilateur donné (par exemple, pour faire taire un avertissement très spécifique qu iest un défaut du compilateur plutôt que de votre propre code) ou à une plateforme donnée (pour utiliser une stratégie d'accès au matrériel très spécifique à un système d'exploitation), les directives indigènes (avec des commentaires en quantité suffisante pour documenter la manoeuvre) sont tout indiquées.

Nous ne couvrirons pas l'étendue des directives indigènes dans ce document – ce ne serait pas pertinent. Si vous avez envie d'en savoir plus, consultez la documentation de votre outil de travail.

Directive Signification et effet
#pragma <varia>

La directive #pragma permet des effets dépendants de la plateforme de développement. Ses effets et possibilités changent selon le compilateur utilisé.

Macros « magiques »

Les préprocesseurs, pour faciliter les diagnostics dans les programmes, offrent des macros « magiques » nommées __FILE__, __LINE__ et __FUNCTION__ pour indiquer, respectivement, le nom du fichier en cours de traitement, la ligne dans ce fichier, et (si cela s'avère) le nom de la fonction en cours de traitement. Le format du texte dans ces macros n'est pas normé, et dépend du compilateur.

Par exemple :

Programme Sortie possible
#include <iostream>
using namespace std;
void f(int) {
   cout << __FILE__ << endl
        << __LINE__ << endl
        << __FUNCTION__ << endl;
}
void f() {
   cout << __FILE__ << endl
        << __LINE__ << endl
        << __FUNCTION__ << endl;
}
int main() {
   f();
   f(3);
   cout << __FILE__ << endl
        << __LINE__ << endl
        << __FUNCTION__ << endl;
}
z.cpp
10
f
z.cpp
5
f
z.cpp
17
main

Ceci permet entre autres une forme extrêmement limitée de réflexivité sur la structure des fichiers sources.

Comme le fait remarquer Wojciech Muła en 2018 (source), mieux vaut ne pas mêler macros et templates en C++ :

#include <map>
#define decl(name, type) type name
decl(kitten, int); // OK
decl(nope, std::map<int,bool>); // error: macro "decl" passed 3 arguments, but takes just 2

Macros variadiques

Le langage C permet d'écrire des macros variadiques, mais en comparaison avec les templates variadiques de C++, c'est un mécanisme très douloureux. Tout de même, si vous y tenez :

#include <iostream>
void f(int) { std::cout << "Un seul parametre\n"; }
void f(int,int) { std::cout << "Deux parametres\n"; }
void f(int,int,int) { std::cout << "Trois parametres\n"; }
#define F(...) (f(__VA_ARGS__))
int main() {
   F(2); // un seul parametre
   F(2,3,5); // trois parametres
}

Ici, __VA_ARGS__ signifie « les paramètres dans la section ... de la macro ».

Trucs et techniques

Il existe des milliers de trucs et de techniques à partir de macros. Je ne recommande pas d'en abuser : en C++, particulièrement, nous essayons de les faire disparaître le plus possible, les actions des macros échappant au compilateur et étant quelque peu hostiles à son égard.

Tout de même, les macros existent, et les gens s'en servent, alors voici quelques trucs et techniques à base de macros que vous trouverez peut-être utiles.

Le TODO

Cette technique m'a été suggérée par Peter Bindels en 2018 (source) :

« I've had the mix of these two - a macro called TODO that would print what it expected you to write on what line and that then aborted. It's actually a really nice way to expand on something by tagging all of your shortcuts with what to fix where, if you hit it »

Son idée est donc d'écrire une macro comme la suivante (présumant les inclusions requises faites au préalable) :

#define TODO(msg) { puts(msg); abort(); }

... pour ensuite l'utiliser à titre de rappel pour les programmeuses et les programmeurs :

void f(X *p) {
   TODO("f(X*) n'est pas encore implémentée");
}

Lectures complémentaires

Quelques liens pour enrichir le propos.

Un illustre collègue, Pierre Prud'homme, m.'a demandé en 2011 si je connaissais la raison pour laquelle la directive #pragma porte ce nom. Ma curiosité étant piquée, j'ai creusé (un peu, tout de même) la question :

« A preprocessing directive of the form

#pragma pp-tokensopt new-line 

causes the implementation to behave in an implementation-defined manner. The behavior might cause translation to fail or cause the translator or the resulting program to behave in a non-conforming manner. Any pragma that is not recognized by the implementation is ignored. »

... et c'est tout. Il faut donc comprendre que tout ce que la norme définit pour #pragma est qu'il s'agit d'une directive dont le comportement dépend de l'implémentation (pas nécessairement du compilateur ou du préprocesseur).

Pour ce qui est de l'étymologie d'origine du terme, Pierre Prud'homme (ami et illustre collègue susmentionné) ajoute :

« Pragmatic modules, or pragmata, for short, or pragmas, for shorter affect the compilation of your program. It is called this way because the compiler needs to treat it pragmatically.

Pragmas were introduced in the Ada 83 in 1983, and were reused in many computer languages since (C, perl, ECMAScript, PL/SQL...) The naming rationale appears in the document "Rationale for the Design of the Ada® Programming Language". »

Pierre ajoute :

« Le document en question dit : A pragma (from the Greek word meaning action) is used to direct the actions of the compiler in particular ways, but has no effect on the semantics of a program (in general). »


Valid XHTML 1.0 Transitional

CSS Valide !