Licence CC BY-NC-SA

Les exceptions

Publié :

Je me suis efforcé depuis les premiers chapitres de vous apprendre à rédiger correctement vos algorithmes afin d'éviter toute erreur. Je vous ai régulièrement incité à penser aux cas particuliers, aux bornes des intervalles des tableaux, aux conditions de terminaison des boucles itératives ou récursives… Malheureusement, il y a des choses que vous ne pouvez pas prévoir car elle ne dépendent pas du programmeur mais de l'utilisateur : un fichier nécessaire à votre logiciel qui aurait été malencontreusement supprimé, un utilisateur qui n'aurait pas compris que le nombre entier demandé par le programme ne peut pas avoir de virgule ou que 'A' n'est pas un nombre, la mémoire est saturée…

Vos codes, pour l'heure, peuvent gérer certains de ces problèmes mais cela vous oblige généralement à les bidouiller et à les rendre illisibles (usage de boucles, saisie de texte converti ensuite en nombre…). Et qui plus est, vous ne pouvez gérer tous les cas de plantage. Heureusement, le langage Ada peut gérer ces erreurs grâces à ce que l'on appelle les exceptions. Les exceptions permettront de signaler le type d'erreur commis voire même de les traiter et de rétablir le fonctionnement normal du programme. Le langage Ada est même réputé pour la gestion des erreurs.

Fonctionnement d'une exception

Vous avez dit exception ?

Vous avez du régulièrement vous retrouver face au message suivante : raised CONSTRAINT_ERROR : ###.abd:.... Je vous ai même parfois incité à écrire des programmes menant à ce genre d'erreur.

Ah oui… le fameux plantage du Constraint_error ! Je ne sais même plus combien de fois il a accompagné le crash de mes programmes… :D

C'est certainement l'une des exceptions les plus courantes au début. L'exception CONSTRAINT_ERROR, indique que vous tentez d'accéder à un élément inexistant (cible d'un pointeur nul, accès à la 11ème valeur d'un tableau à 10 éléments…) ou qu'un calcul risque de générer un overflow (pour des rappels sur l'overflow, voir le premier exercice du chapitre sur la récursivité ou bien le chapitre traitant de la représentation des nombres entiers). Mais vous ne devez pas la voir comme un plantage de votre programme. En fait, Ada se rend compte à un moment donné qu'il y a une erreur et lève une exception afin d'éviter un véritable crash, c'est à dire le programme qui se ferme tout seul sans vous dire merci et votre système d'exploitation qui vous indique qu'il a rencontré un petit soucis.

Et ça nous avance à quoi que Ada s'en rende compte puisque de toutes façons le résultat est le même ? o_O

Que vous êtes défaitistes, c'est au contraire d'une grande aide :

  • Ada nous indique de quel type d'erreur il s'agit tout d'abord. Il nous indique également la ligne du code où le soucis est apparu et ajoute quelques informations supplémentaires. Ces indications sont très utiles à la fois pour la personne qui développe le programme, mais aussi pour celle qui sera chargée de sa maintenance.
  • Une fois l'exception connue, il sera possible d'effectuer un traitement particulier adapté au type d'erreur rencontrée, et parfois même de résoudre ce problème afin de rétablir un fonctionnement normal du programme.

Une exception n'est donc pas un simple plantage de votre programme mais plutôt une alerte concernant un évènement exceptionnel méritant un traitement approprié. Jusque là, aucun traitement n'étant proposé, Ada décidait de mettre lui-même fin au programme pour éviter tout problème.

Le fonctionnement par l'exemple

Testons par exemple le code suivant afin de comprendre ce qu'il se passe :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
procedure test_exception is

   type Tableau is array(1..10) of integer ; 

   procedure init(T : out Tableau ; dbt, fin : integer) is
   begin
      for i in dbt..fin loop
         T(i) := 0 ; 
      end loop ; 
   end init ; 

   T : Tableau ; 

begin
   init(T,0,11) ;      --Il y aura un gros soucis à ce moment là !
