Aller au menu - Aller au contenu

Icône La gestion des erreurs avec les exceptions

Mise à jour : 27/05/2011
Difficulté : Intermédiaire Intermédiaire Durée d'étude : 1 jour Creative Commons BY-NC-SA
75 639 visites depuis 7 jours, dont 380 sur ce chapitre classé 5/786
Jusque là, nous avons toujours supposé que tout se déroulait bien dans nos programmes. Mais ce n'est pas toujours le cas, des problèmes peuvent survenir. Pensez par exemple aux cas suivants:
  • Un problème à l'ouverture d'un fichier.
  • La connexion au serveur de chat qui n'arrive à se faire.
  • On a entièrement rempli la mémoire de l'ordinateur.
  • On accède à la case n°12 d'un tableau de 7 éléments.

Les exceptions sont un moyen de gérer efficacement les erreurs qui pourraient survenir dans votre programme ; on peut alors tenter de traiter ces erreurs, remettre le programme dans un état normal et reprendre l'exécution du programme.

Dans ce chapitre, je vais vous apprendre à créer des exceptions, à les traiter et à sécuriser vos programmes en les rendant plus robustes.
Sommaire du chapitre :
Icône du chapitre
Chapitre précédent Sommaire Chapitre suivant

Un problème bien ennuyeux

En programmation, quel que soit le langage utilisé (et donc en C++ :p ), il existe plusieurs types d'erreurs qui peuvent survenir. Parmi les erreurs possibles, on connaît déjà les erreurs de syntaxe qui surviennent lorsque l'on fait une faute dans le code source, par exemple si l'on oublie un point-virgule à la fin d'une ligne.
Ces erreurs sont facilement corrigées car le compilateur peut les signaler.

Un autre type de problèmes peut survenir si le programme est syntaxiquement correct mais qu'il exécute une action interdite. On peut citer comme exemple les cas où l'on essaye de lire la 10ème case d'un tableau de 8 éléments ou encore le calcul de la racine carrée d'un nombre négatif.
On appelle ces erreurs les erreurs d'implémentation.

La gestion des exceptions permet, si elle est réalisée correctement, de corriger les erreurs d'implémentation en les prévoyant à l'avance. Ceci n'est pas toujours réalisable, car il faudrait penser à toutes les erreurs qui pourraient survenir, mais on peut facilement en éviter une grande partie.
Le plus simple pour comprendre le but de la gestion des exceptions est de prendre un exemple concret.

Exemple d'erreur d'implémentation



Cet exemple n'est pas très original (on le trouve dans presque tous les livres), mais c'est certainement parce que c'est un des cas les plus simples.

Imaginons que vous ayez décidé de réaliser une calculatrice. Vous auriez par exemple pu coder la division de deux nombres entiers de cette manière :

Code : C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
int division(int a,int b) // Calcule a divisé par b.
{
   return a/b;
}
 
int main()
{
   int a,b;
   cout << "Valeur pour a : ";
   cin >> a;
   cout << "Valeur pour b : ";
   cin >> b;
 
   cout << a << " / "<< b << " = " << division(a,b) << endl;
   
   return 0;
}


Ce code est tout à fait correct et fonctionne parfaitement. Sauf dans un cas : si b vaut 0. En effet la division par 0 n'est pas une opération arithmétique valide. Si on lance le programme avec b=0, on obtient une erreur et le message suivant s'affiche :

Code : Console
Valeur pour a : 3
Valeur pour b : 0
Exception en point flottant (core dumped)


Il faudrait donc ne pas réaliser le calcul si b vaut 0, mais que faire à la place ?

Quelques mauvaises solutions



Une première possibilité serait de renvoyer un nombre prédéfini à la place du résultat. Ce qui donnerait par exemple :
Code : C++
1
2
3
4
5
6
7
int division(int a,int b) // Calcule a divisé par b.
{
   if( b!=0)   // Si b ne vaut pas 0.
      return a/b;
   else         // Sinon.
      return ERREUR;
}


