Préprocesseur
Le préprocesseur permet de rajouter des macros et de modifier le code source avant la compilation. Il y a plusieurs utilisations pratiques :
- remplacer des constantes, e.g.
#define X 32 - inclusion de fichier header
- la compilation conditionnelle “si je suis sous Windows, alors inclus ce fichier, sous Linux, inlus ce fichier etc.”
- créer un système de tests unitaires, avec affichage de numéros de ligne et tout
Le principe exite dans d’autres langages : en développement Web avec la transcompilation Typescript vers Javascript, en Rust, en LISP… En Rust, en LISP, le préprocesseur est syntaxique : il réécrit des portions de l’arbre syntaxique du programme. Malheureusement, en C/C++, le préprocesseur est textuel : il ne connait pas la syntaxe du C. L’avantage est que le préprocesseur est rapide, mais au prix de problèmes causés par ce choix. On va voir ce problème avec un système de tests unitaires.
Textuel VS syntaxique
En C c’est textuel
En C, le système de macros est textuel.
Avec
#define SQUARE(x) x * x
Que devient SQUARE(4 + 2)?
4 + 2 * 4 + 2 alors que l’on s’attend à (4 + 2) * (4 + 2). Bref, c’est textuel…
La solution est de parenthéser :
#define SQUARE(x) (x) * (x)
En Rust c’est syntaxique
En Rust, qu’affiche ce code qui définit une macro square ?
macro_rules! square {
($x:expr) => {
$x * $x
};
}
fn main() {
println!("{}", square!(4+2));
}
36.
Ma première macro en C
#define ASSERT(condition) \
if (!condition) \
printf(" [FAIL] %s (Ligne %d): %s\n", __func__, __LINE__, #condition)
#conditionreprésente la chaîne de caractère"1+1 == 2"sans qu’on ait besoin de la taper à la main__func__est le nom de la fonction courante__LINE__est le numéro de ligne__FILE__est le nom du fichier source
Comme :
ASSERT(1+1 == 2)
Mais en fait ça fonctionne pas bien car ça écrit !1+1 = 2…
Il faut faire :
#define ASSERT(condition) \
if (!(condition)) \
printf(" [FAIL] %s (Ligne %d): %s\n", __func__, __LINE__, #condition)
Et là on a !(1+1==2).
Attention aux blocs
Mais en fait, ça ne fonctionne pas bien.
if(debug) ASSERT(1+1==2) else printf("ok\n");devient
if(debug) if(!(1+1==2)) printf(" [FAIL]...", ...); else printf("ok\n");
Accolades
On pourrait imaginer mettre des accolades comme :
#define ASSERT(condition) \
{ \
if (!(condition)) \
printf(" [FAIL] %s (Ligne %d): %s\n", __func__, __LINE__, #condition); \
}
Mais alors :
if(debug) ASSERT(1+1==2); else printf("ok\n");devient
if(debug) { if(!(1+1==2)) printf(" [FAIL]...", ...); }; else printf("ok\n");
Et le else ne va pas car le point-virgule ; qui le précède ferme le bloc if.
Solution idiomatique
L’astuce consiste à utiliser do { ... } while (0).
#define ASSERT(condition) \
do { \
if (!(condition)) { \
printf(" [FAIL] %s (Ligne %d): %s\n", __func__, __LINE__, #condition); \
return 1; \
} \
} while (0)
if(debug) ASSERT(1+1==2); else printf("ok\n");devient
if(debug) do { if(!(1+1==2)) printf(" [FAIL]...", ...); } while(0); else printf("ok\n");
Là le point-virgule termine l’instruction do {…} while(0);.
tests_helper.h qui contient vos macros de tests unitaires. Construire votre projet en plaçant les tests dans un répertoire tests. Ajouter une commande à votre Makefile make test qui compile et exécute les tests unitaires.