end test_exception ;

Vous devriez obtenir le doux message suivant : raised CONSTRAINT_ERROR : test_exception.adb:8 index check failed. Traduction : Ada a levé (raised) une exception de type CONSTRAINT_ERROR. Celle-ci a été déclenchée lors de l'exécution de l'instruction située à la ligne 8 de test_exception.adb. Information complémentaire : vous avez commis une erreur sur les indices du tableau.

Sauf que l'erreur dans ce code est à la ligne 15 et pas 8 ! Quelle idée d'initialiser 12 éléments dans un tableau de 10 ?! o_O

Reprenons le cheminement tranquillement. Votre programme commence par effectuer ses déclarations de types, variables et procédures. Jusque là, rien de catastrophique. Puis il exécute l'instruction Init() durant laquelle une boucle est exécutée qui demande l'accès en écriture à l'élément n°0 de notre tableau. C'est cette demande qui lève une exception. Que fait le programme ? Il cherche, au sein de la procédure Init(), une proposition de traitement lié à ce type d'erreur. Puisqu'il n'en trouve pas, il met fin à la procédure et répercute l'exception dans la procédure principale et recommence. Il cherche à nouveau un traitement lié à cette exception et comme il n'y en a toujours pas, il met fin à cette nouvelle procédure. Et comme il s'agit de la procédure principale, le programme prend fin en affichant l'exception rencontrée.

Traitement de l'exception

Comme le montre le schéma ci-dessous, la levée d'une exception ne génère pas automatiquement l'arrêt brutal de votre programme, mais plutôt un arrêt en chaîne si aucun traitement approprié n'a été prévu. Votre programme cherchera un remède à l'exception rencontrée au sein même du sous-programme responsable avant de transmettre ses ennuis au sous-programme qui l'avait auparavant appelé.

Traitement d'une exception

Le bloc EXCEPTION

Vous aurez également remarqué sur le schéma que les flèches indiquant la recherche d'un traitement des exceptions pointent toutes vers la fin des procédures. En effet, la levée d'une exception dans une procédure ou une fonction entraîne immédiatement son arrêt. Le programme «saute» alors à la fin de la procédure ou fonction pour y chercher un éventuel traitement. C'est donc à la toute fin de nos sous-programmes que nous allons tâcher de régler nos erreurs à l'aide du mot clé EXCEPTION. Vous allez voir, ce nouveau mot-clé a un fonctionnement analogue à celui de CASE :

1
2
3
4
5
6
7
8
9
procedure init(T : out Tableau ; dbt, fin : integer) is
begin
   for i in dbt..fin loop
      T(i) := 0 ; 
   end loop ; 
exception
   when CONSTRAINT_ERROR => Put_line("Tu pouvais pas reflechir avant d'ecrire tes indices ?") ; 
   when others           => Put_line("Qu'est-ce que t'as encore fait comme anerie ?") ; 
end init ;

Nous nous contenterons pour l'instant d'un simple affichage. Si vous testez de nouveau votre code, vous devriez obtenir le compliment suivant : Tu pouvais pas reflechir avant d'ecrire tes indices ?. Cela ne règle toujours pas notre problème mais nous avons progressé : plus de vilains affichages agressifs et incompréhensibles. Une autre solution aurait consisté à gérer l'exception non pas dans la procédure init() mais dans la procédure principale :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
procedure test_exception is

   type Tableau is array(1..10) of integer ;

   procedure init(T : out Tableau ; dbt, fin : integer) is
   begin
      for i in dbt..fin loop
         T(i) := 0 ;
      end loop ;
   end init ;

   T : Tableau ;

begin
   init(T,0,11) ;
exception
   when CONSTRAINT_ERROR => Put_line("Tu pouvais pas reflechir avant d'ecrire tes indices ?") ; 
   when others           => Put_line("Qu'est-ce que t'as encore fait comme anerie ?") ; 
end test_exception ;

Où et quand gérer une exception