En spécifiant une valeur précise pour ERREUR. Mais cela pose un nouveau problème, quelle valeur choisir pour ERREUR ? On ne peut pas renvoyer un nombre puisque tous les nombres pourraient être renvoyés par la fonction dans un cas normal. Ce n'est donc pas une bonne solution.

Une autre idée que l'on rencontre souvent, c'est d'afficher un message d'erreur, ce qui donnerait quelque chose comme :

Code : C++
1
2
3
4
5
6
7
int division(int a,int b) // Calcule a divisé par b.
{
   if( b!=0)   // Si b ne vaut pas 0.
      return a/b;
   else         // Sinon.
      cout << "ERREUR: Division par 0 !" << endl;
}


Mais cela pose deux nouveaux problèmes, la fonction ne renvoie aucune valeur en cas d'erreur et un effet de bord se produit ; en effet la fonction division n'est pas forcément censée utiliser cout surtout si par exemple, on a réalisé un programme avec une GUI comme Qt par exemple.

La 3e et dernière solution, que l'on rencontre parfois dans certaines bibliothèques, est de modifier la signature et le type de retour de la fonction de la manière suivante :

Code : C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
bool division(int a,int b, int& resultat)
{
   if(b!=0)     // Si b est différent de 0.
   {
       resultat = a/b;   // On effectue le calcul et on met le résultat dans la variable passée en argument.
       return true;        // On renvoie vrai pour montrer que tout s'est bien passé.
   }
   else        // Sinon
       return false;       // On renvoie false pour montrer qu'une erreur s'est produite.
}


Cette solution est la meilleure des 3 proposées (ceux qui connaissent le C sont habitués à ces choses), mais elle souffre d'un gros problème, elle n'est pas du tout intuitive à utiliser. Il est en particulier impossible de réaliser le calcul {a}/(b / c) de manière simple et intuitive.

Ces 3 solutions proposées sont là à titre d'illustration de ce qu'il ne faut pas faire. La bonne solution est présentée dans la suite.

La gestion des exceptions

Voyons comment résoudre ce problème de manière élégante en C++.

Principe général



Le principe général des exceptions est le suivant :
  • On crée des zones où l'ordinateur va essayer le code en sachant qu'une erreur peut survenir.
  • Si une erreur survient, on la signale en lançant un objet qui contient des informations sur l'erreur.
  • À l'endroit où l'on souhaite gérer les erreurs survenues, on attrape l'objet et on gère l'erreur.


C'est un peu comme si vous étiez coincé sur une île déserte. Vous lanceriez une bouteille à la mer avec des informations dedans permettant de vous retrouver. Il n'y aurait alors plus qu'à espérer que quelqu'un attrape votre bouteille. Sinon vous mourrez de faim.
C'est la même chose ici, on lance un objet en espérant qu'un autre bout de code le rattrapera, sinon le programme plantera.

Dans le principe général, j'ai volontairement mis 3 mots en rouge. Ces 3 mots correspondent aux 3 mots-clés qui sont utilisés par le mécanisme des exceptions.

  • try{ ... }(essaye en français) permet de signaler une portion de code où une erreur peut survenir.
  • throw(lance en français) permet de signaler l'erreur en lançant un objet.
  • catch(...){...}(attrape en français) permet d'introduire la portion de code qui va récupérer l'objet et s'occuper de gérer l'erreur.


Voyons cela plus en détail.

Les 3 mots-clés en détail



Commençons par try, il est très simple d'utilisation. Il permet d'introduire un bloc sensible aux exceptions. C'est-à-dire qu'on indique au compilateur qu'une certaine portion du code source pourrait lancer un objet (la bouteille à la mer).

On l'utilise comme ceci :
Code : C++ - Le mot-clé try
1
2
3
4
5
// Du code sans risque.
try
{
   // Du code qui pourrait créer une erreur.
}


