Les exceptions
Comment écrire un code robuste qui ne crashe pas sur une erreur runtime : try, throw, catch, propagation à travers les frames, classes d'exception personnalisées, hiérarchie std::exception.
📄 Ressources du cours
🎥 Vidéos recommandées
-
🇫🇷
Exception Handling In C++ Introduction
CppNuts · 10:27 · 9k vues — In C++, exceptions are runtime anomalies or abnormal conditions that a program encounters during its execution time. The way of handling these exceptions is ...
-
🇫🇷
8.01 Introduction to Exception in C++, try-catch-throw
Darshan University · 05:29 · 5k vues — Explanation of the concept exception handling with try, catch, throw keywords. Understanding with C++ program.Runtime errors are termed as exception. Excepti...
-
🇬🇧
Exception Handling In C++ Introduction
CppNuts · 10:27 · 9k vues — In C++, exceptions are runtime anomalies or abnormal conditions that a program encounters during its execution time. The way of handling these exceptions is ...
1. Pourquoi les exceptions ?
L'approche naïve — ne pas gérer
int division(int a, int b) {
return a / b;
}
int main() {
int a, b;
cout << "a : "; cin >> a;
cout << "b : "; cin >> b;
cout << a << "/" << b << " = " << division(a, b);
}
// $ ./prog
// a : 5 b : 0
// Exception en point flottant (core dumped) ← crash brutal
Et la gestion à l'ancienne ?
Deux contournements traditionnels, tous deux insatisfaisants :
int division(int a, int b) {
if (b != 0) return a / b;
else cout << "ERREUR : div 0";
// la fonction doit retourner un int — que faire ?
}
int division(int a, int b) {
if (b != 0) return a / b;
else return ERREUR;
}
// Que vaut ERREUR ? -1 ? 0 ? INT_MIN ?
// Comment distinguer un résultat valide d'un code d'erreur ?
Séparer le chemin nominal (calcul normal) du chemin d'erreur (gestion de l'exception), sans polluer la signature de retour et sans risquer de mélanger un résultat valide avec un code-sentinelle.
2. Le mécanisme — try / throw / catch
Trois mots-clés à connaître :
| Mot-clé | Rôle |
|---|---|
throw | Lance (lève) une exception. Suit immédiatement la valeur à transporter. |
try | Délimite un bloc sous surveillance — toute exception levée dedans sera attrapable. |
catch | Suit un try. Reçoit l'exception et la traite. Plusieurs catch possibles, un par type. |
try {
// code sensible
if (problème) throw valeur;
// si throw, les lignes suivantes ne s'exécutent PAS
}
catch (Type t) {
// gestionnaire — exécuté si une exception de type Type a été lancée
}
// la suite du programme reprend ici (sauf si on a appelé exit)
Reprise de l'exemple — division par zéro
int division(int a, int b) {
if (b == 0)
throw string("Erreur : division par zéro");
return a / b;
}
int main() {
int a, b;
cout << "a : "; cin >> a;
cout << "b : "; cin >> b;
try {
cout << a << "/" << b << " = " << division(a, b) << endl;
}
catch (string const& chaine) {
cerr << chaine << endl;
}
return 0;
}
// a : 5 b : 0
// Erreur : division par zéro ← plus de crash, gestion propre
3. Propagation à travers les frames d'appel
Une exception levée dans F3 peut être attrapée dans F2, F1, ou jusque dans main(). Elle remonte la pile tant qu'aucun catch compatible ne l'attrape.
Schéma : les frames bleus représentent l'exécution normale, l'éclair jaune le throw, et la flèche rouge le déroulement (stack unwinding) jusqu'au gestionnaire.
- Une exception non rattrapée remonte automatiquement jusqu'au prochain
try/catchcompatible. - Si elle remonte hors de
main()sans avoir été rattrapée → le programme s'interrompt avec un message abscons (terminate called). - Avec des
tryimbriqués, c'est le plus interne qui essaie d'abord, puis on remonte vers les blocs englobants.
4. Plusieurs catch — un par type
try {
// code sensible
}
catch (int e) { // rattrape les entiers lancés
cerr << "erreur code " << e;
}
catch (string const& e) { // rattrape les chaînes lancées
cerr << e;
}
catch (Personnage const& e) { // rattrape les objets Personnage
cerr << "Personnage en erreur";
}
catch (...) { // catch-all — toutes les autres
cerr << "Exception inattendue";
}
catch(...) avec les trois points est le catch-all — il attrape tout ce qui n'a pas matché les précédents, mais ne récupère pas l'objet.
Passer par référence
Pour les types non-triviaux, on capture par référence constante (const&) : pas de copie, et impossible de modifier l'exception.
5. Relancer une exception
Parfois on veut faire un traitement local (logger, libérer une ressource), puis laisser l'exception remonter au gestionnaire supérieur. Pour ça : throw; sans argument.
try {
// ...
}
catch (TypeException& e) {
// traitement local : log, libération de ressources
logger("erreur attrapée");
throw; // relance l'exception EXISTANTE telle quelle
}
throw; ≠ throw e;throw; sans argument relance la même exception. throw e; en construit une nouvelle (copie), ce qui peut perdre le type dynamique si e est une référence sur une classe de base.
6. Classe d'exception personnalisée
Lancer un int ou un string est limité. Mieux : lancer un objet qui transporte plusieurs informations :
- une phrase décrivant l'erreur
- un code numérique
- un niveau (fatal, mineur)
- l'heure de l'erreur, le fichier, la ligne…
// essai.h
class Erreur {
private:
int _val;
public:
Erreur(int n = 0) : _val{n} { }
int get_val() { return _val; }
};
class Essai {
public:
void f1() {
throw Erreur(10); // construit une Erreur(10) et la lance
}
};
// main.cpp
int main() {
try {
Essai e1;
e1.f1();
cout << "jamais affiché";
}
catch (Erreur e) {
cout << "Erreur numéro : " << e.get_val() << endl;
}
}
// affiche : Erreur numéro : 10
7. std::exception — la classe de base standard
std::exception est la classe de base de toutes les exceptions lancées par la bibliothèque standard. Pour s'intégrer proprement à l'écosystème, les classes d'erreur perso devraient dériver de std::exception.
#include <exception>
class exception {
public:
exception();
exception(const exception& right);
exception& operator=(const exception& right);
virtual ~exception();
virtual const char* what() const; // message d'erreur
};
Dériver pour ses propres erreurs
#include <exception>
class DivisionParZero : public std::exception {
public:
const char* what() const noexcept override {
return "Division par zéro interdite";
}
};
int division(int a, int b) {
if (b == 0) throw DivisionParZero();
return a / b;
}
try { ... }
catch (std::exception& e) {
cerr << e.what() << endl; // affiche le message via polymorphisme
}
catch(std::exception&) attrape toute exception dérivant de std::exception. Le what() virtuel renvoie le bon message grâce au polymorphisme (cf. CM4). Une seule clause pour toute la hiérarchie.
8. Exemple — contrôle de saisie
On veut forcer l'utilisateur à entrer un entier entre 1 et 10. Tant que c'est invalide, on redemande.
#include <iostream>
#include <exception>
using namespace std;
int saisirInt() {
int a;
while (1) {
try {
cout << "entrer un entier entre 1 et 10 : ";
cin >> a;
if (a < 1 || a > 10) throw 10;
return a;
}
catch (int code) {
cout << "erreur de saisie" << endl;
}
}
}
int main() {
int a = saisirInt();
cout << "valeur saisie = " << a << endl;
return 0;
}
Note du cours : la signature historique int saisirInt() throw() (qui spécifiait que la fonction ne lève aucune exception) est obsolète depuis C++17. La remplaçante moderne est noexcept : int saisirInt() noexcept.
★ Réviser le chapitre
Pour vérifier ta compréhension
Quelle est la structure de base d'une gestion d'exception en C++ ?
try { /* code risqué */ } + catch (TypeException& e) { /* traitement */ } + throw exception; pour lever une exception. Plusieurs catch peuvent suivre un try, ordonnés du plus spécifique au plus général.
Quelle est la classe de base de toutes les exceptions standard ?
std::exception (dans <exception>). Toutes ses dérivées (std::runtime_error, std::logic_error, std::out_of_range, std::bad_alloc…) ont une méthode what() qui retourne un const char* décrivant l'erreur.
Pourquoi attraper une exception par référence (catch (e&)) plutôt que par valeur ?
(1) Évite la copie de l'objet exception (potentiellement coûteuse). (2) Évite le slicing : avec une capture par valeur d'une exception de classe de base, on perd l'information de la classe dérivée. (3) La référence permet d'utiliser const& pour préciser qu'on ne modifie pas l'exception.
Que se passe-t-il si une exception n'est pas attrapée ?
La fonction std::terminate() est appelée, ce qui termine brutalement le programme (par défaut via abort()). Les destructeurs des objets locaux peuvent ne pas être appelés (selon l'implémentation), risquant des fuites de ressources.
Comment créer une exception personnalisée ?
Dériver de std::exception (ou d'une de ses sous-classes) et redéfinir what() :
class MonErreur : public std::exception {
std::string msg;
public:
MonErreur(const std::string& m) : msg(m) {}
const char* what() const noexcept override { return msg.c_str(); }
};
🃏 Flashcards
throw (lever), try (zone surveillée), catch (gestionnaire). Le throw doit être atteint depuis un try englobant, sinon le programme se termine.catch compatible. Si elle sort de main() sans gestionnaire, std::terminate est appelée et le programme s'arrête brutalement.catch sélectionne-t-il les exceptions ?catch(int e) n'attrape que les int, catch(string const& e) que les string, etc. catch(...) est le catch-all.const&) ?const pour ne pas pouvoir modifier l'exception accidentellement.throw; sans argument ?catch.throw; vs throw e; ?throw; propage la même exception. throw e; lance une copie — perd le type dynamique si e est une référence sur une classe de base. Préférer throw; pour relancer.std::exception ?<exception>. Méthode virtuelle clé : what() renvoie le message.what() ?std::exception qui renvoie un const char* décrivant l'erreur. À override dans les classes dérivées pour personnaliser le message.int ?std::exception, et permet le polymorphisme dans les catch.try imbriqués, quel catch tente en premier ?catch (...) ?catch qui ne relance pas ?try/catch. L'exception est considérée traitée. Le code qui suit le throw dans le try n'est pas exécuté.throw() en signature ?noexcept (depuis C++11, obligatoire depuis C++17). void f() noexcept; garantit que f ne lève aucune exception. Si elle en lève quand même : std::terminate.✎ Quiz éclair
throw dans un bloc try :throw interrompt l'exécution courante et transfère le contrôle au catch compatible (par type) le plus proche.catch aboutit à :main() sans gestionnaire, le runtime appelle terminate, qui par défaut appelle abort.throw; sans argument propage la même exception. throw e; en construit une copie, ce qui peut perdre le type dynamique.std::exception est la racine. Toutes les classes std::runtime_error, std::logic_error, etc. en dérivent.what() dans une exception personnalisée ?virtual const char* what() const est la signature canonique. Pour des informations supplémentaires, on ajoute des accesseurs (getters) dans sa classe dérivée.catch (std::exception& e) avant catch (DivisionParZero& e) :catch sont testés dans l'ordre. Une DivisionParZero matchera le premier catch (std::exception&). Toujours mettre les types les plus spécifiques en premier.throw() ?noexcept (C++11+) déclare qu'une fonction ne lève aucune exception. Si elle en lève quand même, std::terminate est appelée. throw() est obsolète depuis C++17.Score : 0 / 8 ·