Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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));
}

Il affiche 36.

Ma première macro en C

#define ASSERT(condition) \
        if (!condition)  \
            printf("  [FAIL] %s (Ligne %d): %s\n", __func__, __LINE__, #condition)
  • #condition repré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);.

Construire un fichier 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.