Logo HE-ARC Logo HES-SO

1242.2 Langage C++ - 2024-2025

Chapitre 8

Les exceptions

1. Gestion des erreurs

2. Principe des exceptions

3. Try, catch et throw

4. Relance d'exceptions

5. noexcept

6. std::exception et what()

8.1 Gestion des erreurs en C++

Méthode historique


                // Computes f(x) = 1/x^2
                #define ERROR -1
                int main()
                {
                  int x = 0;
                  double result = ERROR;
                  cin >> x;
                  result = f(x);
                  if (ERROR == result)
                    cout << "erreur";
                  else 
                    cout << result;

                  return 0;
                }
              

                double f(int x)
                {
                 double res = ERROR;
                 if (false == inv(x, res)) {
                   cout << "inversion error";
                   return ERROR;
                 }
                 else 
                   return res*res; // (1/x)^2
                }
                bool inv(int n, double &res)
                {
                  if (n==0) return false;
                  else {
                    res = 1.0 / n;
                    return true;
                  }
                }
              

8.1 Gestion des erreurs en C++

Inconvénients

Code de gestion d'erreurs mélanger au "vrai" code

L'appelant doit savoir ce que signifient les codes d'erreurs retournés

Retouner une valeur et un code d'erreur en même temps

8.1 Gestion des erreurs en C++

Les exceptions


                // Computes f(x) = 1/x^2

                int main()
                {
                  int x = 0;
                  double result = -1;
                  cin >> x;
                  try {
                    result = f(x);
                  }
                  catch (...) {
                    cout << "erreur";
                  }
                  
                  cout << result;
                }
              

                double f(int x)
                {
                 double res = inv(x);
                 return res*res;
                }
                double inv(int x)
                {
                  if (x==0) throw(0);
                  
                  return 1./x;
                }
              

throw : lance une exception

catch : récupère une exception

8.2 Principe des exceptions

Lors de la détection d'une «erreur» (fonctionnement imprévu)

Exception : signal logiciel, accompagné de données (int, string, ..., objet). Une exception est souvent un objet de la classe std::exception ou d'une classe dérivée

Lorsqu'une exception est lancée (avec throw), le traitement en cours est interrompu

L'exception remonte les fonctions appelantes jusqu'à être récupérée (avec catch)

Si elle n'est pas récupérée, alors le programme se termine


Remarques

Les exceptions permettent de séparer le traitement d'erreur du reste du code

Une exception n'est pas forcément une erreur (fonctionnement non prévu)

8.2 Syntaxe de la gestion de exceptions

Les instructions pouvant lever une exception doivent être à l'intérieur d'un bloc :

try { /*instructions*/ }


Un bloc try doit être immédiatement suivi d'au moins un bloc de gestion d'erreur :

catch () { /*instructions*/ }


Une exception est levée par l'instruction :

throw

8.2 Exemple simple


            int main()
            {
              auto diviseur = 0;
              try
              {
                if (diviseur == 0) throw diviseur;
                cout << "Q:" << dividende/diviseur << endl;
              }
              catch (int &d) // Exception handler for int              
              { 
                cout << "Division par " << d << endl; 
              }
              catch (...) // Exception handler for all other types
              { 
                cout << "Autre erreur" << endl; 
              }

              return 0;
            }
          

8.2 Exemple simple

Le premier gestionnaire compatible avec le type de l'exception l'attrape

Au plus un gestionnaire est exécuté pour une exception levée

Donc, si plusieurs gestionnaires sont appropriés pour le type de l'exception, seul le premier sera exécuté

Il convient donc de commencer par les gestionnaires les plus spécialisés, et terminer par le plus général

8.3 Le bloc try

Pour être gérable, une exception doit être levée dans un bloc try



                try
                {
                  throw "err!";
                }

                
              

                void foo()
                {
                  throw ERROR_CODE_27;
                }

                try
                {
                  foo();
                }

                

Si l'instruction throw n'est pas dans un bloc try, la levée de l'exception entraine la fin du programme ⇒ terminate()

⚠️ Attention : throw n'apparait pas toujours explicitement dans le code. Exemple avec l'opérateur new

8.3 Le bloc catch