Mouais, bah ça c'est typiquement le genre d'exemple inutile que j'aurais pu trouver tout seul. Tu chercherais pas à meubler parce que tu n'as plus rien à dire ?

Vous croyez ? :p Eh bien non, je n'essaie pas de meubler mais bien de vous présenter deux cas différents. Alors oui, je sais : au final, le programme affiche toujours la même phrase. Alors pour comprendre la différence, ajoutez cette ligne juste après « init(T,0,11) ; » et avant la fin du programme et le bloc de gestion des exceptions :

1
put_line("Initialisation reussie !") ;

Testez ensuite les deux cas. Quand le bloc EXCEPTION se trouve dans la procédure principale, rien ne change ! La procédure init() échoue toujours entraînant l'affichage de cette fameuse phrase de congratulation. :( Mais lorsque le bloc EXCEPTION se trouve au sein même de la procédure init(), l'affichage obtenu est tout autre :

1
2
Tu pouvais pas reflechir avant d'ecrire tes indices ?
Initialisation reussie !

Eh oui ! Le programme principal reprend son cours, comme si de rien n'était. Victoire ! Il ne plante plus !

Wouhouh ! Mais au fait, que vaut le tableau alors ? :o

C'est justement sur ce point que je souhaitais attirer votre attention. La levée de l'exception n'entraîne plus l'arrêt du programme grâce au traitement effectué, mais le problème n'a pas été résolu. Le programme principal continue donc comme si de rien n'était alors que le tableau qu'il était sensé initialiser n'a jamais vu la moindre affectation. Tentez un tri sur ce tableau ou la moindre lecture, et vous aurez droit de nouveau à une jolie exception. De ce simple exemple, nous devons tirer quelques enseignements :

  • Soit nous traitons cette erreur dans la procédure en cause, init(), et alors nous traitons le problème à fond afin de retourner un tableau vraiment initialisé :
1
2
3
EXCEPTION
   WHEN CONSTRAINT_ERROR => Put_line("Il y a eu un petit contre-temps, mais c'est fait.") ;
                            T := (OTHERS => 0) ;
  • Soit nous propageons l'exception au programme principal sciemment à l'aide du mot clé RAISE :
1
2
3
EXCEPTION
   WHEN CONSTRAINT_ERROR => Put_line("Il y a eu un petit soucis.") ;
                            RAISE ;
  • Soit nous ne nous en préoccupons que dans le programme principal en considérant que le problème provient de là et qu'il doit donc être traité par la procédure test_exception().

Enfin, quel que soit le cas de figure que vous choisirez, gardez en mémoire le petit schéma vu précédemment : avant de traiter une exception vous devez réfléchir à sa propagation et au meilleur moment de la traiter.

Exceptions prédéfinies

Exceptions standards

Le langage Ada dispose nativement de nombreuses exceptions prédéfinies. Nous avons jusque là traité de l'exception CONSTRAINT_ERROR qui est levée lors d'une erreur sur l'indexation d'un tableau, lors de l'accès à un pointeur NULL, d'un overflow, d'une opération impossible (division par 0 par exemple). Mais d'autres existent et sont définies par le package Standard :

  • PROGRAM_ERROR : celle-là, vous ne l'avez probablement jamais vue. Elle est appelée lorsqu'un programme fait appel à un sous-programme dont le corps n'a pas encore été rédigé ou lorsqu'une fonction se termine sans être passée par la case RETURN. Cela peut arriver notamment lors d'une erreur avec une fonction récursive (nous verrons un exemple juste après).
  • STORAGE_ERROR : cette exception est levée lorsqu'un programme exige plus de mémoire qu'il n'en a le droit. Cela arrive régulièrement avec des programmes récursifs mal ficelés : une boucle récursive infinie est engendrée, réquisitionnant plus de mémoire à chaque appel. L'ordinateur met fin à cette gabegie lorsque la mémoire demandée devient trop importante.
  • TASKING_ERROR : Celle-là non plus, vous n'avez pas du la voir beaucoup, et pour cause elle est levée lorsqu'un problème de communication apparaît entre deux tâches. Mais comme vous ne savez pas encore ce qu'est une tâche en Ada, cela ne vous avance pas à grand chose, patience. :p