Entre les accolades du bloc try on peut trouver n'importe quelle instruction C++, notamment un autre bloc try.

Le mot-clé throw est lui aussi très simple d'utilisation. C'est grâce à lui qu'on lance notre bouteille. La syntaxe est la suivante : throw expression

On peut lancer n'importe quoi comme objet, par exemple un int qui correspond au numéro de l'erreur ou une string contenant le texte de l'erreur. On verra plus loin un type d'objet particulièrement utile pour les erreurs.

Code : C++ - Le mot-clé throw
1
2
3
4
5
6
7
throw 123;   // On lance l'entier 123, par exemple si l'erreur 123 est survenue.
 
throw string("Erreur fatale. Contactez un administrateur"); // On peut lancer une string.
 
throw Personnage; // On peut tout à fait lancer une instance d'une classe.

throw 3.14 * 5.12; // Ou même le résultat d'un calcul


throw peut se trouver n'importe où dans le code, mais s'il n'est pas dans un bloc try, l'erreur ne pourra pas être rattrapée et le programme plantera.


Terminons avec le mot-clé catch. Il permet de créer un bloc de gestion d'une exception survenue. Il faut créer un bloc catch par type d'objet lancé. Chaque bloc try doit obligatoirement être suivi d'un bloc catch. De manière réciproque, tout bloc catch doit être précédé d'un bloc try ou d'un autre bloc catch.

La syntaxe est la suivante : catch (type const& e){}

