Tutoriel « exceptions » [HTML]
Le but de cet exercice est de reprendre l'exemple du cours illustrant la gestion des exceptions.
Commençons par « préparer le terrain » en définissant notre programme de base. Il s'agit 10 fois de suite de demander un nombre, de calculer son inverse par une fonction f et d'afficher le résultat.
Aucune difficulté donc ici :
#include <iostream>
using namespace std;
double f(double x) {
if (x == 0.0) {
return 0.0; // n'importe quoi. C'est ici que nous allons
// introduire les exceptions
}
else return 1.0/x;
}
double demander_nombre() {
// version de base
cout << " nombre ? ";
double z;
cin >> z;
return z;
}
int main () {
for (int i(1); i <= 10; ++i) {
double x, y;
x = demander_nombre();
cout << "La valeur de f(" << x <<") ";
y = f(x);
cout << "est " << y << endl;
}
return 0;
}
Compilez et vérifiez que le programme fonctionne.
Introduisons maintenant l'exception pour la division par zéro dans f.
Cela se fait en 3 étapes :
- « lancement » de l'exception. Dans cette première version, lançons simplement une string contenant le message d'erreur.
Cela se fait par l'instruction throw au niveau où se produit l'erreur/l'exception :
#include <iostream> #include <string> ... // reste du programme inchangé double f(double x) { if (x == 0.0) { throw string("division par 0"); } else return 1.0/x; } ... - Prévoir la « capture » de l'exception aux endroits appropriés. Ici, à l'endroit de l'appel de f dans main().
Cela se fait par l'ajout d'un bloc « try » autour de l'appel :
... // reste du programme inchangé for (int i(1); i <= 10; ++i) { double x, y; x = demander_nombre(); cout << "La valeur de f(" << x <<") "; try { y = f(x); cout << "est " << y << endl; } ... - « capture » de l'exception et gestion par le bloc « catch » correspondant.
Ce bloc doit définir le type des exceptions qu'il gère. Cela se fait par la syntaxe catch(type& nom).
Dans notre cas, on souhaite « attraper » une string.
De plus dans ce cas simple, la gestion de l'exception consiste simplement à afficher le message. Nous avons donc :
... // reste du programme inchangé for (int i(1); i <= 10; ++i) { double x, y; x = demander_nombre(); cout << "La valeur de f(" << x <<") "; try { y = f(x); cout << "est " << y << endl; } catch(string& erreur) { cerr << "Erreur : " << erreur << endl; } ...Cependant si l'on s'arrête là, la phrase « La valeur de f(0) » commencée plus haut ne sera pas terminée en cas d'erreur.
Il serait plus correct de penser à la conclure :
... // reste du programme inchangé catch(string& erreur) { cout << "n'est pas définie." << endl; cerr << "Erreur : " << erreur << endl; }
Nous avons finit ici la première gestion (élémentaire) de l'exception. Compilez et testez votre programme.
Si cela fonctionne, passons maintenant à une gestion plus avancée des exceptions.
Pour l'instant nous ne passons comme exception qu'un message d'erreur, mais on peut « lancer » n'importe quel objet. En particulier, on pourrait « lancer » plus d'information quant à l'erreur :
- un niveau d'erreur (par exemple~: avertissement, mineure, majeure, critique)
- une indication si l'erreur doit ou non arrêter le programme
- un code d'erreur, identifiant par exemple de quelle erreur il s'agit, où elle s'est produite, etc.
- un message d'erreur
- etc.
Choisissons ici pour illustrer le propos de coder les erreurs par la structure suivante :
struct Erreur {
bool arret;
unsigned int niveau;
int code;
string message;
};
Notre programme devient donc (en gras les parties modifiées) :
#include <iostream>
#include <string>
using namespace std;
struct Erreur {
bool arret; // on arrête ou non le programme ?
unsigned int niveau; // gravité de l'erreur, de 0 à ...
int code; // code unique identifiant les erreurs
string message; // message d'erreur
};
double f(double x) {
if (x == 0.0) {
Erreur err;
err.arret = false; // par exemple : on ne s'arrête pas sur cette erreur
err.niveau = 10; // par exemple
err.code = 1; // code arbitraire identifiant l'erreur
err.message = "division par 0"; // notre message
throw err;
}
else return 1.0/x;
}
double demander_nombre() {
... // code inchangé
}
int main() {
for (int i(1); i <= 10; ++i) {
double x, y;
x = demander_nombre();
cout << "La valeur de f(" << x <<") ";
try {
y = f(x);
cout << "est " << y << endl;
}
catch(Erreur& erreur) {
if (erreur.code == 1) cout << "n'est pas définie." << endl;
if (erreur.niveau < 5) cerr << "Avertissement : ";
else cerr << "Erreur : ";
cerr << erreur.message << endl;
if (erreur.arret)
return erreur.code; // sort du main, donc quitte le programme !
}
}
return 0;
}
Compilez et testez ce nouveau code.
Pour finir, introduisons la gestion d'une vraie exception, c'est-à-dire d'un cas singulier mais qui n'est pas une erreur : si l'utilisateur saisit 'q' au lieu d'un nombre, le programme s'arrête.
Cette exception est « lancée » par la fonction demander_nombre et doit être « capturée » dans le main().
Nous déciderons de ne lancer que la touche saisie par l'utilisateur (donc un char) :
double demander_nombre() {
cout << " nombre ? [ou q pour sortir] ";
double z;
cin >> z;
if (cin.fail()) { // ce n'est pas un nombre
cin.clear(); // remet le flot cin dans un état normal
char lu;
cin >> lu; // essaye de lire un char
if (cin.fail() || (lu != 'q')) {
// ici c'est une vraie erreur que nous « lançons »
Erreur err = { true, 2, 2, "saisie erronée" };
throw err;
}
else {
// ici c'est une vraie exception.
// on « lance » 'q'
throw lu;
}
}
return z;
}
Le bloc try doit maintenant englober aussi l'appel à la fonction demander_nombre :
int main () {
for (int i(1); i <= 10; ++i) {
try {
double x, y;
x = demander_nombre();
cout << "La valeur de f(" << x <<") ";
y = f(x);
cout << "est " << y << endl;
}
...
Il nous faut de plus ajouter la gestion de notre vraie exception (la nouvelle erreur est parfaitement gérée par notre ancien bloc catch)
catch(char x) {
if (x == 'q') {
cout << "bye bye" << endl;
return 0; // quitte le main()
}
}
Compilez et testez cette dernière version du programme.
#include <iostream>
#include <string>
using namespace std;
struct Erreur {
bool arret; // on arrête ou non le programme ?
unsigned int niveau; // gravité de l'erreur, de 0 à ...
int code; // code unique identifiant les erreurs
string message; // message d'erreur
};
double f(double x) {
if (x == 0.0) {
Erreur err;
err.arret = false; // par exemple : on ne s'arrête pas sur cette erreur
err.niveau = 10; // par exemple
err.code = 1; // code arbitraire identifiant l'erreur
err.message = "division par 0"; // notre message
throw err;
}
else return 1.0/x;
}
double demander_nombre() {
cout << " nombre ? [ou q pour sortir] ";
double z;
cin >> z;
if (cin.fail()) { // ce n'est pas un nombre
cin.clear(); // remet le flot cin dans un état normal
char lu;
cin >> lu; // essaye de lire un char
if (cin.fail() || (lu != 'q')) {
// ici c'est une vraie erreur que nous « lançons »
Erreur err = { true, 2, 2, "saisie erronée" };
throw err;
}
else {
// ici c'est une vraie exception (au sens usuel).
// on « lance » 'q'
throw lu;
}
}
return z;
}
int main() {
for (int i(1); i <= 10; ++i) {
try {
double x, y;
x = demander_nombre();
cout << "La valeur de f(" << x <<") ";
y = f(x);
cout << "est " << y << endl;
}
catch(Erreur& erreur) {
if (erreur.code == 1) cout << "n'est pas définie." << endl;
if (erreur.niveau < 5) cerr << "Avertissement : ";
else cerr << "Erreur : ";
cerr << erreur.message << endl;
if (erreur.arret)
return erreur.code; // sort du main, donc quitte le programme !
}
catch(char x) {
if (x == 'q') {
cout << "bye bye" << endl;
return 0; // quitte le main()
}
}
}
}
