IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)
logo
Sommaire > Utilisation des exceptions
        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 ?
        Comment indiquer qu'une fonction ne lève jamais d'exception ?
        Quel est l'équivalent C++ du bloc finally des autres langages ?



Qu'est-ce qu'une exception ?
Créé le 22/11/2004[haut]
auteur : Aurélien Regat-Barrel
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 :
// 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  !".
    }
}
Les exceptions permettent de grandement simplifier le code précédent tout en améliorant la qualité du renseignement sur l'origine de l'erreur :
// 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":
    }
}

Comment lever une exception ?
Créé le 22/11/2004[haut]
auteur : Aurélien Regat-Barrel
Les exceptions sont déclenchées grâce à l'utilisation du mot-clé throw :
// lève une exception de type e
throw e;
Les exceptions peuvent être de n'importe quel type (type de base du langage ou classe quelconque). Mais il est conseillé d'utiliser une classe dérivant de la classe standard std::exception définie dans le fichier d'en-tête standard <exception>. Cette classe dispose d'une fonction membre what() qui renvoie une description de l'exception.
try
{
    // instructions pouvant déclencher des exceptions
    // dérivant de std::exception
}
catch ( const std::exception & e )
{
    std::cerr << e.what();
}
Les exceptions doivent être de préférence déclenchées par valeur, et attrapée par référence. Déclencher une exception par pointeur pose en effet un problème :
try
{
    // déclencher une exception par pointeur
    throw new int( 10 );
}
catch ( const int * e )
{
    std::cerr << "Erreur numéro " << *e;
}
Le code précédent fonctionne mais une question subsiste : comment est libéré le pointeur alloué dans le bloc try ? La réponse est qu'il ne l'est pas, et il se produit donc une fuite de mémoire. Le problème de déclencher une exception par pointeur est donc de savoir à qui incombe la responsabilité de libérer ce dernier. Voilà pourquoi on préfère lever des exceptions par valeur, et les attraper par référence (de préférence constante) afin de permettre le polymorphisme et d'améliorer les performances en évitant une recopie de l'objet.
#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();
    }
}
Un cas particulier est celui des chaînes de caractères littérales :
try
{
    throw "Message d'erreur";
}
catch ( const char * Msg )
{
    std::cerr << Msg;
}
Dans l'exemple précédent, bien qu'on utilise un pointeur, il n'y a pas de fuite de mémoire tout simplement parce qu'il n'y a pas d'allocation dynamique. Ce serait même une grosse erreur de libérer ce pointeur avec delete [] car ce dernier pointe vers une zone de mémoire spéciale (généralement en lecture seule) qui contient l'ensemble de chaînes de caractères littérales utilisées dans le programme. Donc, à priori, utiliser des chaînes littérales est une bonne idée, mais cela est néanmoins déconseillé car leur utilité est vite limitée du fait de l'impossibilité de formater les chaînes pour donner la valeur d'une variable par exemple. Il y a fort à craindre que tôt ou tard un programmeur voudra effectuer cette opération et provoquera alors inconsciemment une fuite de mémoire (car dans le bloc catch il n'y a aucun moyen de distinguer une chaîne littérale d'une chaîne allouée avec new []).
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.
try
{
    throw std::string( "Message d'erreur" );
}
catch ( const std::string & Msg )
{
    std::cerr << Msg;
}

Comment capturer les exceptions dans mon code ?
Mise à jour le 17/03/2008[haut]
auteur : Aurélien Regat-Barrel
Le code susceptible de déclencher des exceptions doit être placé dans un bloc try...catch (essaye...attrape) de cette manière :
int * ptr;
try
{
    // tenter d'allouer 100 entiers
    ptr = new int [ 100 ];
}
catch ( const std::bad_alloc & )
{
    // échec de l'allocation
}
On peut mettre autant de blocs catch qu'il y a d'exceptions à rattraper.
Mauvais chaînage des blocs catch
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";
}
Les exceptions levées dans le bloc try vont être filtrées par les différents blocs catch suivant leur ordre d'apparition. Ce filtrage est effectué en fonction du type de l'exception, et est naturellement sensible au polymorphisme. C'est-à-dire que le premier bloc catch rencontré capable de traiter l'exception levée est celui qui est utilisé. Les autres sont ignorés, même si certains seraient mieux adaptés. En l'occurrence, dans l'exemple précédent, l'exception out_of_range est levée et ce serait tout naturellement le dernier bloc catch qui devrait la traiter. Mais std::out_of_range dérive de std::exception qui est la classe de base pour les exceptions standards, et donc c'est le premier bloc catch qui est appelé.
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.
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";
}
Il est donc important de faire apparaître les blocs catch des classes dérivées en premier.
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.