On attrape les exceptions par référence constante (d'où la présence du &) et pas par valeur, ceci afin d'éviter une copie et de conserver le polymorphisme de l'objet reçu. Souvenez-vous des ingrédients nécessaires au polymorphisme, une référence ou un pointeur sont nécessaires. Comme l'objet lancé pourrait avoir des fonctions virtuelles, on l'attrape via une référence de sorte que les deux ingrédients soient réunis.


Ce qui donne par exemple :

Code : C++ - Le mot-clé catch
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
try
{
    // Le bloc sensible aux erreurs.
}
catch(int e)  // On rattrape les entiers lancés (pour les entiers, une référence n'a pas de sens).
{
   // On gère l'erreur.
}
catch(string const& e)  // On rattrape les strings lancés.
{
   // On gère l'erreur.
}
catch(Personnage const& e) // On rattrape les personnages.
{
   // On gère l'erreur.
}


Vous pouvez mettre autant de blocs catch que vous voulez. Il en faut au moins un par type d'objet pouvant être lancé.


Qu'est-ce que ça va changer durant l'exécution du programme ?


À l'exécution, le programme va se dérouler normalement comme si les instructions try et les blocs catch n'étaient pas là.
Par contre, au moment où l'ordinateur arrive sur une instruction throw, il va sauter toutes les instructions suivantes, appeler le destructeur de tous les objets déclarés à l'intérieur du bloc try. Il va chercher le bloc catch qui correspond à l'objet qui a été lancé.
Arrivé au bloc catch, il va exécuter ce qui se trouve dans le bloc et reprendre l'exécution du programme après le bloc catch.

Je me répète, mais c'est une erreur courante. L'exécution reprend après le bloc catch et pas à l'endroit où se trouve le throw.


Le mieux pour comprendre le fonctionnement est encore de reprendre l'exemple de la calculatrice et de la division par 0.

La bonne solution



Reprenons donc notre fonction de calculatrice.

Code : C++
1
2
3
4
int division(int a, int b)
{
    return a/b;
}


Nous savons qu'une erreur peut survenir si b vaut 0, il faut donc lancer une exception dans ce cas. J'ai choisi, arbitrairement, de lancer une chaîne de caractères. C'est néanmoins un choix intéressant, puisque l'on peut ainsi décrire le problème survenu.

Code : C++
1
2
3
4
5
6
7
int division(int a,int b)
{
    if(b == 0)
       throw string("ERREUR : Division par zéro !");
    else
       return a/b;
}


Souvenez-vous, un throw doit toujours se trouver dans un bloc try qui doit lui-même être suivi d'un bloc catch. Ce qui donne la structure suivante :

Code : C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
int division(int a,int b)
{
    try
    {
        if(b == 0)
           throw string("Division par zéro !");
        else
           return a/b;
   }
   catch(string const& chaine)
   {
       // On gère l'exception.
   }
}


Il ne reste plus alors qu'à gérer l'erreur, c'est-à-dire par exemple, afficher un message d'erreur.

Code : C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
int division(int a,int b)
{
    try
    {
        if(b == 0)
           throw string("Division par zéro !");
        else
           return a/b;
   }
   catch(string const& chaine)
   {
       cerr << chaine << endl;
   }
}


Ce qui donne le résultat suivant :

Code : Console
Valeur pour a : 3
Valeur pour b : 0
ERREUR : Division par zéro !


Plutôt que cout, on utilise dans le cas des erreurs le flux standard d'erreur nommé cerr. Il s'utilise exactement de la même manière que cout. On peut ainsi séparer les informations qui doivent s'afficher dans la console et les informations qui sont dues à des erreurs.


Cette manière de faire est correcte. Cependant, cela ressemble un peu au mauvais exemple numéro 2 ci-dessus. o_O En effet, la fonction peut potentiellement écrire dans la console alors que ce n'est pas son rôle. De plus le programme continue, alors qu'une erreur est survenue. Le mieux à faire serait alors de lancer l'exception dans la fonction et de récupérer l'erreur, si elle se produit, dans le main. De cette manière, celui qui appelle la fonction a conscience qu'une erreur s'est produite.

Code : C++ - La meilleure solution
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
int division(int a,int b) // Calcule a divisé par b.
{
   if(b==0)
      throw string("ERREUR : Division par zéro !");
   else
      return a/b;
}
 
int main()
{
    int a,b;
    cout << "Valeur pour a : ";
    cin >> a;
    cout << "Valeur pour b : ";
    cin >> b;
 
    try
    {
        cout << a << " / "<< b << " = " << division(a,b) << endl;
    }
    catch(string const& chaine)
    {
        cerr << chaine << endl;
    }
    return 0;
}


Vous pouvez remarquer que le throw ne se trouve pas directement à l'intérieur du bloc try, mais qu'il se trouve à l'intérieur d'une fonction qui est appelée, elle, dans un bloc try.

Le else dans la fonction division n'est pas nécessaire puisque si l'exception est levée, le reste du code jusqu'au catch n'est pas exécuté.


Cette fois, le programme ne plante plus et la fonction n'a plus d'effet de bord. C'est la meilleure solution.

Les exceptions standards

Maintenant que l'on sait gérer les exceptions, la question principale est de savoir quel type d'objet lancer.

Je vous ai présenté avant la possibilité de lancer des exceptions de type entier ou string. Il est également possible de lancer un objet par exemple qui contiendrait plusieurs attributs comme :

  • Une phrase décrivant l'erreur.
  • Le numéro de l'erreur.
  • Le niveau de l'erreur (erreur fatale, erreur mineure...).
  • L'heure à laquelle l'erreur est survenue.
  • ...


Un bon moyen de réaliser ceci est de dériver la classe exception de la bibliothèque standard du C++. Eh oui, là aussi la SL vient à notre secours. ;)

On parle d'exception et pas d'erreur, puisque si on la traite, ce n'est plus une erreur. :)


La classe exception



La classe exception est la classe de base de toutes les exceptions lancées par la bibliothèque standard. Elle est aussi spécialement pensée pour qu'on puisse la dériver afin de réaliser notre propre type d'exception. La définition de cette classe est :

Code : C++ - La classe exception
1
2
3
4
5
6
7
8
class exception 
{
public:
    exception() throw(){ }            // Constructeur.
    virtual ~exception() throw();     // Destructeur.
 