Exceptions et fonctions récursives

Avant de présenter de nouvelles exceptions, attardons-nous sur le cas des fonctions récursives évoqué ci-dessus. Voici une fonction toute simple qui divise un nombre par 4, 3, 2, 1 puis 0. Bien entendu, la division par 0 engendrera une exception :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
with ada.integer_text_io ;          use ada.integer_Text_IO ; 

procedure Test_exception_recursive is

   function division(a : integer ; n : integer) return integer is
   begin
      put(a) ; 
      return division(a/n,n-1) ; 
   end division ;

begin
   put(division(125, 4) ) ; 
end test_exception_recursive ;

Comme attendu, nous nous retrouvons avec une belle exception :

1
2
125     31     10     5     5
raised CONSTRAINT_ERROR : test_exception_recursive.adb:8 divide by zero

Nous allons résoudre ce problème à l'aide de nos exceptions (oui, je sais, il y a plus simple mais c'est un exemple :-° ) :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
with ada.Text_IO ;                  use ada.Text_IO ; 
with ada.integer_text_io ;          use ada.integer_Text_IO ; 

procedure Test_exception_recursive is

   function division(a : integer ; n : integer) return integer is
   begin
      put(a) ; 
      return division(a/n,n-1) ; 
   exception
      when others => Put_line("Division par zero !") ; 
   end division ;

begin
   put(division(125, 4) ) ; 
end test_exception_recursive ;

Et que se passe-t-il désormais ?

1
2
3
4
5
6
125     31     10     5     5Division par zero !
Division par zero !
Division par zero !
Division par zero !
Division par zero !
raised PROGRAM_ERROR : test_exception_recursive.adb:8 missing return

Pourquoi Ada lève-t-il une exception ? On les avait toutes traitées avec EXCEPTION et WHEN OTHERS, non ?

Pas tout à fait. Nous avons réglé le cas des exceptions levées lors de la fonction. Notre fonction division() n°1 (avec n valant 4) effectue quatre appels récursifs. Et l'appel division() n°5 (avec n valant 0) lève une exception (la division par zéro) au lieu de renvoyer un integer. Du coup, le même phénomène se produit pour les division() n°4, 3, 2 puis 1 ! Tout cela est très bien, mais il n'empêche que notre programme principal attend que la fonction lui renvoie un résultat, ce qu'elle ne peut pas faire puisque notre traitement des exceptions ne contient pas d'instruction RETURN. Ce qui lève donc cette fameuse exception : PROGRAM_ERROR. Encore une fois, il est important de réfléchir à la propagation d'une exception avant de se lancer dans un traitement hasardeux.

Autres exceptions prédéfinies

D'autres exceptions sont définies par le langage dans divers packages. Mais vous avez du en rencontrer beaucoup lorsque vous avez travaillé sur les fichiers : lorsque le mode n'était pas bon, que vous n'aviez pas pensé à gérer les fins de fichiers, ou que le programme ne trouvait pas votre fichier pour une toute petite erreur d'haurtogaffe. Ces exceptions concernant les entrées-sorties sont définies dans le package Ada.IO_Exceptions (a-ioexce.ads) :

  • Status_Error : levée lorsque le status d'un fichier (ouvert/fermé) est erroné. Par exemple, fermer ou lire un fichier déjà fermé.
  • Mode_Error : levée lorsque le mode d'un fichier (in_file/out_file/append_file) est erroné. Par exemple, si vous tentez d'écrire dans un fichier en lecture seule.
  • Name_Error : levée lorsque le nom d'un fichier est erroné, c'est-à-dire quand il est introuvable.
  • Use_Error : levée lorsque l'usage d'un fichier ne correspond pas à son type. Pour rappel, nous avions vu trois types de fichiers : texte, séquentiel et à accès direct.
  • Device_Error : levée lors de problèmes liés au matériel.
  • End_Error : levée lorsque vous tentez de lire au delà de la fin du fichier (pensez à utiliser End_of_page(), End_of_line() et End_of_file() ).
  • Data_Error : levée lorsque l'utilisateur fournit des données du mauvais type. Par exemple, lorsque le programme demande un entier et que l'utilisateur lui renvoie un 'A'.
  • Layout_Error : levée lorsqu'un string est trop long pour être affiché.

