Pourquoi les exceptions existent
À la page précédente, vous avez utilisé enum class pour donner des noms parlants aux états d'erreur. C'est parfait pour les résultats qu'une fonction prévoit et que l'appelant est censé inspecter. Mais certains échecs sont différents : une fonction au fond de votre pile d'appels découvre qu'un fichier ne s'ouvre pas, ou qu'un argument n'a aucun sens, et elle n'a aucune idée de ce que le programme devrait faire à ce sujet. Renvoyer un code d'erreur ne fonctionne que si chaque appelant de la chaîne pense à le vérifier et à le faire remonter. Ratez une seule vérification et le programme poursuit sa route avec des données corrompues.
Les exceptions résolvent cela. Lorsqu'un problème survient, vous lancez un objet avec throw. L'exécution s'arrête immédiatement, la pile se déroule (le destructeur de chaque objet local situé entre le lancer et le gestionnaire est exécuté) et le contrôle saute au catch correspondant le plus proche. Une exception non traitée ne peut pas être ignorée silencieusement : si rien ne l'attrape, le programme appelle std::terminate et avorte.
Cette page se concentre sur le côté lanceur : les objets d'erreur eux-mêmes. La page suivante creuse en détail la mécanique du try/catch.
Le lancer et le message de what()
Techniquement, vous pouvez lancer (throw) n'importe quelle valeur —throw 42; ou throw "oops"; sont légaux— mais ne le faites pas. La convention que tout le monde suit est de lancer un objet dérivé de std::exception. Cette classe de base déclare une seule méthode virtuelle, what(), qui renvoie une description const char* du problème. Respecter la convention signifie qu'un seul catch (const std::exception& e) peut tout traiter.
L'en-tête <stdexcept> vous fournit des types prêts à l'emploi dont le constructeur prend le message :
Remarquez que what() renvoie exactement la chaîne avec laquelle vous avez construit l'exception. Remarquez aussi que nous l'avons attrapée par const exception& alors que nous avons lancé un runtime_error : cela fonctionne parce que runtime_error est un std::exception (une relation que vous reconnaîtrez de la page sur l'héritage).
La hiérarchie d'exceptions standard
Avant d'écrire votre propre type d'exception, vérifiez si la bibliothèque standard en a déjà un qui convient. Ils héritent tous de std::exception et se répartissent en deux familles dans <stdexcept> :
logic_error— un défaut dans la logique du programme qui pourrait, en principe, être détecté avant l'exécution. Ses sous-types incluentinvalid_argument,out_of_range,domain_erroretlength_error.runtime_error— un échec qui n'apparaît qu'à l'exécution et qui n'est pas une erreur de programmation à proprement parler. Ses sous-types incluentrange_error,overflow_erroretunderflow_error.
De nombreuses fonctions de bibliothèque les lancent à votre place. Par exemple, std::vector::at() effectue une vérification des bornes et lance out_of_range plutôt que de vous laisser lire au-delà de la fin :
Ce at() est l'équivalent sûr de v[9]. Le simple operator[] n'effectue aucune vérification des bornes : lire v[9] ici est un comportement indéfini, pas une exception. Choisir at() est la façon de transformer une corruption silencieuse en une erreur que l'on peut attraper.
Choisissez le type qui décrit l'erreur : invalid_argument lorsqu'un appelant passe quelque chose d'absurde, out_of_range pour les problèmes d'indice/de clé, runtime_error pour « le monde extérieur m'a fait défaut ».
Écrire votre propre type d'exception
Lorsque aucun type standard ne convient —vous voulez attacher des données supplémentaires, ou attraper (catch) spécifiquement votre erreur et rien d'autre— définissez une classe qui hérite de std::exception (ou de l'un de ses sous-types) et redéfinissez what(). Hériter de std::runtime_error est la voie la plus simple, car il stocke déjà le message et implémente what() à votre place :
Comme NetworkError transporte un code de statut, le gestionnaire peut y réagir : réessayer sur un 5xx, abandonner sur un 4xx. Une simple chaîne d'erreur ne pourrait pas le faire. Le type personnalisé permet aussi à un catch (const NetworkError&) de n'attraper que les problèmes réseau et de laisser tout le reste au gestionnaire plus général situé en dessous.
Si un jour vous héritez directement de std::exception (et non de runtime_error), pensez à redéfinir what() vous-même et à le marquer noexcept pour correspondre à la signature de la base :
class ParseError : public std::exception {
public:
const char* what() const noexcept override {
return "failed to parse input";
}
};
Lancez par valeur, attrapez par référence
C'est la règle la plus importante des exceptions en C++, et celle que les débutants ratent. Lancez les objets par valeur et attrapez-les par référence const.
throw runtime_error("oops"); // par valeur - correct
catch (const runtime_error& e) { ... } // par référence const - correct
Attraper par valeur à la place —catch (std::exception e)— copie l'exception dans un objet de la classe de base et tronque la partie dérivée. Après la troncature, e.what() appelle l'implémentation de la base, et non la vôtre redéfinie, si bien que votre message soigneusement élaboré disparaît :
try {
throw NetworkError(503, "service unavailable");
} catch (std::exception e) { // par valeur - troncature d'objet !
std::cout << e.what(); // message générique, status() a disparu
}
La référence (&) préserve le vrai type dynamique, de sorte que le what() virtuel est dispatché correctement et que vous pouvez toujours accéder aux membres dérivés. Ajoutez const car vous ne faites que lire l'exception, pas la modifier. Ne lancez jamais un pointeur (throw new runtime_error(...)) : celui qui l'attrape devrait faire un delete, mais sur quel chemin d'exécution ? C'est exactement la fuite que les exceptions sont censées éviter.
Suivant : try-catch
Vous savez désormais créer et lancer (throw) des exceptions bien formées et choisir le bon type standard pour chaque échec. L'autre moitié de l'histoire, c'est le côté capture. La page suivante couvre try/catch en entier : ordonner plusieurs blocs catch du plus spécifique au plus général, le fourre-tout catch (...), le relancement avec un throw; nu, et comment RAII (repensez aux pointeurs intelligents) garantit que vos ressources sont libérées au fur et à mesure que la pile se déroule.
Questions fréquentes
Qu'est-ce qu'une exception en C++ ?
Une exception est un objet qui signale une erreur que la fonction courante ne peut pas traiter seule. Vous la lancez avec throw, la pile se déroule (détruisant les objets locaux au passage) et un bloc catch correspondant, situé plus haut, prend le relais. Cela sépare le code qui détecte un problème du code qui décide quoi en faire.
Quelle est la différence entre throw et return pour les erreurs ?
Une valeur de return doit être vérifiée par l'appelant, et il est facile de l'oublier : le programme continue alors avec des données erronées. Une exception lancée ne peut pas être ignorée : si personne ne l'attrape, le programme se termine. Les exceptions servent aux véritables échecs (un fichier ne s'ouvre pas, une entrée est invalide) ; les valeurs de retour restent appropriées pour les résultats ordinaires, y compris les cas attendus de « non trouvé ».
Que fait la méthode what() dans les exceptions C++ ?
Toute classe dérivée de std::exception fournit une méthode virtuelle what() qui renvoie un const char* décrivant l'erreur. Lorsque vous attrapez une exception, appeler e.what() vous donne le message lisible que vous pouvez journaliser ou afficher. Les types d'exception standard le définissent à partir de la chaîne que vous passez à leur constructeur.