    virtual const char* what() const throw();  // Renvoie une chaîne "à la C" contenant des infos sur l'erreur.
};


Pour l'utiliser, il faut inclure le fichier d'en-tête correspondant, soit ici le fichier exception.

Vous pouvez remarquer que la classe possède des fonctions virtuelles et donc également un destructeur virtuel. C'est un bon exemple de polymorphisme.


Les méthodes de la classe sont suivies du mot-clé throw. Cela sert à indiquer que ces méthodes ne vont pas lancer d'exceptions... ce qui est bien parce que la si la classe d'exception commence à lancer des exceptions, on n'est pas sorti de l'auberge. ;)
Indiquer qu'une méthode ne lance pas d'exception est un mécanisme du C++ très rarement utilisé. En fait, cette classe est à peu près le seul endroit où vous verrez cela.


On peut alors créer sa propre classe d'exception en la dérivant grâce à un héritage. Ce qui donnerait par exemple :

Code : C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <exception>
using namespace std;
 
class Erreur: public exception
{
public:
    Erreur(int numero=0, string const& phrase="", int niveau=0) throw()
         :m_numero(numero),m_phrase(phrase),m_niveau(niveau)
    {}
 
     virtual const char* what() const throw()
     {
         return m_phrase.c_str();
     }
     
     int getNiveau() const throw()
     {
          return m_niveau;
     }
    
    virtual ~Erreur() throw()
    {}
 
private:
    int m_numero;               // Numéro de l'erreur.
    string m_phrase;            // Description de l'erreur.
    int m_niveau;               // Niveau de l'erreur.
};


On pourrait alors récrire notre fonction de division de 2 entiers de la manière suivante :

Code : C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
int division(int a,int b) // Calcule a divisé par b.
{
   if(b==0)
      throw Erreur(1,"Division par zéro",2);
   else
      return a/b;
}
 
int main()
{
   int a,b;
   cout << "Valeur pour a : ";
   cin >> a;
   cout << "Valeur pour b : ";
   cin >> b;
 
   try
   {
       cout << a << " / " << b << " = " << division(a,b) << endl;
   }
   catch(std::exception const& e)
   {
       cerr << "ERREUR : " << e.what() << endl;
   }
 
   return 0;
}


Ce qui donne à l'exécution :

Code : Console
Valeur pour a : 3
Valeur pour b : 0
ERREUR : Division par zéro


Quel est l'intérêt de dériver la classe exception, alors qu'on pourrait faire sa propre classe sans aucun héritage ?


Excellente question. Il faut savoir que vous n'êtes pas le seul à lancer des exceptions. o_O Certaines fonctions standards lancent elles aussi des exceptions. Toutes les exceptions lancées par les fonctions standards dérivent de la classe exception, ce qui permet avec un code générique de rattraper toutes les erreurs qui pourraient potentiellement arriver. Ce code générique est le suivant :

Code : C++
1
2
3
4
catch(std::exception const& e)
{
   cerr << "ERREUR : " << e.what() << endl;
}


Ceci est possible grâce au polymorphisme. On attrape un objet de type exception, mais grâce aux fonctions virtuelles et à la référence (les deux ingrédients !), c'est la méthode what() de la classe fille qui sera appelée, ce qui est justement ce que l'on souhaite. :)

La bibliothèque standard peut lancer 5 types d'exceptions différents résumés dans le tableau suivant :

Nom de la classeDescription
bad_alloc Lancée s'il se produit une erreur lors d'une manipulation de la mémoire.
bad_cast Lancée s'il se produit une erreur lors d'un dynamic_cast.
bad_exception Lancée si aucun catch ne correspond à un objet lancé.
bad_typeid Lancée s'il se produit une erreur lors d'un typeid.
ios_base::failure Lancée s'il se produit une erreur avec un flux.


On peut par exemple observer un exemple de bad_alloc avec le code suivant :