Créer et lever ses propres exceptions

Déclarer une exception

Supposons que vous soyez en pleine conception d'un programme d'authentification. Votre programme doit saisir un identifiant composé de 5 lettres ainsi qu'un mot de passe comportant 6 lettres suivies de 2 chiffres et les enregistrer dans un fichier. La partie sensible se pose au moment de la saisie du mot de passe : on ne va pas enregistrer un mot de passe incorrect ! Vous avez donc rédigé pour cela une fonction mdp_correct() qui renvoie FALSE en cas d'erreur.

Et justement s'il y a erreur, votre programme devra lever une exception. Nous appellerons notre exception PWD_ERROR. Notez au passage que j'écrirai toujours mes exceptions en majuscules et les nommerai toujours avec le suffixe ERROR. Une autre solution consiste à les préfixer avec EXC, de la même manière que nos types étaient préfixés par un T_ et nos packages par un P_.

La déclaration de l'exception se fait en même temps que les déclarations de types ou de variables de la façon suivante :

1
PWD_ERROR : EXCEPTION ;

Lever sa propre exception

C'est bien gentil d'avoir ses propres exceptions, mais comment Ada peut-il savoir quand la lever ? Ce n'est pas une exception standard !

Cela se fera à l'aide du mot clé RAISE, déjà vu précédemment. Voici donc un exemple de programme :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
procedure Authentification is
   PWD_ERROR : exception ;
   login     : string(1..5) ; 
   pwd       : string(1..8) ; 
begin
   login := saisie_login ; 
   pwd   := saisie_pwd ; 
   if mdp_correct(pwd)
      then Enregistrement(login, pwd) ; 
      else raise PWD_ERROR ;
   end if ; 
exception
   when PWD_ERROR => put_line("Mot de passe incorrect !") ; 
   when others    => put_line("Le programme a rencontré une erreur fatale !") ; 
end Authentification ;

Et de la même manière qu'avec une exception standard, l'instruction RAISE va mettre fin à notre procédure et effectuer un bond vers le bloc de traitement pour trouver une réponse adaptée. Il est également possible avec l'instruction WITH d'indiquer immédiatement à la levée de l'exception le message d'alerte qui sera affiché.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
procedure Authentification is
   PWD_ERROR : exception ;
   login     : string(1..5) ; 
   pwd       : string(1..8) ; 
begin
   login := saisie_login ; 
   pwd   := saisie_pwd ; 
   if mdp_correct(pwd)
      then Enregistrement(login, pwd) ; 
      else raise PWD_ERROR with "Mot de passe incorrect !" ; 
   end if ; 
exception
   when PWD_ERROR => null ; 
   when others    => put_line("Le programme a rencontré une erreur fatale !") ; 
end Authentification ;

Propagation de l'exception

Mais si la procédure Authentification() fait partie d'un programme plus vaste, que va-t-il se passer pour le programme principal ? L'authentification n'aura pas eu lieu et le programme principal ne connaîtra pas mon exception puisqu'elle est définie seulement dans la sous-procédure.

Une solution consiste à propager l'exception aux programmes appelants en écrivant :

1
WHEN PWD_ERROR => put_line("Mot de passe incorrect !") ; RAISE ;

Mais le programme principal ne connaîtra pas PWD_ERROR, car sa portée est limitée à la procédure Authentification(). Le programme principal devra donc comporter un bloc EXCEPTION dans lequel PWD_ERROR sera récupéré par l'instruction WHEN OTHERS.