Un bloc try doit être immédiatement suivi par au moins un gestionnaire catch



            try {
              ...
            }
            catch (int except1) { // Handle int exceptions
              ...
            }
            catch (std::string except2) { // Handle string exceptions
              ...
            }
            catch (std::exception &except3) { // Handle std::exception exceptions
              ...
            }
            catch (...) { // Handle all other exceptions
              ...
            }            
            

8.3 Le mot-clef throw

throw peut envoyer des messages de n'importe quel type, y compris des objets



            try { 
              switch(i) {
                case 1: throw 2;          break;
                case 2: throw "Erreur";   break;
                case 3: throw objet;      break;
              }
            }
            catch(int x) {
              cout << x;
            }
            catch(const char* str) {
              cout << str;
            }
            catch(Classe &obj) {
              cout << obj->afficher();
            }
          

8.3 Gestionnaires d'exceptions

Lorsqu'une exception de type T est levée, le compilateur cherche un gestionnaire :

1. de type T

2. de superclasse de T

3. pointeur sur une classe dérivée

4. type indéterminé : catch(...) (à placer en dernier)


Si l'exception est levée dans une fonction, le compilateur cherche un gestionnaire dans la fonction. Si ce n'est pas le cas, dans la fonction appelante, etc.

Le gestionnaire peut mettre fin au programme. Si ce n'est pas le cas, l'exécution continue par l'instruction qui suit les gestionnaires : la fin du bloc try n'est pas exécutée.

Si aucun gestionnaire n'est trouvé, il appelle la fonction terminate(). Par défaut, celle-ci appelle abort()

🌶️ 8.3 Déclaration et problématique

Le principal problème lié aux exceptions est la libération des ressources :

- Libérer les ressources allouées dynamiquement

- Fermer les flux


Solutions

1. Les allocateurs

2. RAII (Resource Acquisition Is Initialization)

8.4 Relance d'exceptions

Il est possible de relancer une exception déjà lancée

Il faut utiliser l'instruction throw sans argument


                void foo()
                {
                  try {
                    int n=2;
                    throw n;
                  }
                  catch(int) {
                    cout<<"foo \n";
                    throw;
                  }
                }
              
            

                int main()
                {
                  try {
                    foo();
                  }
                  catch(int) {
                    cout <<"main\n";
                    exit(-1);
                  }
                  
                  return 0;
                }
                

8.5 noexcept

Le mot clé noexcept indique que la fonction ne doit pas lever d'exception

Il est possible de spécifier une condition pour lever une exception



            // Do not throw an exception
            void f1() noexcept(true) { ... }

            // Might throw an exception
            void f2() noexcept(false) { ... }

            // Might throw an exception if expr is false
            void f3() noexcept(expr) { ... }

            // Might throw an exception if foo() may throw too
            void f4() noexcept(noexcept(foo())) { ... }
          

8.6 Les classes exceptions standards

La bibliothèque standard définit des classes exceptions

Leur superclasse commune est std::exception

Elle est définie dans le fichier d'en-tête stdexcept

Elle comporte une méthode what() retournant une chaîne de caractères

Les exceptions personnalisées hériteront donc de exception et la méthode what() what() sera redéfinie.

Double intérêt :

1. Transmettre des informations (attributs, what())

2. catch (const exception &e) plutôt que catch(...)

Les exceptions sont lancées par valeur et attrapées par référence

📚 Exceptions (cppreference)

8.6 Spécialisation de la classe std::exception



            #include <iostream>
            #include <stdexcept> // or #include <exception>
            using namespace std;
            
            class Exception1 : public exception {
              public:
              Exception1() noexcept {}
              const char* what() const noexcept override {
                return "exception1";
              }
            };
              
            class Exception2 : public exception {
              public:
              Exception2() noexcept {}
              const char* what() const noexcept override {
                return "exception2";
              }
            };
            

8.6 Surcharge de what()



            int main()
            {
              try {  
                cout << "bloc try 1" << endl;
                throw Exception1();
              }
              catch(const Exception1 & e) { 
                cout << e.what() << endl;
              }
              try { 
                cout << "bloc try 2" << endl;
                throw Exception2();
              }
              catch (const exception & e) {
                cout << e.what() << endl;
              }
              return 0;
            }
            

⚠️ Attention : une référence est nécessaire pour le polymorphisme