Le carnet On est
Séance 6 · encre charbon

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.

~ 50 min Source : Cours6 Exception + TP6 13 flashcards · 8 QCM

📄 Ressources du cours

📥 Télécharger les slides  ·  🔗 Ouvrir dans un nouvel onglet  ·  📝 Ouvrir le TD/TP

🎥 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 ?

Définition Une exception indique une erreur pendant l'exécution. C'est un événement qui suspend définitivement l'instruction en cours et passe la main à un gestionnaire d'exception (le « preneur en main »).

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 :

⚠ Cas 1 — afficher et retourner quoi ?
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 ?
}
⚠ Cas 2 — code de retour magique
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 ?
📌 La promesse des exceptions
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
throwLance (lève) une exception. Suit immédiatement la valeur à transporter.
tryDélimite un bloc sous surveillance — toute exception levée dedans sera attrapable.
catchSuit 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.

⚡ throw main F1 F2 F3 flux normal exception levée
Règles de propagation
  • Une exception non rattrapée remonte automatiquement jusqu'au prochain try/catch compatible.
  • Si elle remonte hors de main() sans avoir été rattrapée → le programme s'interrompt avec un message abscons (terminate called).
  • Avec des try imbriqué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";
}
Sélection par type Le compilateur dispatch sur le type de la valeur lancée. 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

Définition 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
}
🔑 Capture par référence sur la base
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

Quels sont les trois mots-clés du mécanisme d'exceptions ?
throw (lever), try (zone surveillée), catch (gestionnaire). Le throw doit être atteint depuis un try englobant, sinon le programme se termine.
Que se passe-t-il si une exception n'est pas attrapée ?
Elle remonte la pile jusqu'à trouver un catch compatible. Si elle sort de main() sans gestionnaire, std::terminate est appelée et le programme s'arrête brutalement.
Comment le catch sélectionne-t-il les exceptions ?
Par type. catch(int e) n'attrape que les int, catch(string const& e) que les string, etc. catch(...) est le catch-all.
Pourquoi capturer par référence (const&) ?
Pour éviter la copie de l'objet exception (efficace) et pour préserver le type dynamique en cas d'héritage. const pour ne pas pouvoir modifier l'exception accidentellement.
Que fait throw; sans argument ?
Relance l'exception courante telle quelle (le même objet, le même type dynamique). Utilisable uniquement à l'intérieur d'un catch.
Différence 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.
Qu'est-ce que std::exception ?
La classe de base de toutes les exceptions lancées par la bibliothèque standard. Définie dans <exception>. Méthode virtuelle clé : what() renvoie le message.
À quoi sert what() ?
Méthode virtuelle de std::exception qui renvoie un const char* décrivant l'erreur. À override dans les classes dérivées pour personnaliser le message.
Pourquoi préférer une classe d'exception à un simple int ?
Un objet transporte plusieurs informations (message, code, contexte, heure…), participe à la hiérarchie std::exception, et permet le polymorphisme dans les catch.
Avec try imbriqués, quel catch tente en premier ?
Le plus interne. Si aucun ne convient à ce niveau, on remonte au try englobant. Modèle stack unwinding.
Que fait catch (...) ?
C'est le catch-all : attrape toutes les exceptions, peu importe leur type. Inconvénient : on n'a pas accès à l'objet exception.
Que devient le programme après un catch qui ne relance pas ?
L'exécution reprend après le bloc try/catch. L'exception est considérée traitée. Le code qui suit le throw dans le try n'est pas exécuté.
Remplaçant moderne de 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

1.Une exception levée par throw dans un bloc try :
  • Interrompt définitivement le programme
  • Saute directement au premier catch compatible
  • Reprend après le throw à la ligne suivante
  • Génère un core dump
Le throw interrompt l'exécution courante et transfère le contrôle au catch compatible (par type) le plus proche.
2.Une exception non rattrapée par aucun catch aboutit à :
  • Un avertissement à la compilation
  • Une attente infinie
  • Une valeur 0 retournée par main
  • L'appel de std::terminate et l'arrêt brutal du programme
Si l'exception sort de main() sans gestionnaire, le runtime appelle terminate, qui par défaut appelle abort.
3.Pour attraper toutes les exceptions sans tenir compte du type :
  • catch (Exception e)
  • catch (auto e)
  • catch (...)
  • catch (void* e)
Les trois petits points sont littéraux. C'est le catch-all, à placer en dernier puisqu'il intercepte tout ce que les précédents n'ont pas matché.
4.Pour relancer l'exception courante sans la modifier :
  • throw e;
  • throw new e;
  • throw; tout court
  • continue;
throw; sans argument propage la même exception. throw e; en construit une copie, ce qui peut perdre le type dynamique.
5.Quelle classe est la racine de la hiérarchie standard des exceptions ?
  • std::error
  • std::exception
  • std::throwable
  • std::Object
std::exception est la racine. Toutes les classes std::runtime_error, std::logic_error, etc. en dérivent.
6.Que renvoie typiquement what() dans une exception personnalisée ?
  • Un const char* décrivant l'erreur
  • Le numéro de ligne
  • Un int code d'erreur
  • L'objet entier sérialisé
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.
7.Si on a catch (std::exception& e) avant catch (DivisionParZero& e) :
  • Aucun problème — le compilateur choisit la plus spécifique
  • Les deux sont exécutés
  • Erreur de compilation
  • Le catch de la classe de base capture tout : la branche spécifique est inaccessible
Les 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.
8.Quel remplaçant moderne pour la spécification d'exception throw() ?
  • safe
  • final
  • noexcept
  • constexpr
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 ·