Les exceptions en C++

© D. Mathieu     mathieu@romarin.univ-aix.fr
I.U.T.d'Aix en Provence - Département Informatique
Créé le 20/12/1999 - Dernière mise à jour : 16/01/2000

Sommaire

Introduction
Schéma général
Type des exceptions
Exception dans une classe
Récupération d'exceptions diverses
Récupération de toutes les exceptions
Traitement partiel d'exceptions
Non récursivité des exceptions
Spécification des exceptions levées
Exceptions dans les constructeurs
Exceptions standard
Classe exception
Classes d'exceptions standard
Classe ios_base::failure
Classe bad_exception
Fonctions modifiant le comportement du programme
Fonction unexpected()
Fonction uncaught_exception()

Introduction

    En C++, une exception est un objet. Lorsque l'exception est levée (throw) directement dans un bloc try, ou dans une fonction plus ou moins profondément imbriquée, l'objet exception est propagé vers les couches plus externes du programme, jusqu'à être interceptée par le bloc catch appelé traitant d'exception, correspondant à son type. L'ensemble du bloc try et du ou des blocs catch qui lui sont associés (au même niveau) est une instruction, appelée par la suite bloc try-catch qui, comme le bloc d'instructions {...}, ne nécessite pas d'être terminée par un ; .

Schéma général

TQuelconque ExcX;
// ...
try
{
    Traitement ();      // contient, directement ou non, "throw ExcX;" 
}
catch (TQuelconque)
{
    TraiteException (); // par exemple message d'erreur 
}

Sommaire

Type des exceptions

    Le type d'une exception peut être par exemple un type énuméré :
 
enum TExc {CstExcOverflow, CstExcUnderflow};
// ...
try
{
    Traitement ();   // contient, directement ou non, "throw xxx;" où
                     // xxx est un objet de type TExc
}
catch (TExc UneExc)
{
    switch (UneExc)
    {
      case CstExcOverflow : 
        TraiteOverflow  (); 
        break;

      case CstExcOverflow : 
        TraiteUnderflow (); 
        break;

      default :
          TraiteDefault ();
          break
    }
}

ou bien une classe :
 
class CExc  {};    // contient un constructeur par défaut
// ...
try
{
    Traitement ();   // contient, directement ou non, "throw CExc();"
                     // qui renvoie l'objet de la classe CExc créé
                     // par l'appel de son constructeur
}
catch (CExc)
{
    TraitementException (); // par exemple message d'erreur
}

Sommaire

Exception dans une classe

    Une classe peut contenir une classe ou un type d'exception qui lui est propre. C'est le cas de l'exemple suivant :
 
class CList
{
    // ...
  public :
    // ...
    class CDebord {};    // contient un constructeur par défaut
    // ...

}; // CList

class CTree
{
    // ...
  public :
    // ...
    class CDebord {};    // contient un constructeur par défaut
    // ...

}; // CTree

class CArray
{
    // ...
  public :
    // ...
    class CDebord {};    // contient un constructeur par défaut
    // ...

}; // CArray

Sommaire

Récupération d'exceptions diverses

    En utilisant les déclarations ci-dessus, on peut écrire :
 
// ...
try
{
    Traitement ();     // manipule une liste, un arbre, un tableau
                       // qui peuvent lever une exception CDebord
}
catch (CList::CDebord)
{
    TraitExcList(); //       par exemple message d'erreur
}
catch (CTree::CDebord)
{
    TraitExcTree(); //       par exemple message d'erreur
}
catch (CArray::CDebord)
{
    TraitExcArray(); //       par exemple message d'erreur
}

    Si aucune exception n'est levée, les blocs catch sont sautés. Si une des exceptions est levée, le traitement du bloc catch correspondant est effectué et le contrôle passe à la suite du dernier bloc catch.

Sommaire

Récupération de toutes les exceptions

    Dans l'exemple ci-dessus, si seules les exceptions concernant les arbres méritent un traitement spécifique, on peut écrire la séquence suivante :
 
//...
try
{
    Traitement ();     // manipule une liste, un arbre, un tableau
                       // qui peuvent lever une exception CDebord
}
catch (CTree::CDebord)
{
    TraitExcTree ();   // par exemple message d'erreur
}
catch (...)
{
    TraiExcQuelc ();   // ce traitement ne peut évidemment pas dépendre
                       // de l'exception, puisque celle-ci est
                       // anonyme (pas nommée)
}

Sommaire

Traitement partiel d'exceptions

    Dans certains cas une exception peut n'être traitée localement que partiellement, et elle doit être levée de nouveau pour être capturée à un niveau supérieur. Si elle est identifiée, la solution est simple :
 