Code : C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
#include <iostream>
#include <vector>
using namespace std; 

int main()
{
    try
    {
        vector<int> a(1000000000,1);   //Un tableau bien trop grand
    }
    catch(exception const& e)          // On rattrape les exceptions standards de tous types.
    {
        cerr << "ERREUR : "<< e.what() << endl;   // On affiche la description de l'erreur.
    }
    return 0;
}


Ce qui donne le résultat suivant dans la console :

Code : Console
ERREUR : std::bad_alloc


Si l'on avait attrapé l'exception par valeur et pas par référence (c'est-à-dire sans le &), le message aurait été std::exception, car le polymorphisme n'est pas conservé. C'est pour cela que l'on attrape toujours les exceptions par référence. C'est fort quand même ce polymorphisme ! :-°


Le travail pré-mâché



Si comme moi (et beaucoup de programmeurs :-° ) vous êtes un fainéant et que vous n'avez pas envie de créer votre propre classe d'exception, sachez qu'il existe un fichier standard qui contient des classes d'exception pour les cas les plus courants.
Le fichier stdexcept contient 9 classes d'exceptions séparées en 2 catégories, les exceptions « logiques » (logic errors en anglais) et les exceptions « d'exécution » (runtime errors en anglais).

Toutes les exceptions présentées dérivent de la classe exception et possèdent un constructeur prenant en argument une chaîne de caractère permettant de décrire le problème.

Nom de la classeCatégorieDescription
domain_error logique A Lancer s'il se produit une erreur de domaine mathématique.
invalid_argument logique A Lancer si un des arguments d'une fonction est invalide.
length_error logique A Lancer si un objet aura une taille invalide. Par exemple si la classe Pile vue précédemment a une taille dépassant la taille de la mémoire.
out_of_range logique A Lancer s'il y a une erreur avec un indice. Par exemple si on essaye d'accéder à une case inexistante d'un tableau.
logic_error logique A Lancer lors de n'importe quel autre problème de logique du programme.
range_error exécution A Lancer lors d'une erreur de domaine à l'exécution.
overflow_error exécution A Lancer s'il y a une erreur d'overflow.
underflow_error exécution A Lancer s'il y a une erreur d'underflow.
runtime_error exécution A Lancer pour tout autre type d'erreur non-prévue survenant à l'exécution.


Si vous ne savez pas quoi choisir, prenez simplement runtime_error, cela n'a de toute façon que peu d'importance.

Et comment on les utilise ?


Reprenons une dernière fois notre exemple de division. Nous avons une erreur de domaine mathématique si l'argument b est nul. Choisissons donc de lancer une domain_error.

Code : C++
1
2
3
4
5
6
7
int division(int a,int b) // Calcule a divisé par b.
{
   if(b==0)
      throw domain_error("Division par zéro");
   else
      return a/b;
}


On aurait très bien pu choisir une argument_error ou encore une runtime_error. Cela n'a que peu d'importance puisqu'en général on attrape toujours les exceptions par la méthode indiquée plus haut.


Les exceptions de vector



Je vous ai dit dans l'introduction qu'une erreur possible (et courante !) était le cas où un utilisateur cherche à accéder à la 10ème case d'un vector de 8 éléments.
Accéder aux objets stockés dans un tableau, vous savez le faire depuis longtemps. On utilise bien sûr les crochets []. Or, ces crochets ne font aucun test. Si vous fournissez un index invalide, le programme va planter et c'est tout. :colere2:
Et après ce chapitre, on pourrait se demander si c'est vraiment une bonne idée. Utiliser une exception en cas d'erreur d'index vous paraît peut-être une bonne idée... et aux concepteurs de la STL aussi ! ;)
C'est pour ça que les vector (et les deque) proposent une méthode appelée at() qui fait exactement la même chose que les crochets mais qui lance une exception en cas d'indice erroné.

Code : C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
#include <vector>
#include <iostream>
using namespace std;

int main()
{
    vector<double> tab(5, 3.14);  //Un tableau de 5 nombres à virgule

    try
    {
        tab.at(8) = 4.;  //On essaye de modifier la 8ème case
    }
    catch(exception const& e)
    {
        cerr << "ERREUR : " << e.what() << endl;
    }
    return 0;
}


Ce qui nous donne :

Code : Console
ERREUR : vector::_M_range_check


Encore un nouveau type d'exception ! :( Oui, oui, mais ce n'est pas grave. Car comme je vous l'ai dit, tous les types d'exception utilisé dérivent de la classe exception et notre catch "standard" est donc suffisant. Il n'y a donc qu'une seule syntaxe à apprendre. Plutôt sympa non ? :soleil:

En pratique, on utilise très rarement, voire même jamais la méthode at(). On considère plutôt que c'est à l'utilisateur de vector d'utiliser le tableau correctement.


Terminons avec un point qui pourrait vous sauver la vie lors de la lecture de codes sources obscures.

Relancer une exception



Il est possible de relancer une exception reçue par un bloc catch afin de la retraiter une deuxième fois plus loin dans le code. Pour ce faire, il faut utiliser le mot-clé throw sans expression derrière.

Code : C++
1
2
3
4
5
6
7
8
catch(exception const& e) // Rattrape toutes les exceptions
{
   //On traite une première fois l'exception
   cerr << "ERREUR: " << e.what() << endl;
 
   throw; // Et on relance l'exception reçue pour la retraiter 
          // dans un autre bloc catch plus loin dans le code.
}

Les assertions

Les exceptions c'est bien. Mais il y a des cas où mettre en place tous ces blocs try / catch est fastidieux. Ce n'est pas pour rien que vector propose les [] pour accéder aux éléments. On n'a pas toujours envie d'avoir à traiter les exceptions.
Il existe un autre mécanisme de détection et gestion qui vient du langage C : les assertions.

Claquer une assertion



Pour utiliser les assertions, il faut inclure le fichier d'en-tête cassert. Et c'est certainement l'étape la plus difficile. ;)

Une assertion permet de tester si une expression est vraie ou non. Si c'est vrai, rien ne se passe et le programme continue. Par contre, si le teste est négatif, le programme s'arrête brutalement et un message d'erreur s'affiche dans le terminal.

Code : C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#include <cassert>
using namespace std;

int main()
{
    int a(5);
    int b(5);

    assert(a == b) ; //On vérifie que a et b sont égaux
    
    //reste du programme
    return 0;
}


Lors de l'exécution, rien ne se passe, normal les deux variables sont égales. :) Par contre, si vous modifiez la valeur de b, alors le message suivant s'affiche à l'exécution :

Code : Console
monProg: main.cpp:9: int main(): Assertion `a == b' failed.
Abandon


C'est super, le message d'erreur indique le fichier où se situe l'erreur, le nom de la fonction et même la ligne ! Avec ça, impossible de ne pas trouver la cause d'erreur. Je vous avais bien dit que c'était simple !

Mais pourquoi utiliser des exceptions si les assertions sont mieux ?


Attention, je n'ai pas dit que les assertions étaient mieux ! Les deux méthodes de gestion des erreurs ont leur domaine d'application. Si vous claquez une assertion, le programme s'arrête brutalement. Il n'y a aucun moyen de réparer l'erreur et tenter de continuer. Si vous avez un programme de chat et qu'il n'arrive pas à se connecter au serveur, c'est une erreur. Vous aimeriez bien que votre programme réessaye de se connecter plusieurs fois. Il faut donc utiliser une exception, pour tenter de réparer l'erreur. Une assertion aurait complètement tué le programme. Ce n'est clairement pas la bonne solution dans ce cas !
A vous de choisir ce dont vous avez besoin au cas par cas.

Désactiver les assertions



Un autre point fort des assertions est la possibilité de les désactiver totalement. En faisant ça, le compilateur va simplement ignorer les lignes assert(...) et ne pas effectuer le test qui se trouve entre les parenthèses. En faisant ça, le code sera (légèrement) plus rapide, mais aucun test ne sera effectué. Il faut donc choisir. :pirate:

Pour désactiver les assertions, il faut ajouter l'option -DNDEBUG à la ligne de compilation.

Si vous utilisez Code::Blocks, cela se fait via le menu project > build options. Dans la fenêtre qui s'ouvre il faut sélectionner l'onglet Compiler settings et dans le champ Other options, vous ajoutez simplement -DNDEBUG, comme sur l'illustration suivante.

Image utilisateur


Avec cette option activée, le code d'exemple précédent s'exécute sans problème même si a est différent de b. La ligne de test a simplement été ignorée par le compilateur.

Les assertions sont souvent utilisées durant la phase de création d'un programme pour tester si tout se passe bien. Une fois que l'on sait que le programme fonctionne, on les désactive, on compile et on vend le programme au client. ;) Ce dernier ne veut pas de messages d'erreurs et il veut un programme rapide.
Si par contre il découvre un bug, on réactive les assertions et on cherche l'erreur. C'est vraiment un outil destiné aux développeurs, au contraire des exceptions.
Armés de ces nouveaux outils, vous êtes maintenant aptes à créer des projets plus robustes et à gérer les évènements inattendus qui pourraient surgir durant l'exécution de vos futurs programmes.

Sachez tout de même que la gestion correcte de toutes les exceptions qui pourraient survenir dans un programme est une tâche qui peut s'avérer très compliquée. Penser à tout est souvent bien plus difficile que l'on se l'imagine. :soleil:
Chapitre précédent Sommaire Chapitre suivant

Partager

6 commentaires pour "La gestion des erreurs avec les exceptions"
Note moyenne : 3.85 / 4 (1749 votes)
Pseudo Commentaire
En ligne Nanoc # Posté le 27/05/2011 à 22:56:20
Aimez-vous le C++ ?
Avatar
Validateurs

Ville : Durham
Pays : Royaume-Uni
Études : EPFL

Les valeurs sont récupérées via cin >>. Il n'y a donc pas besoin de les initialiser.
 
Hors ligne Copelnug # Posté le 28/05/2011 à 16:37:26

Bon tutoriel. Par contre, il y a deux sujets qui ne sont pas abordés:
  • Le «catch(...)» qui permet d’attraper toutes les exceptions.
  • La relance d’exception.

Il me semble qu’ensemble ces fonctionnalités peuvent être utiles et mériteraient donc d’être abordées.
Hors ligne Bizzi # Posté le 21/06/2011 à 02:54:08
Avatar

Juste un "petit" détail: considérer que toutes les exceptions dérivent de la classe std::exception est une erreur assez grave; elle pourrait causer des arrêts de programme non désirés. Il faut utiliser catch(...) pour tout attraper
Connecté Nanoc # Posté le 21/06/2011 à 08:09:30
Aimez-vous le C++ ?
Avatar
Validateurs

Ville : Durham
Pays : Royaume-Uni
Études : EPFL

Citation : Bizzi
Juste un "petit" détail: considérer que toutes les exceptions dérivent de la classe std::exception est une erreur assez grave; elle pourrait causer des arrêts de programme non désirés. Il faut utiliser catch(...) pour tout attraper


Et que faire une fois que tu les as attrapées via catch(...) ? Tu n'as aucun moyen de savoir proprement ce que tu as attrapé et donc de traiter l'exception correctement. Tu ne peux donc pas remettre le programme dans un état correct non plus.
 
Hors ligne youyou # Posté le 12/01/2012 à 23:51:52
Shibboleet !
Avatar

Le RAII mérite vraiment d’être abordé. C'est la seule manière propre (AMHA) de gérer les exceptions...
 

Voir tous les commentaires