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 | // 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 | 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++ | 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++ | 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.

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.