try
{
    Traitement (); // qui peut lever une exception CExc 
}
catch (CExc Exc)
{
    TraitePartielExc ();
    throw Exc;
}

    En revanche, l'exception peut rester anonyme, comme dans l'exemple ci-dessous :
 
// ...
try
{
    Traitement ();   // qui peut lever une exception CExc
}
catch (...)
{
    TraitPartiel (); // ce traitement ne peut évidemment pas dépendre 
                     // de l'exception, puisque celle-ci est 
                     // anonyme (pas nommée)
    throw;
}

    Une instruction throw; seule ne peut se trouver que dans un bloc catch.

Sommaire

Non récursivité des exceptions

    Dans le schéma ci-dessous, si l'exception ExcX est levée dans le traitement, le contrôle est passé au bloc catch qui relève la même exception. Celle-ci a pour effet de sortir du bloc qui contient les blocs try et catch et de se propager jusqu'au prochain bloc catch susceptible de la capturer :
 
//...
try
{
    // ...
    try
    { 
        Traitement (); // contient, directement ou non, "throw ExcX;"
    }
    catch (...)
    {
        TraitPartiel ();  throw;
    }
    //...
}
catch (...)
{
    // c'est ici qu'est passé le contrôle
}

Sommaire

Spécification des exceptions levées

    La norme du C++ recommande l'ajout de la spécification des exceptions susceptibles d'être levées, directement ou par l'appel d'autres fonctions, dans la déclaration d'une fonction, qu'elle soit globale ou membre d'une classe. Par exemple :
 
extern void Test (paramètres_formels) throw (CExcTypeA, CExcTypeB);

où  CExcTypeA et CExcTypeB sont deux types ou classes distinctes, en principe n'appartenant pas à la même hiérarchie. Ainsi, l'utilisateur est-il averti explicitement et peut-il prévoir leur traitement :
 
try
{
    // ...
    Test (paramètres_effectifs);
    // ...
}
catch (const CExcTypeA & Exc)
{
    // Traitement de l'exception de la classe CExcTypeA
}
catch (const CExcTypeB & Exc)
{
    // Traitement de l'exception de la classe CExcTypeB
}

    La définition de la fonction pourrait être :
 
void Test (paramètres_formels) throw (CExcTypeA, CExcTypeB)
{
    // ...
    if (Cond_1) throw CExcTypeA (parametres_du_constructeur);
    // ...
    if (Cond_2) throw CExcTypeB (parametres_du_constructeur);
    // ...

} // Test()

    Cependant, certains compilateurs non conformes n'autorisent pas cette écriture. Il est alors vivement recommandé de l'indiquer sous forme de commentaire :
 
extern void Test (paramètres_formels) /* throw (CExcTypeA, CExcTypeB) */;

    Une fonction qui ne comporte pas cette indication est, par défaut, susceptible de lever n'importe quelle exception. Cette règle permet d'assurer la compatibilité ascendante avec les anciennes versions de C/C++ qui ne comportaient pas cette possibilité.
 
extern void OldTest (paramètres_formels);

    Pour être sûr de capturer une exception provenant d'une telle fonction, il est donc nécessaire d'utiliser le symbole "ellipse" ... :
 
try
{
    // ...
    OldTest (paramètres_effectifs);
    // ...
}
catch (...)
{
    // Traitement
}

    Il est possible de garantir à l'utilisateur qu'une fonction ne doit lever aucune exception, et il est d'ailleurs très recommandé de le faire, de la façon suivante :
 
extern void NewTestSansExcept (paramètres_formels) throw ();

    Dans ce cas, le compilateur peut vérifier :

    Il est inutile d'énumérer les différentes classes d'exceptions susceptibles d'être levées, si celles-ci appartiennent à la même hiérarchie, il suffit d'indiquer la plus haute dans la hiérarchie. Certains compilateurs signalent ce fait. Le passage de l'exception par référence dans le blac catch permet de plus d'utiliser les ressources du polymorphisme.

    Lorsque des exceptions sont levées dans une fonction (y compris la fonction main()) sans y être interceptées, et qui ne sont pas signalées dans la signature (le profil) de la fonction, différents traitements standard sont appliqués. Ceux-ci peuvent être modifiés par les fonctions

Sommaire

Exceptions dans les constructeurs

    Il peut arriver qu'une erreur rédhibitoire empêche la construction de la totalité d'un objet.  Dans ce cas l'objet est considéré comme non créé. Cependant certaines opérations ont pu déjà être accomplies. Il faut donc veiller à ce qu'elles ne soient pas irréversibles. Considérons l'exemple suivant :
 
