Dès qu’elle atteint une certaine taille, il est important qu’une application soit décomposée en plusieurs niveaux hiérarchiques clairs, pour sa lisibilité, sa maintenabilité et son évolutivité.
Le plus haut niveau est la fonction main()
.
C’est par l’exécution de cette fonction que commence (presque …) l’application.
Il est possible de lui faire passer des informations (des arguments) au lancement de la commande correspondante : options, nombres, noms de fichiers, etc. :
CopyFile FicDest FicSource -b
Cette possibilité a déjà été étudiée.
Le rôle de la fonction main()
est donc de commencer par analyser et valider ces arguments.
Puis vient l’exécution d’une ou de plusieurs fonctions relativement générales (pour nous les fonctions testXxx()
).
Enfin, avant de “rendre la main” au système, la fonction main()
doit clore proprement l’application :
- ne pas laisser remonter une exception non capturée, qui provoquerait le désagréable et sibyllin message :
abort
sans autre explication,
- préparer le “compte-rendu” de l’exécution sous la forme d’un entier renvoyé à la procédure appelante (en général le shell) : la fameuse variable
$?
.
En d’autres termes, une application pourrait avoir la structure suivante :
... int main (...) { validerArguments (); initialisation (); phase_1 (); ... phase_N (); terminaison (); return compteRendu; } // main()
En principe, dans une application idéale, toutes les exceptions levées devraient être traitées localement, ou à un niveau hiérarchique supérieur, mais aucune ne devraient remonter jusqu’à la fonction main()
.
On peut cependant supposer :
- que certaines exceptions sont levées “à l’insu” du développeur, et non capturées par la suite (nul n’est parfait !)
- que certaines exceptions sont délibérément ignorées et non capturées car l’erreur est tellement grave qu’aucun traitement ne peut la corriger et que la seule chose à faire est de laisser se terminer l’application.
Chaque étape de l’application étant susceptible de lever une exception, la structure de la fonction main()
devrait être la suivante :
... int main (...) { try { validerArguments(); initialisation(); phase_1(); ... phase_N(); terminaison(); return compteRenduDeSucces ; // = 0 } catch (...a_preciser...) { affichageApproprie (); return compteRenduDEchec ; // O < n < 255 } } // main()
Tout objet ou toute valeur peut servir d’exception.
Cependant, pour suivre les recommandations de la norme de C++ et de tous les spécialistes, les seules exceptions à utiliser devraient être les exceptions standard ou de classes dérivées par l’utilisateur de la classe standard exception
, ce qui est le cas de notre classe CException
.
Nous nous limiterons donc à capturer trois sortes d’exceptions :
- les exceptions standard,
- nos exceptions
CException
, - toutes les autres.
En cas de capture d’une exception inconnue, nous utiliserons la constante KExcInconnue
(= 255
) comme compteRenduDEchec
.
En cas de capture d’une exception standard exception
, nous utiliserons la constante KExcStd
(= 254
) comme compteRenduDEchec
,
En cas de capture d’une exception CException
, nous utiliserons la donnée-membre myCodErr
comme compteRenduDEchec
.
En cas de déroulement normal, nous utiliserons les constantes KNoError
ou KNoExc
(= 0
) comme compteRenduDeSucces
.
Nous considèrerons que toutes les fonctions de niveau immédiatement inférieur à main()
(validerArguments()
, initialisation()
, phase_1()
, phase_N()
, terminaison()
dans l’exemple ci-dessus, testXxx()
dans les TPs habituels – comme nous l’avons toujours fait), sont susceptibles de lever n’importe quelle exception.
En conséquence, nous ne donnerons aucune indication d’exception dans leur profil :
void testXxx (void) { ...
et non
void testXxx (void) throw (n_importe_quoi) { ...
Travail à réaliser
Créez le projet ExceptionsInMain
.
Y copier le contenu des fichiers CException.h
,
/** * * \file CException.h * * \authors M. Laporte, D. Mathieu * * \date 10/02/2011 * * \version V1.0 * * \brief Declaration de la classe CException * **/ #ifndef __CEXCEPTION_H__ #define __CEXCEPTION_H__ #include <exception> #include <string> #include "CstCodErr.h" namespace nsUtil { class CException : public std::exception { std::string myLibelle; unsigned myCodErr; public : CException (const std::string & libelle = std::string(), const unsigned codErr = KNoExc) noexcept; virtual ~CException (void) noexcept; const std::string & getLibelle (void) const noexcept; unsigned getCodErr (void) const noexcept; virtual const char* what (void) const noexcept; void display (void) const; }; // CException } // namespace nsUtil #endif /* __CEXCEPTION_H__ */
CException.cpp
/** * * \file CException.cpp * * \authors M. Laporte, D. Mathieu * * \date 10/02/2011 * * \version V1.0 * * \brief classe CException * **/ #include <iostream> #include <string> #include "CstCodErr.h" #include "CException.h" using namespace std; #define CEXC nsUtil::CException //========================== // Classe nsUtil::CException //========================== CEXC::CException (const string & libelle /* = string () */, const unsigned codErr /* = KNoExc */) noexcept : myLibelle (libelle), myCodErr (codErr) {} const string & CEXC::getLibelle (void) const noexcept { return myLibelle; } // GetLibelle() unsigned CEXC::getCodErr (void) const noexcept { return myCodErr; } CEXC::~CException (void) noexcept {} const char* CEXC::what (void) const noexcept { return myLibelle.c_str(); } void CEXC::display (void) const { cout << "Exception : " << myLibelle << '\n' << "Code : " << myCodErr << endl; } // Afficher() #undef CEXC
et CstCodErr.h
/** * * \file CstCodErr.h * * \authors M. Laporte, D. Mathieu * * \date 10/02/2011 * * \version V1.0 * * \brief Codes d'erreurs * **/ #ifndef __CSTCODERR_H__ #define __CSTCODERR_H__ namespace nsUtil { enum {KNoExc = 0, KNoError = 0, KExcStd = 254, KExcInconnue = 255 }; } // namespace nsUtil #endif /* __CSTCODERR_H__ */
(toutes les constantes représentant des codes d’erreurs, quels qu’ils soient, dans toutes les applications futures qui vous seront proposées, seront ajoutées dans ce fichier qui sera donc inclus très fréquemment).
Dans l’espace de noms anonyme du fichier testExceptionsInMain.cpp
, écrire la fonction testExceptionsInMain()
qui lève (schéma de choix) :
- soit une exception de base, en appelant le constructeur
exception()
de la classeexception
, - soit une exception standard plus spécifique,
- soit directement en appelant par exemple le constructeur
runtime_error()
de la classeruntime_error
et en lui passant un paramètre effectif (un libellé), - soit indirectement en appelant par une fonction dont vous savez qu’elle lève un exception standard, par exemple la fonction
at()
de la classestring
avec un indice invalide, qui lève une exceptionout_of_range
,
- soit directement en appelant par exemple le constructeur
- soit une exception
CException
, - soit une exception quelconque (un entier par exemple).
Dans la fonction main()
, effectuer les modifications correspondantes aux indications ci-dessus en capturant dans l’ordre :
- les exceptions
CException
, - les exceptions spécifiques standard (
runtime_error
ou autre), - les exceptions standard (
exception
), - toutes les autres exceptions.
Compilez et testez.
Variante 1
Dans la fonction main()
, permutez la capture des exceptions CException
et des exceptions runtime_error
et testez à nouveau.
Vous devez ne constater aucun changement.
Variante 2
Avant la capture de toute exception (catch (...)
), ajoutez la capture d’un unsigned
et levez une exception entière.
Vous constatez qu’elle n’est pas capturée : il n’y a aucun transtypage/conversion entre les exceptions levées et les exceptions capturées.
Remplacez la capture d’un unsigned
par celle d’un int
et répétez le test.
L’exception est correctement capturée.
Variante 3
Dans la fonction main()
, permutez la capture des exceptions exception
et des exceptions runtime_error
et testez à nouveau.
Vous devez constater que les exceptions runtime_error
ne sont plus jamais capturées (ce qui vous est d’ailleurs indiqué par un message warning
lors de la compilation).
Ceci est dû au fait qu’une exception runtime_error
est d’une classe dérivée de exception
, donc plus spécifique, et qu’une instruction catch
qui capture les exceptions exception
capturent aussi les exceptions dérivées.
Vous remarquez cependant que, bien que capturant apparemment une exception
, le programme affiche cependant le
string
que vous avez passé au constructeur de runtime_error
.
C’est du polymorphisme.
En conséquence, vous ne garderez à l’avenir que les captures des deux exceptions CException
et exception
, et toutes les autres.
Variante 4
Au lieu de lever directement une exception runtime_error
, provoquez une exception par une fonction standard : utilisez la fonction at()
membre de la classe string
avec une valeur d’indice invalide.
Compilez et testez
Ne pas oublier de sauvegarder les fichiers sources du projet sur github
.