En programmation, on est souvent confronter à un choix :

  1. faire beaucoup de vérifications, ce qui améliore la robustesse du code (quand une erreur se produit, on est capable de la situer assez rapidement) mais rend le code plus volumineux et plus lent (parfois beaucoup plus) ;
  2. ne faire aucune vérification et supposer que notre code est correct et que les personnes qui l'utiliseront le feront correctement.

La réponse de la plupart des langages (dont C et C++ avec l'en-tête standard « assert.h » pour le C et « cassert » pour le C++) à ce problème sont les assertions.

Les assertions sont des conditions qui se doivent d'être toujours vérifiées si le code est correct. Le vrai avantage de ces assertions c'est qu'une fois que le code a été bien testé et qu'il est considéré comme sûr, elles peuvent être désactivées et ne ralentissent donc plus l'exécution.

Exemple d'assertion dans un code C (C99, donc avec les booléens ;)) :

  1. void foo(bool bar)
  2. {
  3. if (bar)
  4. {
  5. ...
  6. return;
  7. }
  8.  
  9. // On sait que bar est faut car s'il avait vrai on serrait sorti à cause du
  10. // « return » ci-dessus, mais si le code avait été compliqué, en aurions
  11. // nous été aussi sûr ?
  12. // Dans ce ce cas, n'est-il pas mieux d'en être sûr avec une assertion ?
  13. assert(!bar);
  14. ...
  15. }

On peut aussi utiliser les assertions pour vérifier que l'exécution d'une fonction se fait bien dans les conditions requises (les pré-conditions) et qu'elle fasse bien ce qu'elle doit (les post-conditions).

Exemple :

  1. /**
  2.  * Calcule le logarithme népérien de “x”.
  3.  *
  4.  * Pré-condition : x doit être strictement positif.
  5.  */
  6. double log(double x)
  7. {
  8. assert(x > 0); // Vérification de la pré-condition
  9.  
  10. double result;
  11. ... // Calcul
  12.  
  13. assert(exp(result) == x); // Vérification de la post-condition
  14. return result;
  15. }

Et voilà, sans vraiment s'en rendre compte, on a fait de la programmation par contrat : dans la pré-condition on a vérifié que l'utilisateur de la fonction respecte le contrat d'utilisation et dans la post-condition on a vérifié que la fonction respecte bien son engagement (ici, calculer le logarithme népérien).

Maintenant, nous allons, à l'aide de macros, essayer d'un peu plus formaliser cela.

À l'aide du code ci-dessous, on va créer les alias « ensures » et « requires » à « assert » qui vont respectivement symboliser les pré-conditions et les post-conditions.

  1. #define requires(expression) assert(expression)
  2. #define ensures(expression) assert(expression)

Notre fonction log ressemble maintenant à ça :

  1. /**
  2.  * Calcule le logarithme népérien de “x”.
  3.  *
  4.  * Pré-condition : x doit être strictement positif.
  5.  */
  6. double log(double x)
  7. {
  8. requires(x > 0);
  9.  
  10. double result;
  11. ... // Calcul
  12.  
  13. ensures(exp(result) == x);
  14. return result;
  15. }

Vous voyez bien que ça n'était pas difficile de trouver nos pré- et post-conditions, mais il faut bien reconnaître que la fonction « log » n'est pas un modèle de complexité. Dans le cas d'une fonction complexe, comment s'assurer que nos conditions sont correctes ?

Il n'y a pas 36 solutions : il faut les tester.

Le problème c'est que la macro « assert » fournie n'est pas très pratique pour écrire des jeux de tests. En effet, si l'on teste que nos pré-conditions bloquent bien les cas incorrects, la macro « assert » arrêtera tout simplement le programme et nous empêchera de continuer les tests.

Une des solutions possibles est de passer en C++ et d'utiliser les exceptions pour faire en sorte que si nos conditions ne sont pas respectées, le programme ne s'interrompe pas mais génère une erreur récupérable.

Appelons notre type d'exception « ContractViolated » et regardons ce que pourrait donner un test de la pré-condition de notre fonction « log » :

  1. try
  2. {
  3. log(-1);
  4.  
  5. exit(EXIT_FAILURE); // Aucune exception n'a été levée.
  6. }
  7. catch (ContractViolated)
  8. {} // Ok, tout est bon.
  9. catch (...)
  10. {
  11. exit(EXIT_FAILURE); // Le mauvais type d'exception a été levé.
  12. }

Bon, comme c'est pas très sympa a écrire et que je suis sympa, je vous propose d'utiliser la macro « assert_exception » qui est disponible dans mon fichier d'en-tête C/C++ destinée à faire de la programmation par contrat contracts.h.

On peut donc maintenant écrire un jeu de test :

  1. // Vérification de la pré-condition.
  2. assert_exception(log(-1), ContractViolated);
  3. assert_exception(log(0), ContractViolated);
  4.  
  5. // Vérification de la post-condition.
  6. assert(log(exp(1)) == 1);

Alors, ça se fait non ? Vous n'avez donc plus d'excuse pour ne pas faire de la programmation par contrat et écrire des jeux de tests.

Encore une petite chose, il y a un type de contrats que nous n'avons pas vu : les invariants de classes. Ça concerne surtout le C++, mais si vous faites de la programmation orienté objet en C, vous pouvez adapter le raisonnement.

Le principe est le suivant : pour la plupart des classes, il y a des conditions que ses instances se doivent de toujours vérifier, ce sont les invariants de classe.

On peut prendre comme exemple une classe « Voiture » dans laquelle on peut ajouter ou retirer des passagers. Mais le nombre de passagers de la voiture doit toujours être positif :

  1. class Voiture
  2. {
  3. public:
  4. void ajouter_passager();
  5. void retirer_passager();
  6.  
  7. private:
  8. /**
  9. * Le nombre de passager.
  10. *
  11. * Invariant : doit être strictement positif.
  12. */
  13. int nb_passagers;
  14. };

Afin de vérifier les invariants, je propose de rajouter une méthode « isValid() » qui se charge de vérifier si les invariants sont respectés.

  1. bool Voiture::isValid() const
  2. {
  3. return (nb_passagers > 0);
  4. }

Cette méthode devra être appelé à la fin de toutes les méthodes non-constantes de la classe (y compris les constructeurs) comme argument « ensures », car c'est bel et bien une post-condition déguisé. Pour plus de simplicité, je vous propose la macro « validate » qui s'utilise de cette façon :

  1. validate(*this);

Voilà, c'est tout pour le moment, si vous avez des questions/suggestions, n'hésitez pas, sinon, je vous conseille de regarder contracts.h qui est assez bien expliqué et les jeux de tests de ma bibliothèque C++ pour plus d'exemples.