class CDoubleMem
{
    int Size;
    int *p;
    char *q;
  public :
    CDoubleMem (const int nb)
        if ((p = new int  [nb]) == 0) throw ExcMemInsuff,
        if ((q = new char [nb]) == 0) throw ExcMemInsuff;
    }
    // ...

}; CDoubleMem

     Si l'allocation de la mémoire à q échoue, l'espace déjà alloué à p sera définitivement perdu..Cependant, tous les objets non dynamique (données membres ou objet de la classe mère) déjà construits sont détruits dans l'ordre inverse.

    Il est possible d'intercepter des exceptions levées dans la partie initialisation d'un constructeur (c'est-à-dire dans l'appel des constructeurs des objets qui le constituent). La syntaxe est illustrée par l'exemple suivant :
 
class CX : public CBase
{
    CMembre m_Membre;

  public:
    CX (const CBase & Base, const CMembre & Membre);

}; // CX

CX::CX (const CBase & Base, const CMembre & Membre)
try
    : CBase (Base) , m_Membre (Membre)
{
    // Corps de la fonction constructeur
}
catch (...)
{
    // Traitants des exceptions levées par les constructeurs des données 
    //   données membres ou par le corps de la fonction constructeur elle-même
}

    Il ne jamais lever une exception dans un destructeur : le desctructeur peut lui-même être appelé lorsqu'une exception est levée. Donc, dans un destructeur, il faut toujours inclure l'appel d'une fonction susceptible de lever une exception à l'intérieur d'un bloc try-catch.

Sommaire

Exceptions standard

 La bibliothèque C++ propose la classe de base exception à partir de laquelle :     Elle offre aussi la possibilité de modifier le comportement standard du programme dans certaines circonstances particulières : exception levée mais jamais capturée, exception levée dans le corps d'une fonction mais jamais prévue dans son profil, etc...

    Dans tous les cas, le fichier entête <exception> doit être inclus.

Sommaire

Classe exception

        Elle offre :     Aucune de ces méthodes ne peut lever elle-même une exception, d'où, par exemple, la déclaration :
 
virtual const char* what() const throw();

    Il est possible de lever une exception de cette classe dans un programme, mais le constructeur n'ayant aucun paramètre, il est impossible d'initialiser la chaîne de caractères. En revanche, l'utilisation de la fonction what() permet quand même d'accéder à son contenu généré le compilateur, par exemple pour l'afficher. Selon les compilateurs, il peut être assez sibyllin. Il en est de même si on dérive la classe exception en une classe personnelle, CException par exemple, sans ajouter de traitement particulier, mais le message n'est pas obligatoirement le même. Par exemple, la chaîne est initialisée à Unknown exception  par Visual C++ 5.0 de Microsoft, dans les deux cas,

Sommaire

Classes d'exceptions standard

    Leur utilisation nécessite l'inclusion du fichier <stdexcept>. Elles sont dérivées de la classe exception et sont organisées selon la hiérarchie suivante :

    Contrairement à leur classe mère, les constructeurs de toutes ces classes, à l'exclusion des trois premières, ont pour paramètre un string, comme par exemple :
 
logic_error::logic_error (const string& what_arg);

    Les exceptions des deux premières classes (bad_cast et  bad_typeid) sont levées lors de transtypages ou d'identification de type à l'exécution (RTTI).

    Les exceptions de la classe bad_alloc sont levées lors de l'échec d'une allocation mémoire.

    Les exceptions de la classe logical_error sont des exceptions correspondant à des erreurs qui pourraient être détectées lors de la compilation, comme par exemple la tentative d'accès à un élément à un rang invalide (dans un conteneur par exemple).

    Les exceptions de la classe runtime_error sont des exceptions correspondant à des erreurs qui ne pourraient être détectées que lors de l'exécution du programme, comme par exemple une division par zéro.

     La classe ios_base::failure correspond à des exceptions levées lors d'opérations d'entrées/sorties..

Remarque :

    Certaines erreurs de calcul - division par zéro, débordement de la mantisse d'un nombre réel devient trop grande (positivement ou négativement), etc... - sont détectées par la machine (hardware) et/ou par le système d'exploitation qui en informe éventuellement l'application. Sous Unix par exemple, le processus reçoit un signal SIGFPE par exemple (SIGnalFloating Point Error). Ce mécanisme est étudié en détail lors des TPs de système en seconde année. Si ce signal ne subit pas un traitement approprié, le processus est avorté, et un message apparaît :
 
