Au fil des flows

16jan/121

Explorons le langage C et ses outils : Assert et Preprocesseur [Part1 sur n]

Posted by Fabien Arcellier

J'ai décidé de démarrer une série d'articles sur le langage C et quelques mécanismes autour de gcc.

Ce premier billet sera consacré à la macro assert que l'on trouve dans l'include assert.h

Principe

Les assertions sont des hypothèses qui doivent toujours être vrai quelque soit l'état du programme. C'est très pratique en développement pour vérifier l'évidence. Par exemple, qu'on ne dépasse pas les bornes d'un tableau. Ce qui donnerait un beau SEGMENTATION FAULT

Quand vous mettez une bibliotheque à disposition d'un autre développeur, c'est une grande aide. Par un mécanisme astucieux de pré conditions et de post conditions, vous pouvez borner le comportement de votre librairie en arrêtant le code au plus tot pour signaler à un développeur qu'il ne respecte pas les exigences que vous vous êtes fixés.

Les assertions sont évalués à l'exécution. Donc c'est évident que elles ralentiront votre programme

L'intéret c'est justement de pouvoir les retirer totalement du code lors du passage en production. C'est ce que nous allons découvrir

Le code que nous allons analyser

#include <assert.h>
#include <stdlib.h>
#include <stdio.h>

typedef struct 
{
	int width;
	int height;
	int *tab;
} IntArray2D;

IntArray2D * IntArray2D_Create (int width, int height);
void IntArray2D_Destroy (IntArray2D * array2d);
int IntArray2D_GetValue(IntArray2D* array2d, int x, int y);

int main (void)
{
	IntArray2D * array2d = IntArray2D_Create (5, 5);
	int x = IntArray2D_GetValue(array2d, 0, 0);
	IntArray2D_Destroy (array2d);
	return 0;
}

IntArray2D * IntArray2D_Create (int width, int height)
{
	assert(width > 0);
	assert(height > 0);
	
	int size = width * height;
	IntArray2D* array = (IntArray2D*) malloc (sizeof(IntArray2D));
	array -> width = width;
	array -> height = height;
	array -> tab = (int*) malloc (sizeof(int) * size);
	
	return array;
}

void IntArray2D_Destroy (IntArray2D* array2d)
{
	free(array2d -> tab);
	free(array2d);
}

int IntArray2D_GetValue(IntArray2D* array2d, int x, int y)
{
	assert(x >= 0 && x < array2d -> width);
	assert(y >= 0 && y < array2d -> height);
	
	return array2d -> tab[x + y * array2d -> width];
}

Effet du preprocesseur

Sous gcc, vous avez 2 choix possibles pour récupérer le resultat du preprocesseur

Executer seulement le preprocesseur

gcc -E main.c > main.i

Par convention, les fichiers preprocessés portent l'extension .i, les fichiers assembleurs .s et les fichiers objets .o

Demander à gcc de conserver les fichiers intermediaires

gcc -o main -save-temps -g main.c

Ainsi, vous récupérerez en plus du binaire les fichiers main.i, main.s et main.o

A présent, ouvrez le fichier main.i avec votre éditeur préféré.

assert(width > 0);

devient

((width > 0) ? (void) (0) : __assert_fail ("width > 0", "main_not_inline.c", 26, __PRETTY_FUNCTION__));

On remarque que le preprocesseur a rajouté 2 informations :

  • 26 : le numero de ligne
  • "main_not_inline.c" : le nom du fichier

Voici la définition de la macro assert(exp) [\usr\include\assert.h]

# define assert(expr)							\
  ((expr)								\
   ? __ASSERT_VOID_CAST (0)						\
   : __assert_fail (__STRING(expr), __FILE__, __LINE__, __ASSERT_FUNCTION))

Le preprocesseur a la capacite de venir numeroter les lignes et les fichiers grâce à 2 constantes : __LINE__ et __ FILE__

Effacer les asserts lors de la compilation

Il suffit de définir la macro NDEBUG.

gcc -o main -g -save-temps main.c -DNDEBUG

Le fichier resultat

typedef struct
{
 int width;
 int height;
 int *tab;
} IntArray2D;

IntArray2D * IntArray2D_Create (int width, int height);
void IntArray2D_Destroy (IntArray2D * array2d);
int IntArray2D_GetValue(IntArray2D* array2d, int x, int y);

int main (void)
{
 IntArray2D * array2d = IntArray2D_Create (5, 5);
 int x = IntArray2D_GetValue(array2d, 0, 0);
 IntArray2D_Destroy (array2d);
 return 0;
}

IntArray2D * IntArray2D_Create (int width, int height)
{
 ((void) (0));
 ((void) (0));

 int size = width * height;
 IntArray2D* array = (IntArray2D*) malloc (sizeof(IntArray2D));
 array -> width = width;
 array -> height = height;
 array -> tab = (int*) malloc (sizeof(int) * size);

 return array;
}

void IntArray2D_Destroy (IntArray2D* array2d)
{
 free(array2d -> tab);
 free(array2d);
}

int IntArray2D_GetValue(IntArray2D* array2d, int x, int y)
{
 ((void) (0));
 ((void) (0));

 return array2d -> tab[x + y * array2d -> width];
}

On remarque que tous les asserts sont remplacés par ((void) (0));

D'apres ce que j'ai pu lire sur stack overflow, cette instruction indique au compilateur qu'il n'a rien à faire. Elle permet de gérer plus facilement les statements (if, for, ...).

Pour conclure

Les assertions sont un outil intéressant. Elles permettent de valider le fonctionnement d'un programme. Vous ne testez pas tout grace à elle mais c'est un outil efficace qui renforce au fur et à mesure la confiance que l'on peut avoir dans un code.

J'aurai, je pense, l'occasion de revenir sur la notion d'invariant et de programmation par contrat défendu par Bertrand Meyer qui est un outil très puissant pour la conception objet d'un programme et le respect du principe de substitution de liskov.

Les assertions ne remplacent pas les tests unitaires. C'est un outil complémentaire qui vient s'ajouter à la boite à outils du programmeur.

Remplis sous: Non classé 1 commentaire