FAQ C++Consultez toutes les FAQ
Nombre d'auteurs : 34, nombre de questions : 368, dernière mise à jour : 14 novembre 2021 Ajouter une question
Cette FAQ a été réalisée à partir des questions fréquemment posées sur les forums de http://www.developpez.com et de l'expérience personnelle des auteurs.
Je tiens à souligner que cette FAQ ne garantit en aucun cas que les informations qu'elle propose sont correctes ; les auteurs font le maximum, mais l'erreur est humaine. Cette FAQ ne prétend pas non plus être complète. Si vous trouvez une erreur ou si vous souhaitez devenir rédacteur, lisez ceci.
Sur ce, nous vous souhaitons une bonne lecture.
- Qu'est-ce qu'une exception ?
- Comment lever une exception ?
- Comment capturer les exceptions dans mon code ?
- Pourquoi faut-il capturer les exceptions par référence ?
- Est-il possible de capturer plusieurs exceptions dans un seul catch ?
- Comment relancer une exception que l'on a capturé ?
- Que se passe-t-il si aucun bloc catch n'existe pour traiter une exception ?
- Comment créer son propre type d'exception ?
- Peut-on lever des exceptions dans les constructeurs ?
- Peut-on lever des exceptions dans les destructeurs ?
- [C++11] Comment indiquer qu'une fonction ne lève jamais d'exception ?
- [C++98] Comment indiquer qu'une fonction ne lève jamais d'exception ?
- Quel est l'équivalent C++ du bloc finally des autres langages ?
Les exceptions sont un nouveau moyen de gérer les erreurs dans les programmes. La grande différence vis à vis du classique code d'erreur renvoyé par une fonction est qu'une exception se propage depuis l'appelé vers l'appelant jusqu'à ce qu'elle rencontre un bloc de code qui s'occupe de la traiter. Au contraire d'un code d'erreur qui peut être ignoré (ce qui est malheureusement souvent le cas) une exception doit être traitée. Si elle ne l'est pas dans la fonction qui en est à l'origine, elle doit l'être dans l'une des fonctions appelantes. Le compilateur s'occupe tout seul de faire en sorte que l'exception remonte le long de la pile des appels jusqu'à l'endroit où un bloc a été prévu pour la traiter. Cela permet donc de facilement faire « remonter » les erreurs des fonctions appelées vers les fonctions appelantes. L'apparition d'une exception interrompt l'exécution normale du programme et provoque sa reprise dans le gestionnaire d'exception le plus proche (qui peut se trouver beaucoup plus en amont dans une fonction appelante).
Le programmeur n'a donc plus à se soucier de tester la réussite ou non des fonctions qu'il appelle au moyen d'un grand nombre de tests comme dans l'exemple suivant :
Code c++ : | Sélectionner tout |
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 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 | // ces fonctions renvoient false en cas d'erreur bool F1(); bool F2(); bool F3(); bool F4(); bool Test1() { // appeler F1 et F2 if ( !F1() ) { return false; } if ( !F2() ) { return false; } return true; } bool Test2() { // appeler Test1 et F3 if ( !Test1() ) { return false; } if ( !F3() ) { return false; } return true; } bool Test3() { // appeler Test2 et F4 if ( !Test2() ) { return false; } if ( !F4() ) { return false; } return true; } int main() { if ( !Test3() ) { std::cerr << "Une erreur est survenue, mais je ne sais pas où !"; } } |
Code c++ : | Sélectionner tout |
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 27 28 29 30 31 32 33 34 35 36 37 38 39 | // ces fonctions lèvent des exceptions en cas d'erreur void F1(); void F2(); void F3(); void F4(); void Test1() { F1(); F2(); } void Test2() { Test1(); F3(); } void Test3() { Test2(); F4(); } int main() { try { Test3(); } catch ( const std::bad_alloc & ) { std::cerr << "Erreur : mémoire insuffisante.\n"; } catch ( const std::out_of_range & ) { std::cerr << "Erreur : débordement de mémoire.\n"; } } |
Les exceptions sont déclenchées grâce à l'utilisation du mot-clé throw :
Code c++ : | Sélectionner tout |
1 2 | // lève une exception de type e throw e; |
Code c++ : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 | try { // instructions pouvant déclencher des exceptions // dérivant de std::exception } catch ( const std::exception & e ) { std::cerr << e.what(); } |
Code c++ : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 | try { // déclencher une exception par pointeur throw new int( 10 ); } catch ( const int * e ) { std::cerr << "Erreur numéro " << *e; } |
Code c++ : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | #include <iostream> #include <stdexcept> int main() { try { // std::logic_error est une classe standard // qui dérive de std::exception throw std::logic_error( "Exemple d'exception" ); } catch ( const std::exception & e ) { // affiche "Exemple d'exception" std::cerr << e.what(); } } |
Code c++ : | Sélectionner tout |
1 2 3 4 5 6 7 8 | try { throw "Message d'erreur"; } catch ( const char * Msg ) { std::cerr << Msg; } |
Si vous persistez à vouloir utiliser de simple chaînes de caractères au lieu d'une classe dérivant de std::exception, utilisez au moins le type chaîne de caractères du C++ : std::string.
Code c++ : | Sélectionner tout |
1 2 3 4 5 6 7 8 | try { throw std::string( "Message d'erreur" ); } catch ( const std::string & Msg ) { std::cerr << Msg; } |
Le code susceptible de déclencher des exceptions doit être placé dans un bloc try...catch (essaye...attrape) de cette manière :
Code c++ : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 | int * ptr; try { // tenter d'allouer 100 entiers ptr = new int [ 100 ]; } catch ( const std::bad_alloc & ) { // échec de l'allocation } |
Code c++ : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | try { // créer un tableau de taille 10 std::vector<int> tableau( 10 ); // accéder au 11° élément tableau.at( 10 ); } catch ( const std::exception & Exp ) { std::cerr << "Erreur : " << Exp.what() << ".\n"; } catch ( const std::bad_alloc & ) { std::cerr << "Erreur : mémoire insuffisante.\n"; } catch ( const std::out_of_range & ) { std::cerr << "Erreur : débordement de mémoire.\n"; } |
Il existe aussi un moyen d'attraper toutes les exceptions, en utilisant une ellipse (...) comme type de l'exception. Mais alors il n'y a aucun moyen de connaître l'origine et le type de l'exception (sauf à la relancer et la traiter dans un nouveau bloc try...catch). L'utilisation de cette forme générique doit être restreinte car elle ne permet de savoir si l'exception capturée peut être traitée et ignorée ou si elle nécessite de terminer le programme (corruption de la mémoire, etc.). On l'utilise en général comme dernier recours.
Code c++ : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | try { // créer un tableau de taille 10 std::vector<int> tableau( 10 ); // accéder au 11° élément tableau.at( 10 ); } catch ( const std::bad_alloc & ) { std::cerr << "Erreur : mémoire insuffisante.\n"; } catch ( const std::out_of_range & ) { std::cerr << "Erreur : débordement de mémoire.\n"; } catch ( const std::exception & Exp ) { std::cerr << "Erreur : " << Exp.xhat() << ".\n"; } catch ( ... ) // traite toutes les autres exceptions { std::cerr << "Erreur inconnue.\n"; } |
Notez que les exceptions sont récupérées par référence, comme cela est expliqué dans la question Pourquoi faut-il capturer les exceptions par référence ?. Ces références ne sont cependant pas des références sur l'objet initial qui est à l'origine de l'exception, mais sur une copie de celui-ci, car l'objet initial risque d'être détruit si l'on quitte la fonction qui a levé l'exception.
Comme discuté dans la question Comment lever une exception ?, il est fortement recommandé de lever des exceptions par valeur. En revanche, il vaut mieux les capturer par référence et non pas par valeur. Tout d'abord cela permet d'éviter une recopie, mais aussi et surtout cela permet de conserver le polymorphisme. L'exemple suivant illustre les problèmes posés par un traitement des exceptions par valeur :
Code c++ : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | #include <iostream> #include <stdexcept> int main() { using namespace std; try { // std::logic_error hérite de std::exception throw logic_error( "exception de test" ); } catch ( exception e ) // traitement par valeur { cerr << e.what(); } } |
Code c++ : | Sélectionner tout |
catch ( const exception & e ) // traitement par référence
Donc à moins de rechercher volontairement ce comportement, il est recommandé de traiter les exceptions par référence, de préférence constantes afin de permettre au compilateur d'effectuer des optimisations.
Malheureusement, non, ce mécanisme n'est pas possible. Un catch ne pouvant capturer qu'un seul type d'exceptions, il faut définir autant de blocs try/catch qu'il y a d'exceptions possibles.
L'utilisation de
Code c++ : | Sélectionner tout |
1 2 3 4 5 | try { // ... } catch(...) { // ... } |
Le mot-clé throw permet de lever une nouvelle exception, mais aussi de relancer celle qui est en cours de traitement.
Code c++ : | Sélectionner tout |
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 27 28 | #include <iostream> #include <stdexcept> void Test() { try { throw std::logic_error( "Exception de test" ); } catch ( const std::logic_error & e ) { std::cerr << "L'exception '" << e.what() << "' a été levée et va être relancée.\n"; throw; // relancer l'exception courante } } int main() { try { Test(); } catch ( const std::logic_error & e ) { std::cerr << "Erreur : " << e.what() << ".\n"; } } |
Lorsqu'une exception est déclenchée, le compilateur recherche un bloc catch capable de la traiter. S'il n'en trouve pas, il remonte la pile d'exécution (déroulage de la pile) afin d'en trouver un plus en amont dans la hiérarchie des appels. Dépiler un appel revient à quitter une fonction. A cette occasion ses objets locaux sont détruits et les destructeurs appelés, ce qui permet de quitter proprement la fonction en libérant toutes les ressources acquises si les destructeurs on été bien écrits. L'objet qui a servi à lever l'exception est lui même détruit car il est local à la fonction. C'est pourquoi l'objet qui est transmis aux blocs catch est toujours une copie de l'objet initial qui a déclenché l'exception.
Si la pile des appels est vidée (donc que l'on est arrivé à main) et qu'aucun bloc catch satisfaisant n'a été trouvé, la fonction standard terminate est appelée ce qui provoque par défaut un arrêt pur et simple du programme. Ce comportement peut être modifié au moyen de la fonction set_terminate définie dans l'en-tête standard <exception>. Cette fonction installe un nouveau gestionnaire et renvoie l'adresse du précédent. Elle s'utilise de cette manière :
Code c++ : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | #include <iostream> #include <exception> // ancien gestionnaire void (*old_handler)(); // gestionnaire personnalisé void my_handler() { std::cerr << "Exception inattendue.\n"; // appel du gestionnaire par défaut (old_handler)(); } int main() { // installer notre gestionnaire personnalisé old_handler = set_terminate( my_handler ); // lever une exception que l'on ne traite pas throw "test"; } |
This application has requested the Runtime to terminate it in an unusual way.
Please contact the application's support team for more information.
N'importe quel type de base ou classe C++ peut être utilisé comme type d'exception. Mais il est préférable de créer son type qui hérite de la classe de base standard pour les exceptions : std::exception définie dans l'en-tête <exception>. Cette classe possède une fonction membre virtuelle what() qu'il convient de redéfinir :
Code c++ : | Sélectionner tout |
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 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | #include <iostream> #include <sstream> #include <exception> class my_exception : public std::exception { public: my_exception( const char * Msg, int Line ) { std::ostringstream oss; oss << "Erreur ligne " << Line << " : " << Msg; this->msg = oss.str(); } virtual ~my_exception() throw() { } virtual const char * what() const throw() { return this->msg.c_str(); } private: std::string msg; }; int main() { try { throw my_exception( "exception test", __LINE__ ); } catch ( const std::exception & e ) { std::cerr << e.what() << "\n"; } } |
Erreur ligne 29 : exception test
Tout à fait. C'est même une des seules manières d'indiquer que l'initialisation de l'objet a échoué. Il faut cependant être prudent car une exception levée dans un constructeur peut être à l'origine de fuites de mémoires ou d'autres problèmes de non libération de ressources. C'est le cas dans l'exemple suivant :
Code c++ : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | class Test { public: // exception bad_alloc en cas de mémoire insuffisante Test( int A, int B ) : tableau1( 0 ), tableau2( 0 ) { // ici tableau1 et tableau2 valent NULL, donc en cas d'échec d'allocation // on peut appeler delete [] sans problème dans le destructeur this->tableau1 = new int[ A ]; this->tableau2 = new int[ B ]; } ~Test() { delete [] this->tableau2; delete [] this->tableau1; } private: int * tableau1; int * tableau2; }; |
Le problème est que si une exception est levée lors de la construction d'un objet, c'est donc que celle-ci a échoué, et donc que l'objet n'est pas créé. Comme il n'est pas créé, il n'a pas à être détruit, et donc son destructeur ne sera pas appelé. Autrement dit, si une exception est levée dans le constructeur d'un objet, son destructeur ne sera pas appelé.
Il faut donc toujours s'assurer que le code contenu dans le constructeur est exception safe, c'est-à-dire qu'il résiste aux exceptions en ne provoquant pas de pertes de ressources. Dans notre exemple précédent cela signifie qu'il faut gérer l'exception bad_alloc de cette manière :
Code c++ : | Sélectionner tout |
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 27 28 29 30 31 32 33 | class Test { public: // exception bad_alloc en cas de mémoire insuffisante Test( int A, int B ) : tableau1( 0 ) { // ici tableau1 vaut NULL, donc en cas d'échec d'allocation // on peut appeler delete [] sans problème try { this->tableau1 = new int[ A ]; this->tableau2 = new int[ B ]; } catch ( const std::bad_alloc & ) { // tableau2 n'a pas été alloué quoi qu'il arrive // libérer tableau1 s'il a pu être alloué delete [] this->tableau1; // relancer l'exception throw; } } ~Test() { delete [] this->tableau2; delete [] this->tableau1; } private: int * tableau1; int * tableau2; }; |
Il est à noter que si en cas d'exception dans le constructeur le destructeur n'est pas appelé, tous les membres construits jusqu'au point de l'exception sont quant à eux bien détruits.
Il est possible de lever une exception dans un destructeur, mais c'est extrêmement déconseillé et considéré comme une très mauvaise pratique. La raison en est simple : si la destruction d'un objet échoue, que faut-il faire ? Mais aussi un autre problème plus grave peut apparaître. Lorsqu'une exception est levée, la pile des appels est remontée (on parle de stack unwinding ou déroulage de la pile) et à cette occasion les objets locaux de la fonction que l'on s'apprête à quitter sont détruits. Donc leur destructeur respectif est appelé. Si l'un d'entre eux vient à lever une exception, la situation devient alors très complexe : laquelle des deux exceptions faut-il gérer ? N'oubliez pas qu'à ce moment nous ne sommes toujours pas dans un bloc catch, mais en train de nous y rendre en quittant les fonctions appelées qui nous en sépare.
Or, en quittant l'une d'entre elles, on détruit un objet qui lance une nouvelle exception, et nous nous retrouvons alors avec deux exceptions à traiter en même temps. La situation étant insoluble, la norme définit que la fonction standard terminate est appelée dans un tel cas, ce qui provoque la fin brutale du programme.
Pour cette très bonne raison, il est important que les destructeurs ne lèvent jamais d'exceptions. On peut s'en assurer en appelant uniquement des fonctions n'échouant jamais (voir Comment indiquer qu'une fonction ne lève jamais d'exception ?).
Il est possible d'utiliser noexcept pour le faire, et il est particulièrement recommandé de le faire pour certaines opérations comme les constructeurs par déplacement [Ajouter un lien vers une nouvelle entrée de la faq à créer sur le sujet].
Cette déclaration peut s'utiliser de manière simple ou conditionnelle :
Code : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | void f1() noexcept // Cette fonction ne lance pas d'exceptions { //... } void f2() noexcept(true) // Cette fonction non plus { //... } void f3() noexcept(noexcept(g) && noexcept(h)) // Cette fonction ne lance pas d'exceptions si g et h sont aussi déclarés noexcept { //... } |
Certaines fonctions sont par défaut noexcept : Les destructeurs et les operator delete.
À noter : Les spécifications d’exceptions sont dépréciées, et leur usage n'est pas recommandé.
Il n'y a pas de moyen direct. Utiliser une spécification d'exception vide permet de simuler ça, et certains compilateurs considèrent ce cas un peu différemment d'une spécification d'exception non vide:
Code : | Sélectionner tout |
1 2 3 | void Test() throw () { } |
Dans certains langages (tels que Java ou C#), il est possible de créer un bloc finally à la suite d'un bloc try...catch afin de s'assurer qu'une opération (de libération de ressource par exemple) soit bien effectuée. Par exemple, en C++ on ne peut pas écrire ceci :
Code c++ : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 | char * buffer = new char[ 100 ]; try { // opération susceptible de lever une exception } finally { // s'assurer que la mémoire est libérée delete [] buffer; } |
Code c++ : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | char * buffer = new char[ 100 ]; try { // opération susceptible de lever une exception } catch ( ... ) { // éviter les fuites de mémoire delete [] buffer; // relancer l'exception throw; } // tout s'est bien passé, libérer la mémoire delete [] buffer; |
Ce principe s'appelle le RAII, et est développé dans la question Comment gérer proprement des allocations / désallocations de ressources ? Le RAII !.
Proposer une nouvelle réponse sur la FAQ
Ce n'est pas l'endroit pour poser des questions, allez plutôt sur le forum de la rubrique pour çaLes sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2024 Developpez Developpez LLC. Tous droits réservés Developpez LLC. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents et images sans l'autorisation expresse de Developpez LLC. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.