Floating exception

    Cependant, il ne s'agit pas d'une exception au sens du C++, et elle n'est pas récupérable par catch (...). En revanche, Visual C++ 5.0 de Microsoft reçoit bien une exception lors d'une division par 0, qui peut être capturée par catch (...).

Sommaire

Classe ios_base::failure

    Une exception de cette classe peut être levée par différentes opérations sur les flux tamponnés (stream buffer) sous réserve que le traitement par défaut ait été modifié. Par défaut, l'échec de certaines opérations (ouverture d'un flux, extraction d'un flux, etc...) provoquent le positionnement de certaines données membres de la classe ios_base : ios_base::failbit, ios_base::eofbit, ios_base::badbit. Ces booléens peuvent ensuite être testées dans le programme en appelant les prédicats correspondants : ios_base::fail(), ios_base::eof(), ios_base::bad().
 
ifstream is;
is.open ("Fichier", ios::in);
if (is.fail()) 
    ...
else
{
    int Entier;
    is >> Entier;
    if (is.bad()) 
         ...
    else if (is.eof())
        ...
    else
    {
        ...
    }
}

    Une autre possibilité est de forcer le programme à lever une exception de la classe ios_base::failure lorsque le bit est positionné, au moyen de la fonction ios_base::exceptions() de profil :
 
void  ios_base::exceptions (iostate except);

 où except est un masque de bits de type ios_base::iostate, dans lequel peuvent être positionnés les bits correspondant à badbit, eofbit et failbit.

    Le développeur n'a plus à tester individuellement chaque opération mais peut regrouper les traitements d'erreurs en un même point. Par exemple :
 
ifstream is;
is.exceptions (ios_base::badbit | ios_base::eofbit | ios_base::failbit);
try 
{
    is.open ("Fichier", ios::in);
    int Entier;
}
catch ()
{
    // ...
}

Sommaire

Classe bad_exception

    Dérivée publiquement de la classe exception, elle offre une surcharge de toutes les méthodes de sa classe mère. Pour son utilisation, voir la fonction unexpected().

Sommaire

Fonctions modifiant le comportement du programme

Fonctions unexpected(), set_unexpected()

    La fonction standard C++ unexpected(), de profil :
 
void unexpected();

est automatiquement appelée lorsqu'une fonction reçoit une exception qui n'a pas été spécifiée dans son profil. Elle appelle elle-même le traitant (la fonction) d'exception inattendue actif à ce moment-là. Le traitant par défaut d'une exception inattendu est la fonction terminate().

    La fonction  peut aussi être directement appelée dans un programme.

    Une fonction f, de type unexpected_handler défini par :
 
typedef void (*unexpected_handler) ();

peut être substituée au traitant courant d'exeption inatttendue (par défaut à terminate()) au moyen de la fonction set_unexpected() :
 
unexpected_handler set_unexpected (unexpected_handler f) throw ();

     La fonction set_unexpected() renvoie un pointeur vers le précédent traitant.

    La fonction unexpected() ne doit pas revenir normalement. Le traitant d'exception inattendue qui lui est associé ne doit donc pas lui rendre le contrôle. Il peut soit terminer le programme (par l'appel d'une fonction de terminaison terminate(), abort() ou exit()), soit relever une exception qui, elle, ferait partie de la signature de la fonction qui a initialement causé le déclenchement  de ce mécanisme.

    Par exemple :
 
unexpected_handler TraitantDefaut ()
{
    throw CExcListe();

} // TraitantDefaut()

void FctTest () throw (CExcListe)
{
    if (...)
        throw CExcMemoire (); // CExcListe et CExcMemoire
                              //   n'appartiennent pas à la même hiérarchie
} // FctTest()

int main (...)
{
    // ...
    unexpected_handler OldTraitantDefaut = set_unexpected (TraitantDefaut);
    //
    try
    {
        FctTest ();
    }
    catch (const CExcListe & Exc)
    {
        // ...
    }
    // Restauration du traitant par défaut

    set_unexpected (OldTraitantDefaut);

} // main()

    Si l'exception relevée par le traitant d'exception inattendue ne fait elle-même pas partie de la signature de la fonction qui a initialement causé le déclenchement  de ce mécanisme :

Sommaire

Fonction uncaught_exception()

    La fonction uncaught_exception() :
 
bool uncaught_exception();

renvoie true si une exception est en cours de traitement au moment où elle est appelée. Cela permet à une fonction de modifier son comportement selon qu'elle est appelée normalement dans un programme ou au sein d'un traitement d'exception.

Sommaire

© D. Mathieu     mathieu@romarin.univ-aix.fr
I.U.T.d'Aix en Provence - Département Informatique