ELCa11 - BE #4 — Gestion d’erreurs avec exceptions et héritage¶
Cours associé : Cours 4 — Gestion d’erreurs et héritage (voir le polycopié, section Cours 4).
Objectifs¶
- Lever et attraper des exceptions en C++ (
throw,try,catch). - Définir une classe d’exception personnalisée, héritant de
std::exception. - Mettre en œuvre l’héritage entre nos propres classes, avec méthode
virtualredéfinie dans les classes filles.
Prérequis¶
- BE #2 et BE #3 : classe
DamierDyncomplète avec opérateurs.
Principe¶
Le BE est en trois parties :
- Partie 1 — Lever et attraper une exception standard (
std::out_of_range). - Partie 2 — Définir une classe d’exception personnalisée plus expressive.
- Partie 3 — Mettre en place un héritage entre nos propres classes pour modéliser des damiers de jeux (dames, échecs).
À côté de cet énoncé, vous trouverez les codes des trois exemples vus en cours : Exceptions1.zip (premier
throw/catch), Exceptions2.zip (exception sur un vecteur) et Exceptions3.zip (classe d’exception personnalisée). Ils illustrent les concepts de ce BE.
Partie 1 — Lever une exception standard¶
Notions à maîtriser
- Les mots-clés
throw,try,catch; - la hiérarchie des exceptions standard du C++ ;
- la classe
std::out_of_rangeet ses parents.
Mini-encart pédagogique : la hiérarchie des exceptions standard¶
La bibliothèque standard fournit une arborescence de classes d’exceptions (en-tête <stdexcept>), toutes héritant de std::exception :
std::exception
├── std::runtime_error (erreurs détectées à l'exécution)
│ ├── std::range_error
│ ├── std::overflow_error
│ └── std::underflow_error
└── std::logic_error (erreurs imputables au programmeur)
├── std::invalid_argument
├── std::domain_error
├── std::length_error
└── std::out_of_range ← celle qu'on utilise ici
Toutes ces classes possèdent une méthode const char* what() const noexcept; qui renvoie un message d’erreur descriptif. On les construit avec un std::string : throw std::out_of_range("message");.
L’intérêt de la hiérarchie : on peut attraper toutes les exceptions standards d’un coup avec catch (const std::exception& e), ou bien attraper finement un type particulier avec catch (const std::out_of_range& e).
Contexte¶
Vous allez créer une nouvelle classe DamierExc, dérivée de DamierDyn par recopie (et pas par héritage — l’héritage viendra en partie 3). Cette duplication peut sembler artificielle, mais elle nous permet de faire évoluer librement DamierExc (ajout de la borne en partie 2, héritage en partie 3) sans modifier DamierDyn.
Travail à réaliser¶
- Créez un nouveau projet Qt Creator nommé
damierexc. - Recopiez le code de
DamierDyndu BE #3 vers de nouveaux fichiersdamierexc.hetdamierexc.cpp. Renommez la classe (et toutes ses méthodes) enDamierExc. -
Modifiez la méthode
Set(int x, int y, int val): si(x, y)est en dehors du damier, levez une exceptionstd::out_of_range: -
Modifiez aussi
Get(int x, int y)(introduit au BE #2) pour lever la même exception en cas d’accès hors damier — plus besoin de retourner0par convention !
Programme de test¶
Dans main(), attrapez l’exception :
#include "damierexc.h"
#include <iostream>
int main() {
DamierExc D(3, 5);
try {
D.Set(2, -1, 8); // -1 hors bornes : lève std::out_of_range
} catch (const std::out_of_range& e) {
std::cerr << "Exception attrapée : " << e.what() << std::endl;
}
std::cout << "Le programme continue." << std::endl;
return 0;
}
Vérifiez que le programme :
- affiche bien le message d’erreur sur
cerr; - continue son exécution après le
catch(contrairement àstd::exit(1)du BE #3, qui terminait brutalement).
Points à noter¶
throw Xlève une exception ; le flot de contrôle remonte la pile d’appels jusqu’à uncatchcorrespondant.- En remontant, les destructeurs des objets locaux sont appelés (mécanisme appelé stack unwinding) : pas de fuite de mémoire, c’est le grand avantage par rapport à
std::exit. catch (const std::out_of_range& e): on attrape par référence constante, idiome standard.
Questions à se poser¶
- Que se passe-t-il si une exception est levée mais qu’aucun
catchne la rattrape ? (réponse :std::terminate()est appelé, le programme s’arrête). - Si on remplace
catch (const std::out_of_range& e)parcatch (const std::exception& e), le code fonctionne-t-il toujours ? Pourquoi ? - Pourquoi attraper par référence (
const std::exception& e) plutôt que par valeur (std::exception e) ?
Partie 2 — Définir sa propre classe d’exception¶
Notions à maîtriser
- Héritage de
std::exception; - redéfinition de la méthode virtuelle
what(); - mot-clé
noexcept; - macros prédéfinies
__FILE__et__func__.
Contexte¶
std::out_of_range est pratique mais limité : son seul “argument” est un message texte. On veut une exception plus riche, qui transporte du contexte structuré (valeur rejetée, borne, lieu de l’erreur).
Par ailleurs, on veut imposer une borne supérieure sur les valeurs stockables dans le damier. Par exemple, avec une borne de 4, les valeurs admissibles dans chaque case seront dans [0, 4].
Travail à réaliser¶
1. Adapter DamierExc pour gérer une borne¶
- Ajoutez un attribut
int Borne;à la classe. -
Modifiez le constructeur pour recevoir la borne en argument supplémentaire :
-
Modifiez
Set,Initetoperator+=(int c)pour vérifier que la valeur stockée reste dans[0, Borne]. - Pour
operator+=(const DamierExc&)etoperator+, l’addition est autorisée mais la borne du résultat devient la somme des deux bornes.
2. Créer la classe ExceptionDamier¶
Créez deux nouveaux fichiers exceptiondamier.h et exceptiondamier.cpp. La classe hérite de std::exception :
#include <exception>
#include <string>
class ExceptionDamier : public std::exception {
public:
ExceptionDamier(int borne, int valeur,
const std::string& fichier,
const std::string& fonction) noexcept;
const char* what() const noexcept override;
private:
int Borne;
int Valeur;
std::string Fichier;
std::string Fonction;
std::string Message; // construit dans le constructeur, retourné par what()
};
3. Lever ExceptionDamier¶
Lors d’une tentative de stockage hors bornes, lancez :
__FILE__est une macro qui renvoie le nom du fichier source courant.__func__est une constante prédéfinie (C++11) qui renvoie le nom de la fonction courante.
L’appel à what() doit retourner un message du type :
Programme de test¶
#include "damierexc.h"
#include "exceptiondamier.h"
#include <iostream>
int main() {
DamierExc D(3, 5, 4); // damier (3,5) avec borne 4
D.Set(1, 2, 3); // OK
try {
D.Init(6); // 6 > 4 : lève ExceptionDamier
} catch (const ExceptionDamier& e) {
std::cerr << e.what() << std::endl;
}
return 0;
}
Points à noter¶
noexcept: promet au compilateur que la fonction ne lèvera aucune exception. Le constructeur d’une exception, et sa méthodewhat(), doivent eux-mêmes êtrenoexcept— on ne veut pas qu’ils lèvent une exception lors de la levée d’une exception !override: indique au compilateur qu’on redéfinit une méthode virtuelle de la classe parente. Si la signature ne correspond pas exactement, le compilateur émet une erreur — utile pour éviter les fautes de frappe silencieuses.- L’héritage
class ExceptionDamier : public std::exceptionpermet à uncatch (const std::exception& e)d’attraper aussi nosExceptionDamier— c’est le principe du polymorphisme (vu plus en détail en partie 3).
Aparté —
noexceptvsthrow(). La syntaxevoid f() throw();(vue dans certains tutoriels anciens) avait le même rôle quenoexcept, mais elle est dépréciée depuis C++11 et supprimée depuis C++17. Toujours utilisernoexceptaujourd’hui.
Questions à se poser¶
- Pourquoi
Message(et pas la concaténation à chaque appel) est-il un attribut de la classe ? (indice : la durée de vie duconst char*retourné parwhat()) - Que se passe-t-il si on oublie le
overridedans la déclaration dewhat()? - Pourquoi
what()ne peut-elle pas être déclaréenoexcept(false)?
Partie 3 — Héritage et polymorphisme¶
Notions à maîtriser
- Héritage public ;
- visibilité
protected(intermédiaire entreprivateetpublic) ; - méthode virtuelle (
virtual) ; - redéfinition dans les classes filles avec
override.
Contexte¶
Notre DamierExc est suffisamment générale pour servir de base à plusieurs damiers de jeux : dames, échecs, go… Plutôt que de dupliquer son code dans DamierDame, DamierEchec et DamierGo, on va hériter de DamierExc. Chaque jeu apportera juste les spécificités qui lui sont propres : ses dimensions, sa borne, et la disposition initiale des pièces.
Conventions de représentation — chaque jeu a sa propre table.
Dames (10 × 10), borne 4 :
| Valeur | Signification |
|---|---|
0 | case vide |
1 | pion noir |
2 | pion blanc |
3 | dame promue noire |
4 | dame promue blanche |
Échecs (8 × 8), borne 12 :
| Valeur | Pièce blanche | Valeur | Pièce noire | |
|---|---|---|---|---|
1 | pion | 7 | pion | |
2 | tour | 8 | tour | |
3 | cavalier | 9 | cavalier | |
4 | fou | 10 | fou | |
5 | dame | 11 | dame | |
6 | roi | 12 | roi |
(0 = case vide.)
Go (19 × 19), borne 2 :
| Valeur | Signification |
|---|---|
0 | intersection vide |
1 | pierre noire |
2 | pierre blanche |
Travail à réaliser¶
1. Adapter DamierExc pour l’héritage¶
- Passez les attributs
L,C,Borne,Tdeprivate:àprotected:, pour que les classes filles y aient accès. -
Ajoutez une méthode virtuelle pour l’initialisation des pièces :
2. Classe DamierDame (10 × 10)¶
Dans des fichiers damierdame.h / damierdame.cpp, créez :
Le constructeur appelle celui de la classe parente avec les bonnes dimensions et la bonne borne, via la liste d’initialisation :
Important — l’appel au constructeur parent doit se faire dans la liste d’initialisation (le
: DamierExc(...)avant le{). Il n’y a aucune autre syntaxe pour le faire. Si vous omettez cet appel et que la classe parente n’a pas de constructeur sans argument (c’est le cas ici :DamierExc(int, int, int, int)), le code ne compilera pas. La liste d’initialisation a été introduite au BE #2 (partie 2, étape 1) — relisez si besoin.
InitJeu() place les pions de départ : 4 rangées de pions noirs en haut (valeur 1) sur les cases sombres, 4 rangées de pions blancs en bas (valeur 2) sur les cases sombres, 2 rangées vides au milieu. Les dames promues (3 / 4) n’apparaissent qu’en cours de partie.
3. Classe DamierEchec (8 × 8)¶
Constructeur : DamierEchec() : DamierExc(8, 8, 12, 0) { InitJeu(); }.
InitJeu() place les pièces en position de départ — voir le tableau de représentation des pièces ci-dessus. Pour la rangée 0 (noire), l’ordre traditionnel est : tour (8), cavalier (9), fou (10), dame (11), roi (12), fou (10), cavalier (9), tour (8). La rangée 1 est entièrement composée de pions noirs (7). Les rangées 6 (pions blancs, 1) et 7 (pièces blanches, ordre symétrique : 2, 3, 4, 5, 6, 4, 3, 2) sont disposées de manière analogue. Les rangées 2 à 5 sont vides.
4. (Bonus) Classe DamierGo (19 × 19)¶
Constructeur : DamierGo() : DamierExc(19, 19, 2, 0) { InitJeu(); }. Au Go, le plateau de départ est vide ; InitJeu() ne fait rien (ou on peut s’en passer si l’initialisation à 0 du parent suffit).
Programme de test¶
#include "damierdame.h"
#include "damierechec.h"
#include <iostream>
int main() {
DamierDame Dames;
DamierEchec Echecs;
std::cout << "Damier de dames :" << std::endl << Dames << std::endl;
std::cout << "Damier d'échecs :" << std::endl << Echecs << std::endl;
return 0;
}
Encart — Pour aller plus loin : classe abstraite
Dans notre conception,
DamierExcreste concrète : on peut écrireDamierExc D(3, 5, 4);et l’instancier directement. C’est un choix qui maintient la cohérence avec la Partie 2.Une conception plus pure aurait été d’introduire une classe abstraite
DamierJeu, intermédiaire entreDamierExcetDamierDame/DamierEchec/DamierGo. On y déclareraitInitJeu()comme virtuelle pure :Une classe contenant au moins une méthode virtuelle pure est dite abstraite : on ne peut pas l’instancier. Toute classe fille concrète doit fournir une implémentation des méthodes virtuelles pures.
Cette architecture force chaque jeu à définir son
InitJeu()(le compilateur le vérifie). C’est une bonne pratique pour des hiérarchies riches, mais on n’ira pas jusque-là dans ce TD.
Points à noter¶
public DamierExc: héritage public — la classe fille hérite de l’interface publique du parent. C’est l’héritage qu’on utilise 99 % du temps.protected:: les attributs sont accessibles depuis la classe fille, mais pas depuis l’extérieur. Plus permissif queprivate:, plus restrictif quepublic:.virtualsurInitJeu()dans la classe parente : autorise la redéfinition dans les classes filles.overridedans la classe fille : indique qu’on redéfinit une méthode virtuelle. Permet au compilateur de vérifier la cohérence des signatures.- Le constructeur d’une classe fille doit toujours commencer par appeler un constructeur du parent (explicitement avec
: DamierExc(...)ou implicitement si le parent a un constructeur sans argument).
Questions à se poser¶
- Pourquoi passer
T,L,Cenprotected:plutôt que de les laisser enprivate:? Quelles seraient les conséquences ? - Que se passe-t-il si on instancie un
DamierDamemais qu’on appelleInitJeu()via un pointeur surDamierExc(DamierExc* p = new DamierDame; p->InitJeu();) ? Quelle version est appelée, et pourquoi le mot-clévirtualest-il essentiel ici ? - Pourrait-on avoir une méthode
InitJeu()non virtuelle ? Que se passerait-il alors ?