Structures de données IFT-2000 Abder Alikacem Gestion des exceptions Édition Septembre 2009 Département d’informatique et de génie logiciel Département d’informatique et de génie logiciel
Erreur, exception … Solution objet Coder des exceptions en C++ Exceptions standard du langage Plan
Contexte Exemples de dysfonctionnements (ou anomalies) : Demander la valeur du sommet d ’une pile vide Empiler dans une pile pleine Etat des données peuvent rendre compte du dysfonctionnement exception contrôlée Exemples d ’erreurs (à l ’exécution) limite atteinte de la capacité mémoire delete sur un pointeur non défini paramètre de fonction invalide; … => arrêt brutal de l ’exécution du programme
Contexte retourner un code d'erreur assigner une valeur à une variable globale d'erreur ignorer l'erreur (!) imprimer un message d'erreur arrêter le programme lancer une exception. Réagir aux conditions d’erreurs au niveau du code
BUT On cherche à gérer les exceptions afin de Nettoyer les données, Rétablir l’application dans son fonctionnement normal, Terminer l’application. Une exception est l ’interruption gérée de l ’application à la suite d ’un dysfonctionnement. Le but de la gestion des exceptions est de réaliser des traitements spécifiques aux événements qui en sont la cause.
Solution traditionnelle Les fonctions susceptibles de subir des dysfonctionnements renvoient une information précisant : ou bien que l’application s ’est déroulée normalement ou bien qu’une anomalie X a été rencontrée Exemple : les traitements de la classe pile peuvent générer des dysfonctionnements quand la pile est vide (ex : dépiler) ou quand la pile est pleine (ex : empiler)
Solution traditionnelle # include enum exception {PILE_PLEINE, PILE_VIDE, OK }; template class Pile { public : Pile(void); // constructeur ~ Pile(void); // destructeur exception sommet (X&) ; // sélecteur : sommet bool estVide(void); // sélecteur : pile vide ? exception empiler (const X&); // empiler l ’élément donné exception depiler(void); // dépiler l ’élément placé au sommet private :... }; // Classe Pile Générique Traitement des exceptions par retour d'une valeur
Solution traditionnelle Les fonctions susceptibles de subir des dysfonctionnements renvoient une information précisant : ou bien que l’application s ’est déroulée normalement ou bien qu’une anomalie a été rencontrée INCONVENIENTS Nécessite un contrôle à chaque appel Solution avec des switch qui font alourdir le programme. Dans le code : mélange des traitements du fonctionnement normal et ceux gérant les exceptions. L’erreur n ’est pas forcément à corriger dans la méthode appelante directe. Les constructeurs et destructeurs n ’on pas de type retour! Traitement des exceptions par retour d'une valeur
Solution traditionnelle Traitement des exceptions à l’aide d’un variable globale d’erreur La plupart des fonctions de la librairie standard du C fonctionnent avec la célèbre variable globale errno. Exemple si on appelle unlink pour effacer un fichier, la fonction retourne 0 en cas de succès et -1 autrement. En cas d'erreur, on a assigné un code d'erreur à la variable globale errno : EACCES: Accès interdit ENOENT: Chemin d'accès ou fichier introuvable
Solution traditionnelle Traitement des exceptions à l’aide d’un variable globale d’erreur int main() { const char* fichierP = "log.bak"; if (unlink(fichierP) == -1) { if (errno==EACCES) { cout << "L'accès au fichier : " << fichierP << " est interdit" << endl; } else if (errno == ENOENT) { cout << "Le fichier ou le chemin d'acces" << " est introuvable" << endl; } return 0; }
Solution traditionnelle Traitement des exceptions à l’aide d’un variable globale d’erreur Problème important : approche non compatible avec une architecture multi-tâche. On doit protéger l'accès à cette variable globale... On peut perdre de l'information si deux erreurs se produisent coup sur coup. La première erreur est écrasée par la seconde potentiellement on peut mal interpréter la signification de l'erreur.
Solution traditionnelle Ignorer l’erreur Prendre la décision de ne rien faire en cas de demande erronée peut parfois être la seule solution «raisonnable». Beaucoup de fonctions de la librairie standard ont un comportement «non défini» lors d'appel invalide. Que ce passe-t-il si on tente d'accéder un vecteur en dehors de ses limites ? (On ne sait pas !!!) Ce style de programmation n'est pas à favoriser car il peut masquer des erreurs importantes.
Solution traditionnelle Imprimer un message d’erreur Imprimer un message d'erreur décrivant l'erreur : est acceptable pour un petit programme en phase de débogage. est inacceptable pour un logiciel commercial. L'utilisateur ne devrait voir que des messages qui ont du sens pour lui et sur lesquels il peut prendre action. Exemple : si on l'avertit que le disque est plein, il peut effacer des fichiers pour libérer de l'espace et poursuivre.
Solution traditionnelle Arrêter le programme En phase de développement, il est tout à fait acceptable d'arrêter le programme ou d'offrir de démarrer le débuggeur à l'endroit de l'erreur. Pour un produit commercial, ce n'est pas acceptable. L'usager peut avoir travaillé plusieurs minutes (heures) avant de perdre son travail (...) Au minimum, il faut enregistrer les documents modifiés sur disque et enlever les fichiers temporaires.
La solution : lancer une exception Les exceptions ont été ajoutées dans beaucoup de langages orienté objet dont le C++. Le mécanisme des exceptions est un moyen sûr et pratique : pour lancer des exceptions sur le site de l'erreur. transférer le contrôle à une autre partie du code qui pourra gérer la situation avec compétence. Exemple : «Incapable d'écrire ces bytes» pourrait être devenir «Disque plein» à un niveau supérieur.
La solution : lancer une exception IDÉE : Eviter d ’encombrer le code pour tester un comportement anormal => séparer le code propre à l’application de celui qui gère des dysfonctionnements. La fonction dans laquelle survient un dysfonctionnement se contente de lancer une exception. template Pile & Pile :: operator+= (const X& e){ if (indexsommet+1 <maximum) { indexsommet+=1; t[indexsommet]=e; return (*this); } else throw "débordement de pile"; }
La solution : lancer une exception Lorsque se produit une erreur qui ne peut être prise en charge à l'endroit de sa détection, une exception peut être lancée avec la commande throw : throw exp; où exp peut être n'importe quoi : un entier, une string, ou beaucoup mieux, un objet. throw "Division par zéro"; throw ERR_DIVISION_ZERO; throw DivParZeroException();
Structure de contrôle throw /...try/catch throw exp est une expression de type void Elle déroute l ’exécution normale d ’une fonction. L ’exécution peut alors être transférée à un autre point de l ’application pour traiter l ’anomalie (bloc try catch). L ’expression est de type quelconque sauf void. Elle est évaluée puis transmise aux fonctions en attente d ’exécution, de la dernière fonction appelée vers la première. Si rien de particulier n ’a été programmé pour traiter le problème alors le contrôle est passé à une fonction prédéfinie terminate. Par défaut, l ’action de la fonction terminate est d ’exécuter la fonction abort du langage C.
Structure de contrôle throw/...(try/catch) template Pile & Pile :: operator+= (const X& e){ if (indexsommet+1 <maximum) { indexsommet+=1; t[indexsommet]=e; return (*this); } else throw "débordement de pile"; } int main (void) {...p+=(...)... } terminate
Structure de contrôle throw/...(try/catch) Lorsqu'une fonction lance une exception, le mécanisme veut que le système se mette immédiatement à la recherche d'un site d'interception pour cette exception. La fonction ne retourne pas normalement, l'exécution de la fonction se termine immédiatement l'exécution est transférée au site d'interception le plus proche (pour cette exception).
Structure de contrôle throw/...try/catch Les sites d'interception des exceptions sont spécifiés à l'aide de blocs try-catch. try { // code pouvant lancer une exception } catch (Type-exception & e) { // e contient de l'information sur // l'exception. // code de gestion de l'exception }
Structure de contrôle throw/...try/catch Le bloc try peut être déclaré réceptif aux exceptions. Il est alors capable d ’attraper une exception. Syntaxe C++ : int main (void) { try {... p+=(...)... } try signifie que le bloc est candidat pour attraper une exception lancée par une fonction exécutée directement ou indirectement dans le bloc.
Structure de contrôle throw/...try/catch Le bloc doit être suivi d ’au moins un gestionnaire d ’exception qui récupère l ’expression (exp) lancée par une exception pour appliquer un traitement gérant cette exception, le bloc catch. Syntaxe C++ : int main (void) { try {... p+=(...)... } catch (char* msg) {... }
Structure de contrôle throw/...(try/catch) int main (void) { try {... p+=(...)... } catch (char* msg) {... }... } template Pile & Pile :: operator+= (const X& e){... else throw "débordement de pile"; } 1 2
Attraper une exception throw/...try/catch Lorsqu’une exception est lancée, c ’est l ’expression exp qui est lancée. Ensuite, commence la recherche du premier bloc try actif rencontré en remontant la pile des appels à partir de l ’appel de la fonction qui a déclenché l ’exception. Puis, on recherche le premier gestionnaire d ’exception (catch (T)) dont le type T appelé type du gestionnaire, est compatible avec le type de exp. Le contrôle est alors passé à la première instruction du bloc du gestionnaire de type compatible. Lorsque l ’exécution de ce gestionnaire se termine alors le contrôle est passé à la première instruction qui suit la structure try/catch
Exception non attrapée ? throw/...try/catch Si le type du premier gestionnaire rencontré est incompatible, le compilateur cherche un type de gestionnaire compatible dans les gestionnaires suivants (avec éventuellement remontée dans la pile des appels). Si aucun gestionnaire de type compatible est trouvé alors le compilateur exécutera automatiquement l ’instruction terminate. La compatibilité de types est étudié selon les compatibilités standard du langage C++.
Instructions throw; catch(…) int main (void) { try {... P+=(...)... } catch (char* msg) {... } catch (int n) {... } catch (...) { return 1; } Plusieurs catch peuvent apparaître après un try afin de recevoir et traiter plusieurs types d'erreurs. L'expression catch(...) permet de recevoir tous les types d'erreurs non capturés par les catch précédents.
Instruction throw; À l'occasion, nous voulons attraper toutes les exceptions (sans exception!!) qui passent à un endroit donné : pour faire une action protectrice pour faire un nettoyage quelconque. try { // code } catch (...) { // Gestion de n'importe quelle exception throw; // Relance l'exception }
Instruction throw; L ’instruction throw; dans un bloc catch signifie que l ’exception reçue doit continuer sa remontée à travers la pile des appels. int main (void) { try {... p.depilerNfois(z);... } catch (char* msg) {...//Traitement final } catch (...) { return 0; } void depilerNfois(int& n){ try {... for (int i=1; i<=n, i++) p.depiler();... } catch (char* msg) { n=i-1;//traitement partiel throw; } depiler
Instruction throw(...); Une fonction peut spécifier dans son entête les types d ’exceptions qu’elle est en mesure de lancer au bloc appelant. Pile & operator+= (const X& e) throw (char *); throw (char *); signifie que seules les exceptions char* peuvent être lancer par l’opérateur += Syntaxe C++ : TR fonction (T) throw(X,Y,…); La fonction peut lancer des exceptions de types X ou Y … ou compatibles avec ces types. Lorsque rien n’est précisé la fonction peut lancer des exceptions de n’importe quel type.
Instruction throw( ); Une fonction peut spécifier dans son entête qu’aucune exception ne peut être lancée au bloc appelant. Syntaxe C++ : TR fonction (T) throw( ); Aucune exception en être lancée par la fonction Normalisation dans l ’entête des méthodes. Contrôle à la compilation.
Conclusion throw/try/catch Le mécanisme de gestion des exception codée à l ’aide de la structure de contrôle throw/try/catch implémente un moyen de passer d ’un point de l ’exécution d ’un programme qui présente une anomalie à un autre qui traite cette anomalie. Ce passage est accompagné d ’un transfert d ’informations (exp). Ce saut d ’exécution s ’apparente à un appel de fonction avec passage d ’un argument, sauf que la portion de code cible de ce passage est inconnue à la compilation.
Solution objet Il est préférable d'utiliser les objets pour représenter les exceptions : utile pour spécifier le type de l'exception auquel on fait face ; permet de définir des sites d'interception basés sur le type d'exception ; permet de transporter de l'information dans l'objet du site de l'erreur jusqu'au site d'interception de l'exception.
Solution objet throw objet Exemple : throw PilePleine (…); Appel d ’un constructeur La fonction dans laquelle survient un dysfonctionnement lance une exception. Chaque exception est modélisée par une classe. Exemple : class PbPilePleine Il est donc intéressant de créer des types spéciaux pour gérer les exceptions car cela permet de faire remonter une exp plus complexe (plus d ’informations) et cela augmente la lisibilité et la maintenance de l ’application.
Exemple en C++ template Pile & Pile :: operator+= (const X& e){ if (indexsommet+1 <maximum) { indexsommet+=1; t[indexsommet]=e; return (*this); } else } template class Pile { public :... private : X * espace; int IndexSommet; int maximum; }; throw PbPilePleine(maximum); Appel d ’un constructeur
Classes gérant les exceptions // Déclaration des gestionnaires d'exception class PbPilePleine { private : int maxi; public : PbPilePleine (int t) //constructeur {maxi=t; } int GetMaxi(void)//sélecteur {return maxi;} }; class PbPileVide {... };
Exemple : try … catch int main () { string ch; pile PCar(ta); cout > ch; try{int L=ch.length(); int i=0; while (i<=L-1) {Pcar+=(ch[i]); i+=1;} cout << "\n Chaine inversée : "; while (! PCar.vide ( )) {cout <<PCar.sommet(); PCar.depiler ( ); } } catch (PbPilePleine & P){ //P est un paramètre dont l’argument est l ’instance créée // par throw cout << "\n PB de dépassement de taille de la pile"; cout << "\n Taille maximum fixée à : "<<P.S_Maxi(); return 1; } return 0; }
Gestionnaire d ’exception universel Il peut y avoir plusieurs gestionnaires d ’exception dans une application. Il est possible de définir un gestionnaire d ’exception universel. Le gestionnaire d ’exceptions universel récupérera toutes les exceptions possibles, quel que soit leur type. Syntaxe et exemple : // Déclaration des gestionnaires d'exception try { … } catch ( …) // traitement de toutes les autres exceptions { cout << " Exception inattendue " ; } Paramètre noté par trois points de suspension
Gestionnaire d ’exception universel - Exemple // Déclaration des gestionnaires d'exception class Erreur1... try { // calcul } catch (Erreur1 & e) { cout << " Erreur de type "<<1;} catch ( …) // traitement de toutes les autres exceptions { cout << " Exception inattendue " ; }
En détail (tout objet) Le type de ce paramètre (reçu par catch) est appelé type du gestionnaire. Ce paramètre peut être transmis par valeur ou par référence. S ’il est transmis par valeur : copie créée par le constructeur par copie. Le destructeur est appliqué sur l ’instance créée par throw en fin de catch. Un gestionnaire est doté d ’un et d ’un seul paramètre destiné à recevoir la valeur de l ’expression lancée.
En détail (tout objet) Les erreurs faisant chacune l ’objet d ’une classe, on peut les organiser par catégorie et les classer par héritage. Exemple Classe pbDePile avec deux classes dérivées PbPilePleine et PbPileVide. On pourra alors compléter l ’entête d ’une fonction qui traite d ’une pile avec throw (pbDePile) Un constructeur peut inclure un lancement d ’exception. C ’est d ’ailleurs, la seule solution pour gérer une erreur à la construction d ’une instance puisque le constructeur n ’a pas de type retour.
Exceptions standard du langage La bibliothèque standard utilise le mécanisme des exceptions du langage pour signaler les erreurs qui peuvent se produire au sein de ses fonctions. La STL fournit un certain nombre de classes d'exceptions standard, que les fonctions de la bibliothèque sont susceptibles de déclencher. Ces classes peuvent être utilisées telles quelles ou servir de classes de base à des classes d'exceptions personnalisées pour des développements propres.
Exceptions standard du langage std::exception std::logic_errorstd::runtime_error std::domain_error std::invalid_argument std::length_error std::out_of_range std::overflow_error std::underflow_error std::range_error std::bad_alloc
Gérer plus d’une exception À partir d'un bloc try-catch, on peut gérer plusieurs exceptions à la fois : try { // Code pouvant lancer des exceptions } catch (overflow_error& o) { // Gère des exceptions d'overflow } catch (underflow_error& u) { // Gère des exceptions d'underflow }
Gérer plus d’une exception Si le code dans le bloc try génère une exception de type overflow_error ou underflow_error, le traitement correspondant est activé. Si une exception d'un type différent est lancée, elle doit être gérée ailleurs sur un site d'interception du bon type. Si un tel site n'existe pas, le programme s'arrêtera en donnant une erreur du type "Unhandle exception".
Gérer plus d’une exception L'énoncé catch(...) avec les 3 petits points permet de gérer n'importe quel type d'exception. L'énoncé throw sans argument dans un bloc catch permet de relancer l'exception courante de nouveau. De fait, on doit toujours relancer une exception qu'on ne sait pas comment gérer (quoiqu'il aurait été préférable de ne pas l'avoir interceptée). Une exception non interceptée arrête le programme.
Hiérarchie des exceptions standard du langage L ’exception bad_alloc lancée par les gestionnaires de mémoire lorsque l ’instruction new ou new[] est en échec, dérive de la classe exception. Les autres exceptions sont classées en deux grandes catégories : 1/ erreurs de logique dans l ’écriture du programme 2/ erreurs d ’exécution La classe logic_error et ses dérivées signale des erreurs dues à la logique interne du programme. Ex: erreur sur un pointeur La classe runtime_error et ses dérivées signale des erreurs liées à l ’environnement du programme. Ex : pas assez de mémoire
Exceptions standard du langage Les classes gérant les exceptions dérivent toutes de la classe exception class exception { public: exception() throw(); //constructeur exception(const exception &) throw(); //constructeur exception& operator=(const exception &) throw(); virtual ~exception() throw(); //destructeur virtual const char *what() const throw(); };
Exceptions standard du langage class exception { public:... virtual ~exception() throw(); virtual const char* what() const throw(); }; La méthode what retourne une chaîne de caractères décrivant la nature de l’erreur qui s’est produite. Toutes les méthodes de la classe exception sont déclarées comme ne pouvant pas lancer d ’exception elles-mêmes. Evidemment puisqu’on est déjà en train de gérer une exception.
Erreurs de logique class logic_error : public exception { public: logic_error(const string& what_arg); }; Cette classe contient seulement un constructeur qui prend en argument une chaîne de caractères, qui permet de donner un message explicitant l’erreur. la classe domain_error dérive de la classe logic_error. La classe domain_error vérifie que les arguments d ’une fonction correspondent en nombre et compatibilité de types aux paramètres.
Erreurs d ’exécution class runtime_error : public exception { public: runtime_error(const string& what_arg); }; Par exemple la classe range_error dérive de la classe runtime_error. La classe range_error signale qu’une valeur est sortie de la plage de valeurs dans laquelle est devrait se situer.
La pile d’appel Gestion de la pile d'appel par le mécanisme des exceptions. Le flot linéaire d'exécution des instructions ne s'applique plus lorsque qu'une exception est lancée. Au moment où une exception se produit, le contrôle passe au site d'interception le plus proche pour cette exception. Tous les objets entre l'énoncé throw et l'énoncé catch seront détruits et leur destructeur est appelé. Normalement les objets sur la pile sont détruits lorsque l'exécution du programme atteint la fin d'un bloc ou d'une fonction.
La pile d’appel Le mécanisme des exceptions garantit que tous les destructeurs des objets sur la pile seront appelés. Après avoir lancé une exception et l'avoir récupéré, on peut continuer l'exécution. Gérer les ressources de façon cohérente en fonction de cette nouvelle réalité. point du catch point du throw main() f1() f2() f3() f4()
Gestion des ressources La possibilité qu'une exception puisse être lancée et qu'on ne puisse plus compter sur le flot linéaire d'exécution ajoute une préoccupation : Sommes-nous «exception-safe» ? Exemples: ifstream fichierP("input.dat"); foo(fichierP); // peut lancer une exception fichierP.close(); // peut ne jamais être exécuté Employe* eP = new Employe(); foo(eP); // peut lancer une exception delete eP; // peut ne jamais être libérée
Gestion des ressources ifstream fP ("input.dat"); try { foo (fP); } catch (...) { fp.close (); throw; } fclose (fP); Employe* eP = new Employe(); try { foo (eP); } catch (...) { delete eP; throw; } delete eP; Attraper toutes les exceptions, fermer le fichier ou libérer la mémoire et relancer l'exception est techniquement correct. Appliqué à grande échelle, la vie du programmeur deviendrait pire qu'avant...
Gestion des ressources Structurer le code pour bénéficier de la gestion automatique des ressources en utilisant les constructeurs et les destructeurs des classes. En utilisant une variable locale de type ifstream, on ouvre le fichier par le constructeur et le fichier est fermé par le destructeur : ifstream fs("input.dat"); foo (fs); Le fichier sera fermé lorsque le programme sera à la recherche d'un site d'interception pour une exception et que tous objets seront désalloués.
Gestion des ressources Même problème avec la mémoire allouée sur le monceau. On ne doit pas conserver de pointeur comme variable d'une fonction. La classe auto_ptr<> du standard permet de conserver les pointeurs dans des classes dont le destructeur gère la désallocation. #include #include "Employe.h" using namespace std; - Pointeur sur la pile dans une fonction Employe* eP = new Employe("Yves"); - Pointeur comme attribut d'un objet auto_ptr pstr(new Employe("Yves"));
Conclusion Intérêt : robustesse du programme Standardisation de la technique de report d ’erreurs Mécanisme : POO - Séparer code normal / gestion erreurs - Organisation par héritage des exceptions - Programmeur peut coder ses propres exceptions.
Exercice Coder une classe Rationnel qui gère une exception lorsque le dénominateur est égal à zéro. Opération : get_double qui retourne le quotient.