Rectifier son code

Le retour du bloc DECLARE

Tout cela c'est bien beau, mais ça ne répare pas les bêtises. :( Il vaudrait mieux demander à l'utilisateur de recommencer plutôt que de propager PWD_ERROR. Mais comment faire puisque le bloc EXCEPTION est forcément à la fin de la procédure ?

Attention, le bloc exception doit se trouver à la fin d'un autre bloc mais pas nécessairement à la fin du sous-programme. Il peut donc terminer une FUNCTION, une PROCEDURE ou un DECLARE. Et c'est ce dernier que nous allons utiliser :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
procedure Authentification is
   login     : string(1..5) ; 
   pwd       : string(1..8) ; 
begin
   login := saisie_login ; 
   pwd   := saisie_pwd ; 
   declare
      PWD_ERROR : exception ;
   begin
      if mdp_correct(pwd)
         then Enregistrement(login, pwd) ; 
         else raise PWD_ERROR ;
      end if ; 
   exception
      when PWD_ERROR => put_line("Mot de passe incorrect !") ; 
      when others    => put_line("Le programme a rencontré une erreur fatale !") ; 
   end ; 
end Authentification ;

Une autre façon, plus élégante et moins lourde consiste à créer un simple bloc introduit par BEGIN sans rien déclarer :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
procedure Authentification is
   PWD_ERROR : exception ;
   login     : string(1..5) ; 
   pwd       : string(1..8) ; 
begin
   login := saisie_login ; 
   pwd   := saisie_pwd ; 
   TRY : begin
      if mdp_correct(pwd)
         then Enregistrement(login, pwd) ; 
         else raise PWD_ERROR ;
      end if ; 
   exception
      when PWD_ERROR => put_line("Mot de passe incorrect !") ; 
      when others    => put_line("Le programme a rencontré une erreur fatale !") ; 
   end TRY ; 
end Authentification ;

Je conserverai cette seconde solution par la suite car elle a le mérite de ne rien déclarer en cours de route (pratique à bannir autant que possible) et de faire apparaître clairement un bloc de test (rappelons que « try » signifie « essayer ») dans lequel pourra être levé une exception. On parle alors de zone critique. Venons-en maintenant à la façon de rectifier notre code. Il y a deux grandes options.

Les boucles

La première option consiste à enfermer notre section critique dans une boucle dont on ne sort qu'en cas de réussite :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
loop
   TRY : begin
      if mdp_correct(pwd)
         then Enregistrement(login, pwd) ; 
              exit ;                 -- on peut même clarifier ce exit en nommant la boucle. 
         else raise PWD_ERROR ;
      end if ; 
   exception
      when PWD_ERROR => put_line("Mot de passe incorrect !") ; 
      when others    => put_line("Le programme a rencontré une erreur fatale !") ; 
                        raise ; 
   end TRY ; 
end loop ;

Si le mot de passe est valide, il est enregistré et le programme sort de la boucle. La levée de l'exception empêche donc la sortie de boucle et la portion de code est ainsi répétée. Une variante pourrait consister à proposer seulement trois essais et à clore le programme ensuite.

GOTO is back !