Pourquoi faut-il capturer les exceptions par référence ?
Créé le 22/11/2004[haut]
auteur : Aurélien Regat-Barrel
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 :
#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();
    }
}
Cet exemple affiche le message Unknown exception. Si l'on remplace le traitement par valeur par un traitement par référence :
catch ( const exception & e ) // traitement par référence
Alors on obtient le message attendu exception de test. Ceci est dû au fait que le polymorphisme nécessite d'utiliser un pointeur ou une référence, autrement dans notre cas l'objet de type std::logic_error est "tronqué" en un objet de type std::exception. La fonction membre what appelée n'est donc pas celle de std::logic_error mais celle de std::exception, qui n'est pas d'une grande utilité.
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.


Est-il possible de capturer plusieurs exceptions dans un seul catch ?
Créé le 09/10/2003[haut]
auteur : LFE
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
try {
// ...
} catch(...) {
// ...
}
permet de capturer toutes les exceptions pouvant survenir, mais il est, dans ce cas, impossible de faire la distinction.


Comment relancer une exception que l'on a capturé ?
Créé le 22/11/2004[haut]
auteur : Aurélien Regat-Barrel
Le mot-clé throw permet de lever une nouvelle exception, mais aussi de relancer celle qui est en cours de traitement.
#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";
    }
}

Que se passe-t-il si aucun bloc catch n'existe pour traiter une exception ?
Créé le 22/11/2004[haut]
auteur : Aurélien Regat-Barrel
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 :
#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";
}
Le code précédent provoque le résultat suivant avec le compilateur Visual C++ 7.1 :

Exception inattendue. This application has requested the Runtime to terminate it in an unusual way. Please contact the application's support team for more information.


Comment créer son propre type d'exception ?
Mise à jour le 17/03/2008[haut]
auteur : Aurélien Regat-Barrel
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 :
#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";
    }
}
Cet exemple produit le résultat suivant :

Erreur ligne 29 : exception test


Peut-on lever des exceptions dans les constructeurs ?
Créé le 22/11/2004[haut]
auteurs : Aurélien Regat-Barrel, Luc Hermitte
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 :
Exemple de mauvaise gestion d'exceptions
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 vallent 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;
};
L'idée du code ci-dessus est d'initialiser les pointeurs tableau1 et tableau2 à zéro ainsi si l'une des allocations échoue on peut tout de même appeler en toute sérénité delete [] dans le destructeur et ainsi éviter les fuites de mémoire (souvenez vous, appeler delete sur un pointeur nul ne fait rien, voir Que se passe-t-il si je fais un delete sur un pointeur qui vaut NULL ?).
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 :
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;
};
Le nouveau code produit toujours une exception bad_alloc en cas d'échec d'allocation, mais cette fois-ci il n'y a plus de fuite de mémoire. Ce qui a pu être alloué est libéré, et l'exception bad_alloc capturée est relancée via l'instruction throw (à ce sujet lire Comment relancer une exception que l'on a capturé ?).
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.


Peut-on lever des exceptions dans les destructeurs ?
Créé le 22/11/2004[haut]
auteur : Aurélien Regat-Barrel
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 ?).

lien : en Question de la C++ FAQ Lite ayant inspiré cette réponse

Comment indiquer qu'une fonction ne lève jamais d'exception ?
Créé le 22/11/2004[haut]
auteur : Aurélien Regat-Barrel
Pour indiquer qu'une fonction ne lève jamais d'exceptions (ce qui est important si on veut l'appeler depuis un destructeur par exemple (voir Peut-on lever des exceptions dans les destructeurs ?), on rajoute l'instruction throw () à la suite du prototype de la fonction de cette façon :
void Test() throw ()
{
}
Attention à bien faire en sorte qu'aucune exception ne soit effectivement levée, car la norme dit que si tel était le cas la fonction standard unexpected serait appelée, ce qui se traduirait par un arrêt brutal du programme.


Quel est l'équivalent C++ du bloc finally des autres langages ?
Créé le 17/10/2005[haut]
auteur : Aurélien Regat-Barrel
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 :

char * buffer = new char[ 100 ];
try
{
    // opération suceptible de lever une exception
}
finally
{
    // s'assurer que la mémoire est libérée
    delete [] buffer;
}
l'équivalent de l'écriture ci-dessus serait :

char * buffer = new char[ 100 ];
try
{
    // opération suceptible de lever une exception
}
catch ( ... )
{
    // éviter les fuites de mémoire
    delete [] buffer;
    // relancer l'exception
    throw;
}
// tout s'est bien passé, librérer la mémoire
delete [] buffer;
Mais il ne s'agit là que d'une traduction en C++ d'une approche issue d'un autre langage. Or, quand on programme dans un langage, il convient de le faire selon les concepts propres à ce langage, et non avec ceux d'un autre. L'approche C++ à ce problème consiste à encapsuler cette gestion au sein d'un objet qui s'assurera dans son destructeur de la bonne libération de la ressource qu'il gère. Ainsi, on est assuré de ne pas avoir de fuite même en cas d'exception, tout en ayant une écriture plus légère car le bloc try...catch devient inutile dans ce cas.
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 !.



Consultez les autres F.A.Q.


Valid XHTML 1.0 TransitionalValid CSS!

Les 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 © 2008 Developpez LLC. Tous droits réservés Developpez LLC. Aucune reproduction, même partielle, ne peut être faite de ce site ni 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.