Cette solution, si elle a ses adeptes, me semble plutôt lourde et oblige à ouvrir deux blocs : un bloc itératif et un bloc déclaratif ! Et par conséquent, cela nous oblige à fermer deux blocs. Question efficacité, on a déjà fait mieux. :( Ma solution privilégiée est de faire appel à une instruction que je vous avais jadis interdite : GOTO ! :ninja:

Son inconvénient majeur était qu'elle ne réalisait pas de blocs clairement identifiables et générait généralement du code dit «spaghetti» où les chemins suivis par le programme s'entremêlent au point que plus personne n'ose y mettre le nez. Mais dans le cas présent nous avons créé un bloc spécifique, alors profitons-en pour faire appel (proprement) à cette vieille instruction :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<<TRY>>
begin
   if mdp_correct(pwd)
      then Enregistrement(login, pwd) ; 
      else raise PWD_ERROR ;
   end if ; 
exception
   when PWD_ERROR => put_line("Mot de passe incorrect !") ; 
                     goto TRY ; 
   when others    => put_line("Le programme a rencontré une erreur fatale !") ; 
                     raise ; 
end ;

Assertions et contrats

Assertions

Après les exceptions, les assertions ! Qu'est-ce que c'est encore que ça ?

Une assertion est simplement un test rapide permettant de contrôler certains paramètres de votre programme et de lever une exception particulière (ASSERTION_ERROR) si les conditions ne sont pas remplies. Mais avant de vous expliquer sa mise en oeuvre, je dois vous parler des PRAGMA.

Peut-être avez-vous déjà vu cette instruction en feuilletant divers packages sans en comprendre le sens. Les PRAGMA sont ce que l'on appelle en français des directives de compilateur, c'est-à-dire des instructions qui s'adressent non pas au programme et à ses utilisateurs mais à votre bon vieux compilateur GNAT. Elle sont là pour lui donner des instructions sur la façon de compiler, de comprendre votre code, de le contrôler…

Mais ces directives sont également là pour éprouver votre code, vous apporter des garanties de fiabilité. Elles peuvent s'écrire durant les déclarationS ou le déroulement de vos programmes et s'utilisent de la manière suivante :

1
PRAGMA Nom_de_directive(parametres_eventuels) ;

Il existe des dizaines et des dizaines de directives différentes. Certains compilateurs fournissent d'ailleurs leurs propres directives. Celle dont nous allons parler est heureusement utilisée par tous les compilateurs, il s'agit de la directive Assert (comme assertion, vous l'aurez compris). Reprenons le cas de notre mot de passe. Plutôt que de lever notre propre exception, il est possible de vérifier l'assertion (ou affirmation) suivante : «le mot de passe est correct». Si cette assertion est vérifiée, on continue notre bout de chemin. Sinon, l'exception ASSERTION_ERROR sera automatiquement levée. Reprenons notre exemple :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
procedure Authentification is
   login     : string(1..5) ; 
   pwd       : string(1..8) ; 
begin
   login := saisie_login ; 
   pwd   := saisie_pwd ; 
   pragma assert(mdp_correct(pwd)) ; 
   Enregistrement(login, pwd) ; 
exception
   when ASSERTION_ERROR => put_line("Mot de passe incorrect !") ; 
   when others    => put_line("Le programme a rencontré une erreur fatale !") ; 
end Authentification ;

La directive Assert prend en paramètre une expression booléenne. Il est ainsi possible de vérifier la valeur d'un nombre ou d'effectuer un test d'appartenance en écrivant :

1
2
3
PRAGMA Assert(x=0) ; 
   --   OU
PRAGMA Assert(MonObjet IN MonType'class) ;

Assert peut également prendre un second paramètre. On indique alors avec une chaîne de caractères le message à afficher en cas d'erreur :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
procedure Authentification is
   login     : string(1..5) ; 
   pwd       : string(1..8) ; 
begin
   login := saisie_login ; 
   pwd   := saisie_pwd ; 
   pragma assert(mdp_correct(pwd),"Mot de passe incorrect !") ; 
   Enregistrement(login, pwd) ; 
exception
   when ASSERTION_ERROR => null ; 
   when others    => put_line("Le programme a rencontré une erreur fatale !") ; 
end Authentification ;

Supprimer les contrôles du compilateur

Tant que nous parlons de PRAGMA, je vais vous présenter une seconde directive de compilateur : suppress. Cette directive permet de lever le contrôle de certaines exceptions effectué par le compilateur.

Par exemple :

1
PRAGMA suppress(overflow_check) ;

Supprimera le test d'overflow lié à l'exception CONSTRAINT_ERROR sur le bloc entier. De même, si vous disposez d'un type T_tableau, vous pourrez supprimer le contrôle des indices sur tous les types tableaux de tout le bloc en écrivant :

1
PRAGMA suppress(index_check) ;

ou bien ne supprimer ces tests que sur les variables composites de types T_Tableau en écrivant :

1
PRAGMA suppress(overflow_check, T_Tableau) ;

Voici les tests de l'exception CONSTRAINT_ERROR que vous pourrez supprimer avec « PRAGMA suppress » : access_check, discriminant_check, index_check, length_check, range_check, division_check, overflow_check. Mais comme je doute que vous ayez souvent l'occasion de les mettre en œuvre, je ne m'étendrai pas plus longtemps sur cette notion (la plupart de ces noms évoquant d'ailleurs clairement le test effectué).

Programmation par contrats (Ada2012 seulement)

Ce qui suit ne concerne que les détenteurs d'un compilateur compatible avec la norme Ada2012. Si vous disposez d'une version de GNAT antérieure, cela ne fonctionnera pas.

Avec la norme Ada 2012, il est possible d'effectuer automatiquement des assertions à l'entrée et à la sortie d'une fonction ou d'une procédure. Ces conditions d'application de la fonction sont appelées contrats. Pour bien comprendre, changeons d'exemple. Prenons une fonction qui effectue la division de deux natural :

1
function division(a,b : natural) return natural ;

Cette fonction doit remplir quelques conditions. Une précondition tout d'abord : le nombre b ne doit pas valoir 0 (je rappelle qu'il est impossible de diviser par zéro :colere2: ). Nous allons donc affecter un contrat à notre fonction :

1
2
function division(a,b : natural) return natural
   with Pre => (b /= 0) ;

Une exception sera donc levée ou GNAT vous avertira si jamais vous veniez à diviser par 0. Mais notre fonction doit également remplir une postcondition : le résultat obtenu doit être plus petit que le paramètre a (c'est une division entière). Nous allons donc affecter un second contrat :

1
2
3
function division(a,b : natural) return natural
   with Pre => (b /= 0) ,
   with Post => (dision(a,b) < a) ;

Cette programmation par contrat est une des grandes nouveautés de la norme Ada2012, empruntée à des languages tels Eiffel ou D. Elle peut également s'appliquer aux types. Prenons l'exemple d'un type T_Date dont vous souhaiteriez qu'il soit et demeure valide (pas de 35/87/1901 par exemple). Vous définirez alors un invariant de type :

1
2
3
4
type T_Date is record
   jour,mois,annee : integer ; 
end record 
with Type_Invariant => (jour in 1..31 and mois in 1..12 and annee in 1900..2100) ;

Ces nouveautés apportées par la norme Ada2012 vous permettront de mieux sécuriser votre code et de faire reposer une partie du travail de débogage sur le compilateur. Vous savez désormais que le langage Ada est réputé pour sa fiabilité : ses compilateurs ne laissent rien passer ce qui en fait le langage de prédilection d'industries de pointe comme l'aéronautique, l'aérospatiale ou le nucléaire. La programmation par contrats vient ainsi renforcer ces caractéristiques. Plus que jamais, « avec Ada, si ça compile, c'est que ça marche ».


En résumé :

  • Lorsqu'une erreur est détectée à l'exécution du programme, celui-ci lève une EXCEPTION qui met fin au sous-programme en cour.
  • Pour capturer une exception et y apporter un traitement, ajoutez un bloc « EXCEPTION WHEN » à la fin de votre programme.
  • L'instruction RAISE permet de lever vous-même une exception. Dans le bloc de traitement des exceptions, elle répercute au programme appelant le traitement de l'exception.
  • Si vous souhaitez reprendre le cours de votre programme après avoir traité une exception, faites appels à l'instruction GOTO.
  • Le traitement simple des exceptions peut être accéléré en faisant appel à la directive de compilateur « PRAGMA Assert() » ou, avec la norme Ada2012, en utilisant la programmation par contrats.